Compare commits

..

1 Commits

Author SHA1 Message Date
john fb6865638a fix: align DeepSeek canary tool rounds with gate
Memind CI / Test, build, and release guards (pull_request) Successful in 1m32s
2026-07-26 23:49:32 +08:00
19 changed files with 519 additions and 20 deletions
+3
View File
@@ -6,6 +6,7 @@ import { Agent, fetch as undiciFetch } from 'undici';
export const DEFAULT_DEEPSEEK_NO_THINK_PORT = 18036;
export const DEFAULT_DEEPSEEK_UPSTREAM = 'https://api.deepseek.com';
export const DEFAULT_MOONSHOT_UPSTREAM = 'https://api.moonshot.cn';
export const DEEPSEEK_COMPAT_CONTRACT_VERSION = 1;
const insecureDispatcher = new Agent({
connect: { rejectUnauthorized: false },
@@ -256,6 +257,8 @@ export function createDeepseekNoThinkProxy({
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
ok: true,
contractVersion: DEEPSEEK_COMPAT_CONTRACT_VERSION,
deepseekThinking: 'disabled',
upstream: normalizedUpstream,
moonshotUpstream: normalizedMoonshotUpstream,
}));
+21
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
DEEPSEEK_COMPAT_CONTRACT_VERSION,
decodedUpstreamResponseHeaders,
deepseekDisableThinkingEnabled,
flattenLocalJsonSchemaRefs,
@@ -10,6 +11,7 @@ import {
resolveDeepseekNoThinkProxyBaseUrl,
resolveMoonshotCompatProxyBaseUrl,
sanitizeMoonshotToolSchemas,
startDeepseekNoThinkProxy,
} from './deepseek-no-think-proxy.mjs';
test('injectDeepseekThinkingDisabled adds thinking.disabled when absent', () => {
@@ -168,3 +170,22 @@ test('decodedUpstreamResponseHeaders drops stale compression metadata', () => {
'content-type': 'text/event-stream',
});
});
test('compatibility proxy health identifies the enforced production contract', async (t) => {
const server = await startDeepseekNoThinkProxy({
host: '127.0.0.1',
port: 0,
logger: { info() {}, warn() {}, error() {} },
});
t.after(() => new Promise((resolve) => server.close(resolve)));
const address = server.address();
const response = await fetch(`http://127.0.0.1:${address.port}/health`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
ok: true,
contractVersion: DEEPSEEK_COMPAT_CONTRACT_VERSION,
deepseekThinking: 'disabled',
upstream: 'https://api.deepseek.com',
moonshotUpstream: 'https://api.moonshot.cn',
});
});
+22 -7
View File
@@ -11,6 +11,7 @@ Portal 的首个生产动作必须是用户级灰度,不能直接整包替换
| 灰度边缘隧道 | `105 127.0.0.1:19082` | 反向 SSH 到 103 的 18082;不复用已退役的 19081 链路 |
| 候选 Portal | `127.0.0.1:18081` | 独立、无源码 runtime;使用被动运行时模式 |
| 候选 goosed | `127.0.0.1:18015` | 独立容器并挂载候选 runtime,不重建稳定 9 个容器 |
| DeepSeek 兼容代理 | `103 0.0.0.0:18036` | 仅供宿主机和候选容器使用;不接入 105 或公网入口 |
候选 Portal 通过 `MEMIND_PORTAL_H5_ROOT` 共享经审计的持久数据和数据库,但
`MEMIND_RUNTIME_ROLE=candidate` 会禁止重复启动 MindSpace、Plaza、提醒、订阅续期和
@@ -23,6 +24,18 @@ Agent 恢复扫描等单例后台任务。
`Failed to add extension` 和宿主机绝对路径的 `MODULE_NOT_FOUND`,应先核对候选
进程环境与 `/opt/portal` 容器挂载,禁止把宿主机文件复制进容器临时修补。
DeepSeek V4 的工具轮次还必须经过候选 runtime 中的
`deepseek-no-think-proxy.mjs`。候选启动脚本必须强制
`MEMIND_DEEPSEEK_DISABLE_THINKING=1`,并把 goosed 自定义 provider 指向
`http://host.docker.internal:18036/v1`。该代理通过独立 LaunchAgent 管理;
发布必须同时验证 `/health` 的兼容契约和候选容器到该地址的连通性。灰度路由器把
该健康地址纳入候选健康闭包,代理掉线时必须自动回落稳定 Portal。
若日志出现
`The reasoning_content in the thinking mode must be passed back to the API.`
说明 DeepSeek 请求绕过了兼容契约。禁止只在线补环境变量:必须检查 artifact、
LaunchAgent、Portal provider 同步和路由器依赖健康配置是否属于同一 release。
## 身份和回落规则
- `john`:按数据库中的唯一用户名或用户 ID 匹配。
@@ -32,6 +45,7 @@ Agent 恢复扫描等单例后台任务。
- 微信回调通过 `FromUserName``h5_user_wechat_identities` 只读解析到绑定账号,再用唯一账号标识匹配。
- 未命中、无身份、过期/撤销会话、数据库异常:走稳定版本。
- 候选 `/api/status` 不健康:自动走稳定版本。
- DeepSeek 兼容代理不健康:自动走稳定版本。
- SSE、附件、公开页和普通 API 均由同一反向代理转发;微信 XML body 必须原样保留。
## 发布入口
@@ -49,12 +63,13 @@ bash scripts/release-portal-canary-prod.sh --yes
2. 执行 103 只读预检。
3. 上传经过验证的候选包。
4. 创建稳定 live 全量备份、持久目录备份和 105 两份活动 nginx 配置备份,并校验完整性。
5. 启动独立候选 goosed、候选 Portal 和灰度路由器
6. 验证不可变身份配置、稳定/候选健康和路由器诊断
7. 启动独立的 `105:19082 -> 103:18082` 灰度隧道并从 105 验证
8. 短暂阻止新 Agent Run,排空活动任务
9. 逐文件原子替换、验证并 reload 105 nginx,把活动上游从 `58.38.22.103:8081` 切到 `127.0.0.1:19082`
10. 主动停止候选,证明目标身份会自动回落稳定版,再恢复候选
5. 启动独立候选 goosed 和 DeepSeek 兼容代理,验证兼容契约及容器连通性
6. 启动候选 Portal 和灰度路由器。
7. 验证不可变身份配置、稳定/候选/依赖健康和路由器诊断
8. 启动独立的 `105:19082 -> 103:18082` 灰度隧道并从 105 验证
9. 短暂阻止新 Agent Run,排空活动任务
10. 逐文件原子替换、验证并 reload 105 nginx,把活动上游从 `58.38.22.103:8081` 切到 `127.0.0.1:19082`
11. 主动停止候选,证明目标身份会自动回落稳定版,再恢复候选。
任一步失败都会把 105 nginx 恢复到稳定 `58.38.22.103:8081`,停止灰度隧道和候选服务,
并保留候选目录与备份证据。活动 nginx 配置变更只能由已提交的发布脚本执行,禁止在线手改。
@@ -68,7 +83,7 @@ bash scripts/rollback-portal-canary-prod.sh --yes
回滚只执行以下动作:
- 把 105 两份活动 nginx 上游恢复到稳定 Portal `58.38.22.103:8081` 并验证 reload。
- 停止灰度隧道、灰度路由器、候选 Portal候选 goosed。
- 停止灰度隧道、灰度路由器、候选 Portal候选 goosed 和 DeepSeek 兼容代理
- 清除发布排水标记。
- 验证稳定 Portal 和 105 HTTPS 入口健康。
+31
View File
@@ -210,6 +210,37 @@ Provider 'custom_deepseek' is not configured
注意:DeepSeek API key / provider secret 是运行配置,不应该烘焙进镜像。
### 4.7 DeepSeek 工具轮次兼容必须与生产拓扑一致
DeepSeek V4 思考模式在工具调用后的下一轮要求客户端回传上一轮
`reasoning_content`。当前 goosed 工具链不能保证完整回传,因此生产候选必须使用
artifact 内的 `deepseek-no-think-proxy.mjs`,由独立 LaunchAgent 在 `18036`
启动,并强制候选 Portal 设置:
```bash
MEMIND_DEEPSEEK_DISABLE_THINKING=1
MEMIND_DEEPSEEK_NO_THINK_PORT=18036
MEMIND_GOOSED_HOST_GATEWAY=host.docker.internal
```
发布不能只看 Portal `/api/status`。还必须验证:
- `http://127.0.0.1:18036/health` 返回兼容契约版本和
`deepseekThinking=disabled`
- `goosed-prod-canary` 能访问
`http://host.docker.internal:18036/health`
- 灰度路由器把该地址纳入候选健康闭包;
- 失败清理和回滚会停止对应 LaunchAgent。
Gate 本地栈和生产候选必须使用同一组开关与进程依赖。若出现:
```text
The reasoning_content in the thinking mode must be passed back to the API.
```
说明实际生产请求绕过了兼容契约;即使场景 Gate 全部通过,也必须阻断发布并排查
环境漂移。
## 5. goosed-prod 镜像更新必看
当前已验证镜像:
+3 -3
View File
@@ -800,13 +800,13 @@ test('syncProfileToGoosed writes provider, model and secret keys for builtin', a
);
});
test('syncProfileToGoosed routes local DeepSeek through the no-thinking proxy', async () => {
test('syncProfileToGoosed routes explicitly protected production DeepSeek through the compatibility proxy', async () => {
const previousProfile = process.env.MEMIND_RUNTIME_PROFILE;
const previousDisableThinking = process.env.MEMIND_DEEPSEEK_DISABLE_THINKING;
const previousProxyBase = process.env.MEMIND_DEEPSEEK_NO_THINK_BASE_URL;
const previousGateway = process.env.MEMIND_GOOSED_HOST_GATEWAY;
process.env.MEMIND_RUNTIME_PROFILE = 'local';
delete process.env.MEMIND_DEEPSEEK_DISABLE_THINKING;
process.env.MEMIND_RUNTIME_PROFILE = 'production';
process.env.MEMIND_DEEPSEEK_DISABLE_THINKING = '1';
delete process.env.MEMIND_DEEPSEEK_NO_THINK_BASE_URL;
process.env.MEMIND_GOOSED_HOST_GATEWAY = 'host.lima.internal';
+46 -2
View File
@@ -244,9 +244,43 @@ function appendForwardedFor(current, remoteAddress) {
return values.join(', ');
}
function dependencyHealthRequest(target, timeoutMs) {
return new Promise((resolve) => {
const request = http.request(
{
hostname: target.hostname,
port: target.port,
method: 'GET',
path: `${target.pathname}${target.search}`,
headers: { connection: 'close' },
},
(response) => {
response.resume();
response.once('end', () => resolve(response.statusCode === 200));
},
);
request.setTimeout(timeoutMs, () => request.destroy());
request.once('error', () => resolve(false));
request.end();
});
}
function normalizeLoopbackHealthTarget(value, label) {
const target = new URL(String(value ?? ''));
if (
target.protocol !== 'http:'
|| !['127.0.0.1', 'localhost', '::1'].includes(target.hostname)
) {
throw new Error(`${label} must be a loopback http URL`);
}
target.hash = '';
return target;
}
export function createMemindCanaryProxy({
stableTarget,
candidateTarget,
candidateHealthDependencies = [],
policy,
identityResolver,
diagnosticSecret,
@@ -256,6 +290,8 @@ export function createMemindCanaryProxy({
} = {}) {
const stable = normalizeLoopbackTarget(stableTarget, 'stable target');
const candidate = normalizeLoopbackTarget(candidateTarget, 'candidate target');
const candidateDependencies = candidateHealthDependencies.map((value, index) =>
normalizeLoopbackHealthTarget(value, `candidate health dependency ${index + 1}`));
if (!policy) throw new Error('canary proxy requires a routing policy');
if (!identityResolver) throw new Error('canary proxy requires an identity resolver');
if (!diagnosticSecret) throw new Error('canary proxy requires a diagnostic secret');
@@ -269,8 +305,13 @@ export function createMemindCanaryProxy({
return healthCache.healthy;
}
if (!healthPromise) {
healthPromise = healthRequest(candidate, candidateHealthTimeoutMs, 'candidate')
.then((healthy) => {
healthPromise = Promise.all([
healthRequest(candidate, candidateHealthTimeoutMs, 'candidate'),
...candidateDependencies.map((target) =>
dependencyHealthRequest(target, candidateHealthTimeoutMs)),
])
.then((results) => {
const healthy = results.every(Boolean);
healthCache = { checkedAt: Date.now(), healthy };
return healthy;
})
@@ -539,6 +580,9 @@ async function main() {
stableTarget: process.env.MEMIND_CANARY_STABLE_URL || 'http://127.0.0.1:8081',
candidateTarget:
process.env.MEMIND_CANARY_CANDIDATE_URL || 'http://127.0.0.1:18081',
candidateHealthDependencies: csvValues(
process.env.MEMIND_CANARY_CANDIDATE_HEALTH_URLS,
),
policy,
identityResolver,
diagnosticSecret: process.env.MEMIND_CANARY_DIAGNOSTIC_SECRET,
+48
View File
@@ -287,6 +287,54 @@ test('identity lookup errors fail closed to stable', async (t) => {
assert.equal(response.headers['x-memind-runtime-route'], 'stable');
});
test('candidate health fails closed when its DeepSeek compatibility dependency is down', async (t) => {
const stableServer = upstream('stable');
const candidateServer = upstream('candidate');
let dependencyHealthy = true;
const dependencyServer = http.createServer((req, res) => {
res.writeHead(req.url === '/health' && dependencyHealthy ? 200 : 503);
res.end();
});
const stableUrl = await listen(stableServer);
const candidateUrl = await listen(candidateServer);
const dependencyUrl = await listen(dependencyServer);
const proxy = createMemindCanaryProxy({
stableTarget: stableUrl,
candidateTarget: candidateUrl,
candidateHealthDependencies: [`${dependencyUrl}/health`],
policy: createCanaryPolicy({ usernames: ['john'] }),
identityResolver: {
async fromSessionToken() {
return { id: 'user-john', username: 'john' };
},
async fromWechatOpenid() {
return null;
},
},
diagnosticSecret: 'diagnostic-secret',
candidateHealthTtlMs: 0,
});
const proxyUrl = await listen(proxy.server);
t.after(async () => {
await close(proxy.server);
await close(stableServer);
await close(candidateServer);
await close(dependencyServer);
});
const candidate = await request(proxyUrl, {
headers: { cookie: 'tkmind_user_session=john-token' },
});
assert.equal(candidate.headers['x-memind-runtime-route'], 'candidate');
dependencyHealthy = false;
const fallback = await request(proxyUrl, {
headers: { cookie: 'tkmind_user_session=john-token' },
});
assert.equal(fallback.body, 'stable:');
assert.equal(fallback.headers['x-memind-runtime-route'], 'stable');
});
test('candidate health requires the explicit candidate runtime role', async (t) => {
const stableServer = upstream('stable');
const wrongRoleServer = upstream('stable');
+2
View File
@@ -6,6 +6,7 @@ import path from 'node:path';
export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([
'server.mjs',
'memind-canary-proxy.mjs',
'deepseek-no-think-proxy.mjs',
'wechat-mp.bundle.mjs',
'mindspace-sandbox-mcp.mjs',
'tkmind-search-mcp.mjs',
@@ -16,6 +17,7 @@ export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([
'scripts/run-memind-portal-prod.sh',
'scripts/run-memind-portal-candidate.sh',
'scripts/run-memind-canary-proxy-prod.sh',
'scripts/run-deepseek-compat-proxy-candidate.sh',
'scripts/goosed-canary.compose.yml',
'scripts/check-mindspace-public-links.mjs',
'scripts/load-env.mjs',
@@ -0,0 +1,89 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { startDeepseekNoThinkProxy } from '../deepseek-no-think-proxy.mjs';
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
test('sanitized production tool-round replay reaches DeepSeek with thinking disabled', async (t) => {
const forwarded = [];
const proxy = await startDeepseekNoThinkProxy({
host: '127.0.0.1',
port: 0,
logger: { info() {}, warn() {}, error() {} },
fetchImpl: async (_url, init) => {
forwarded.push(JSON.parse(String(init.body)));
return new Response(JSON.stringify({
choices: [{ message: { role: 'assistant', content: 'synthetic result' } }],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
});
},
});
t.after(() => new Promise((resolve) => proxy.close(resolve)));
const address = proxy.address();
const response = await fetch(
`http://127.0.0.1:${address.port}/v1/chat/completions`,
{
method: 'POST',
headers: {
authorization: 'Bearer synthetic-key',
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'deepseek-v4-pro',
thinking: { type: 'enabled' },
messages: [
{ role: 'user', content: 'Use the synthetic filesystem tool.' },
{ role: 'assistant', tool_calls: [{ id: 'call-1', type: 'function' }] },
{ role: 'tool', tool_call_id: 'call-1', content: 'synthetic file list' },
],
}),
},
);
assert.equal(response.status, 200);
assert.equal(forwarded.length, 1);
assert.deepEqual(forwarded[0].thinking, { type: 'disabled' });
assert.equal(forwarded[0].messages[2].role, 'tool');
});
test('Gate, artifact, candidate routing and rollback share one compatibility contract', async () => {
const [
builder,
localStack,
candidateRunner,
compatRunner,
canaryRelease,
canaryRollback,
] = await Promise.all([
fs.readFile(path.join(ROOT, 'scripts', 'build-portal-runtime.mjs'), 'utf8'),
fs.readFile(path.join(ROOT, 'release-gate', 'local-stack.mjs'), 'utf8'),
fs.readFile(path.join(ROOT, 'scripts', 'run-memind-portal-candidate.sh'), 'utf8'),
fs.readFile(
path.join(ROOT, 'scripts', 'run-deepseek-compat-proxy-candidate.sh'),
'utf8',
),
fs.readFile(path.join(ROOT, 'scripts', 'release-portal-canary-prod.sh'), 'utf8'),
fs.readFile(path.join(ROOT, 'scripts', 'rollback-portal-canary-prod.sh'), 'utf8'),
]);
assert.match(builder, /--outfile=\.runtime\/portal\/deepseek-no-think-proxy\.mjs/);
assert.match(
localStack,
/path\.join\(resolvedPortalRoot, 'deepseek-no-think-proxy\.mjs'\)/,
);
assert.match(candidateRunner, /export MEMIND_DEEPSEEK_DISABLE_THINKING=1/);
assert.match(candidateRunner, /export MEMIND_GOOSED_HOST_GATEWAY=host\.docker\.internal/);
assert.match(compatRunner, /source "\$\{STABLE_ROOT\}\/\.env"/);
assert.match(compatRunner, /export MEMIND_DEEPSEEK_PROXY_ENTRYPOINT=1/);
assert.match(canaryRelease, /run-deepseek-compat-proxy-candidate\.sh/);
assert.match(canaryRelease, /MEMIND_CANARY_CANDIDATE_HEALTH_URLS/);
assert.match(canaryRelease, /host\.docker\.internal:\$\{DEEPSEEK_COMPAT_PORT\}\/health/);
assert.match(canaryRelease, /bootout.*DEEPSEEK_COMPAT_LABEL/);
assert.match(canaryRollback, /bootout.*DEEPSEEK_COMPAT_LABEL/);
});
+1 -1
View File
@@ -380,7 +380,7 @@ export async function createLocalGateStack({
deepseekProxyLogFd = fs.openSync(deepseekProxyLogPath, 'a');
deepseekProxyChild = spawn(
process.execPath,
[path.join(root, 'deepseek-no-think-proxy.mjs')],
[path.join(resolvedPortalRoot, 'deepseek-no-think-proxy.mjs')],
{
cwd: root,
env: {
+104 -2
View File
@@ -63,6 +63,16 @@ test('runtime builder materializes and declares required Linux ARM64 native pack
assert.match(source, /缺少原生 \.node 文件/);
});
test('runtime builder bundles the DeepSeek compatibility dependency into the artifact', async () => {
const source = await fs.readFile(
path.join(ROOT, 'scripts', 'build-portal-runtime.mjs'),
'utf8',
);
assert.match(source, /bundleDeepseekCompatProxy/);
assert.match(source, /--outfile=\.runtime\/portal\/deepseek-no-think-proxy\.mjs/);
assert.match(source, /await bundleDeepseekCompatProxy\(\)/);
});
test('packaged runtime gate isolates persistent roots and rejects artifact mutation', async () => {
const localStackSource = await fs.readFile(
path.join(ROOT, 'release-gate', 'local-stack.mjs'),
@@ -70,7 +80,10 @@ test('packaged runtime gate isolates persistent roots and rejects artifact mutat
);
assert.match(localStackSource, /MEMIND_PORTAL_H5_ROOT: sandboxRoot/);
assert.match(localStackSource, /MEMIND_DEEPSEEK_DISABLE_THINKING: '1'/);
assert.match(localStackSource, /deepseek-no-think-proxy\.mjs/);
assert.match(
localStackSource,
/path\.join\(resolvedPortalRoot, 'deepseek-no-think-proxy\.mjs'\)/,
);
assert.match(localStackSource, /MEMIND_ORCHESTRATOR_MODE: 'shadow'/);
assert.match(localStackSource, /MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED: '1'/);
assert.match(localStackSource, /H5_USERS_ROOT: usersRoot/);
@@ -119,6 +132,9 @@ test('production canary keeps stable 8081 live and switches only after verified
const edgeBackup = source.indexOf('Create and verify the active 105 nginx routing backup');
const goosedStart = source.indexOf('Start an isolated goosed candidate on 18015');
const candidateStart = source.indexOf('Start the passive candidate Portal on 18081');
const deepseekCompatStart = source.indexOf(
'Start the DeepSeek tool-round compatibility proxy on 18036',
);
const proxyStart = source.indexOf('Start the fail-closed identity router on 18082');
const tunnelStart = source.indexOf('Start the isolated 105 reverse tunnel on 19082');
const edgeSwitch = source.indexOf(
@@ -129,7 +145,8 @@ test('production canary keeps stable 8081 live and switches only after verified
assert.ok(persistBackup > fullBackup);
assert.ok(edgeBackup > persistBackup);
assert.ok(goosedStart > edgeBackup);
assert.ok(candidateStart > goosedStart);
assert.ok(deepseekCompatStart > goosedStart);
assert.ok(candidateStart > deepseekCompatStart);
assert.ok(proxyStart > candidateStart);
assert.ok(tunnelStart > proxyStart);
assert.ok(edgeSwitch > tunnelStart);
@@ -140,6 +157,12 @@ test('production canary keeps stable 8081 live and switches only after verified
assert.match(source, /nginx -t/);
assert.match(source, /CANARY_PROXY_PORT=18082/);
assert.match(source, /CANARY_TUNNEL_REMOTE_PORT=19082/);
assert.match(source, /DEEPSEEK_COMPAT_PORT=18036/);
assert.match(source, /deepseek-no-think-proxy\.mjs/);
assert.match(source, /run-deepseek-compat-proxy-candidate\.sh/);
assert.match(source, /MEMIND_CANARY_CANDIDATE_HEALTH_URLS/);
assert.match(source, /deepseek_compat_healthy/);
assert.match(source, /bootout.*DEEPSEEK_COMPAT_LABEL/);
assert.match(
source,
/edge_ssh\(\) \{\s*#.*\n\s*#.*\n\s*ssh -n -o BatchMode=yes/,
@@ -176,6 +199,11 @@ test('candidate runner overrides stable host MCP paths with container-visible pa
[
'GOOSED_MCP_NODE_PATH=/opt/homebrew/opt/node@24/bin/node',
'GOOSED_MCP_SERVER_PATH=/Users/john/Project/Memind/mindspace-sandbox-mcp.mjs',
'MEMIND_DEEPSEEK_DISABLE_THINKING=0',
'MEMIND_DEEPSEEK_NO_THINK_PORT=9999',
'MEMIND_DEEPSEEK_NO_THINK_BASE_URL=http://wrong.invalid/v1',
'MEMIND_DEEPSEEK_NO_THINK_HOST=wrong.invalid',
'MEMIND_GOOSED_HOST_GATEWAY=wrong.invalid',
'',
].join('\n'),
);
@@ -186,6 +214,11 @@ test('candidate runner overrides stable host MCP paths with container-visible pa
'printf "cwd=%s\\n" "$PWD"',
'printf "mcp_node=%s\\n" "$GOOSED_MCP_NODE_PATH"',
'printf "mcp_server=%s\\n" "$GOOSED_MCP_SERVER_PATH"',
'printf "deepseek_disable=%s\\n" "$MEMIND_DEEPSEEK_DISABLE_THINKING"',
'printf "deepseek_port=%s\\n" "$MEMIND_DEEPSEEK_NO_THINK_PORT"',
'printf "deepseek_gateway=%s\\n" "$MEMIND_GOOSED_HOST_GATEWAY"',
'printf "deepseek_base=%s\\n" "${MEMIND_DEEPSEEK_NO_THINK_BASE_URL-unset}"',
'printf "deepseek_host=%s\\n" "${MEMIND_DEEPSEEK_NO_THINK_HOST-unset}"',
'printf "entrypoint=%s\\n" "$1"',
'',
].join('\n'),
@@ -215,9 +248,78 @@ test('candidate runner overrides stable host MCP paths with container-visible pa
result.stdout,
/^mcp_server=\/opt\/portal\/mindspace-sandbox-mcp\.mjs$/m,
);
assert.match(result.stdout, /^deepseek_disable=1$/m);
assert.match(result.stdout, /^deepseek_port=18036$/m);
assert.match(result.stdout, /^deepseek_gateway=host\.docker\.internal$/m);
assert.match(result.stdout, /^deepseek_base=unset$/m);
assert.match(result.stdout, /^deepseek_host=unset$/m);
assert.match(result.stdout, new RegExp(`^entrypoint=${ROOT}/server\\.mjs$`, 'm'));
});
test('canary rollback stops the DeepSeek compatibility process with the other canary services', async () => {
const source = await fs.readFile(CANARY_ROLLBACK, 'utf8');
assert.match(
source,
/DEEPSEEK_COMPAT_LABEL="cn\.tkmind\.memind-deepseek-compat-candidate"/,
);
assert.match(source, /bootout.*DEEPSEEK_COMPAT_LABEL/);
assert.match(source, /docker rm -f goosed-prod-canary/);
});
test('DeepSeek compatibility runner preserves stable upstream config but enforces candidate controls', async (t) => {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-deepseek-runner-'));
t.after(() => fs.rm(tempRoot, { recursive: true, force: true }));
const stableRoot = path.join(tempRoot, 'stable');
const fakeNode = path.join(tempRoot, 'node');
await fs.mkdir(stableRoot);
await fs.writeFile(
path.join(stableRoot, '.env'),
[
'DEEPSEEK_API_BASE_URL=https://synthetic-upstream.invalid/v1',
'MEMIND_DEEPSEEK_PROXY_ENTRYPOINT=0',
'MEMIND_DEEPSEEK_NO_THINK_PORT=9999',
'',
].join('\n'),
);
await fs.writeFile(
fakeNode,
[
'#!/usr/bin/env bash',
'printf "proxy_entrypoint=%s\\n" "$MEMIND_DEEPSEEK_PROXY_ENTRYPOINT"',
'printf "proxy_port=%s\\n" "$MEMIND_DEEPSEEK_NO_THINK_PORT"',
'printf "upstream=%s\\n" "$DEEPSEEK_API_BASE_URL"',
'printf "entrypoint=%s\\n" "$1"',
'',
].join('\n'),
{ mode: 0o755 },
);
const result = spawnSync(
'bash',
[path.join(ROOT, 'scripts', 'run-deepseek-compat-proxy-candidate.sh')],
{
cwd: '/',
encoding: 'utf8',
env: {
...process.env,
MEMIND_CANARY_STABLE_ROOT: stableRoot,
MEMIND_CANARY_DEEPSEEK_PROXY_PORT: '18036',
NODE_BIN: fakeNode,
},
},
);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /^proxy_entrypoint=1$/m);
assert.match(result.stdout, /^proxy_port=18036$/m);
assert.match(result.stdout, /^upstream=https:\/\/synthetic-upstream\.invalid\/v1$/m);
assert.match(
result.stdout,
new RegExp(`^entrypoint=${ROOT}/deepseek-no-think-proxy\\.mjs$`, 'm'),
);
});
test('canary release and rollback remote shells remain syntactically valid', async () => {
const releaseSource = await fs.readFile(CANARY_RELEASE, 'utf8');
const rollbackSource = await fs.readFile(CANARY_ROLLBACK, 'utf8');
@@ -0,0 +1,16 @@
{
"id": "PRC-REL-002",
"scenario_ids": ["REL-06", "REL-09", "REL-11"],
"failure_signature": "gate-production-drift-deepseek-thinking-tool-round-reasoning-content-replay-rejected",
"synthetic_intent": "验证工具调用后的合成 DeepSeek 会话经过生产候选兼容链路,禁用思考模式,并在兼容依赖失效时让灰度流量回落稳定版本。",
"source_batch_id": "incident-20260726-deepseek-tool-round",
"evidence_digest": "97171fe1d94e0297cb0b932cf1ff18e8d1760519300f7308b9f8cc4e9517e022",
"contains_personal_data": false,
"privacy_review": {
"reviewed_by": "release-gate-maintainers",
"reviewed_at": "2026-07-26T23:55:00+08:00"
},
"automation": {
"command": ["node", "--test", "release-gate/deepseek-production-parity.test.mjs"]
}
}
@@ -1,6 +1,6 @@
{
"fixture_ids": ["PRC-REL-001"],
"fixture_ids": ["PRC-REL-001", "PRC-REL-002"],
"reviewed_by": "release-gate-maintainers",
"reviewed_at": "2026-07-26T12:00:00+08:00",
"notes": "仅收录已脱敏的 runtime 启动回归签名;不包含聊天原文、用户身份或生产地址。"
"notes": "仅收录已脱敏的 runtime 启动和生产配置同构回归签名;不包含聊天原文、用户身份或生产地址。"
}
+27
View File
@@ -156,6 +156,26 @@ async function bundleCanaryProxy() {
await run(esbuildBin, args);
}
async function bundleDeepseekCompatProxy() {
if (!(await exists(esbuildBin))) {
throw new Error(`未找到 esbuild: ${esbuildBin}`);
}
console.log('==> 打包 DeepSeek 工具轮次兼容代理');
const args = [
'deepseek-no-think-proxy.mjs',
'--bundle',
'--platform=node',
'--format=esm',
`--target=${runtimeNodeTarget}`,
'--outfile=.runtime/portal/deepseek-no-think-proxy.mjs',
'--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
];
for (const pkg of externalPackages) {
args.push(`--external:${pkg}`);
}
await run(esbuildBin, args);
}
async function bundleSandboxMcp() {
if (!(await exists(esbuildBin))) {
throw new Error(`未找到 esbuild: ${esbuildBin}`);
@@ -409,6 +429,7 @@ async function writeMetadata() {
for (const scriptName of [
'run-memind-portal-candidate.sh',
'run-memind-canary-proxy-prod.sh',
'run-deepseek-compat-proxy-candidate.sh',
]) {
const source = path.join(root, 'scripts', scriptName);
if (!(await exists(source))) {
@@ -527,6 +548,7 @@ async function writeMetadata() {
' tkmind-excel-mcp.mjs (esbuild bundle; required when Excel Analyst is enabled)',
' wechat-mp.bundle.mjs (esbuild bundle; hot-swappable WeChat MP module)',
' memind-canary-proxy.mjs (immutable-identity stable/candidate router)',
' deepseek-no-think-proxy.mjs (DeepSeek V4 tool-round compatibility proxy)',
'',
'Post-deploy validation (scripts/check-mindspace-public-links.mjs):',
' Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.',
@@ -598,6 +620,7 @@ async function main() {
await copyRuntimeAssets();
await bundleServer();
await bundleCanaryProxy();
await bundleDeepseekCompatProxy();
await bundleWechatMp();
await bundleSandboxMcp();
await bundleMindSearchMcp();
@@ -611,6 +634,10 @@ async function main() {
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755);
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-candidate.sh'), 0o755);
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-canary-proxy-prod.sh'), 0o755);
await fs.chmod(
path.join(runtimeRoot, 'scripts', 'run-deepseek-compat-proxy-candidate.sh'),
0o755,
);
await fs.chmod(path.join(runtimeRoot, 'scripts', 'wechat-mp-menu.mjs'), 0o755);
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-stream-runtime.mjs'), 0o755);
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'), 0o755);
+65 -2
View File
@@ -13,9 +13,11 @@ RUNTIME_ROOT="${ROOT}/.runtime/portal"
PORTAL_CANDIDATE_LABEL="cn.tkmind.memind-portal-candidate"
CANARY_PROXY_LABEL="cn.tkmind.memind-canary-proxy"
CANARY_TUNNEL_LABEL="cn.tkmind.memind-canary-tunnel"
DEEPSEEK_COMPAT_LABEL="cn.tkmind.memind-deepseek-compat-candidate"
EDGE_HOST="${MEMIND_CANARY_EDGE_HOST:-ssh105-public}"
CANARY_PROXY_PORT=18082
CANARY_TUNNEL_REMOTE_PORT=19082
DEEPSEEK_COMPAT_PORT=18036
EDGE_MOBILE_CONFIG="/etc/nginx/conf.d/m.tkmind.cn.conf"
EDGE_WECHAT_CONFIG="/etc/nginx/conf.d/wechat.m.tkmind.cn.conf"
CANARY_USERNAMES="john"
@@ -46,6 +48,7 @@ Deploys a candidate Portal beside the stable 103 runtime:
canary router 127.0.0.1:18082
candidate Portal 127.0.0.1:18081
candidate goosed 127.0.0.1:18015
DeepSeek compat 0.0.0.0:18036 (host/container only)
The stable runtime is not replaced. A dedicated reverse tunnel exposes the
router only to 105 at 127.0.0.1:19082. The committed release workflow updates
@@ -123,6 +126,7 @@ say "Run release source guards"
required_runtime_paths=(
server.mjs
memind-canary-proxy.mjs
deepseek-no-think-proxy.mjs
wechat-mp.bundle.mjs
mindspace-sandbox-mcp.mjs
tkmind-search-mcp.mjs
@@ -131,6 +135,7 @@ required_runtime_paths=(
package.json
scripts/run-memind-portal-candidate.sh
scripts/run-memind-canary-proxy-prod.sh
scripts/run-deepseek-compat-proxy-candidate.sh
scripts/goosed-canary.compose.yml
scripts/memind-portal-tunnel.sh
)
@@ -155,7 +160,7 @@ printf '%s %s\n' "${bundle_sha}" "$(basename "${BUNDLE_PATH}")" > "${SHA_PATH}"
echo "git_branch=${branch}"
echo "artifact_tree=.runtime/portal"
echo "artifact_bundle_sha256=${bundle_sha}"
echo "routing=stable:8081,proxy:18082,edge-tunnel:19082,candidate:18081,goosed-canary:18015"
echo "routing=stable:8081,proxy:18082,edge-tunnel:19082,candidate:18081,goosed-canary:18015,deepseek-compat:18036"
echo "canary_usernames=${CANARY_USERNAMES}"
echo "canary_wechat_user_ids=${CANARY_WECHAT_USER_IDS}"
echo "manual_environment_changes=none"
@@ -174,6 +179,7 @@ ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" \
&& test -f '${GOOSED_DIR}/docker-compose.prod.yml' \
&& curl -fsS http://127.0.0.1:8081/api/status >/dev/null \
&& ! lsof -nP -iTCP:${CANARY_PROXY_PORT} -sTCP:LISTEN >/dev/null 2>&1 \
&& ! lsof -nP -iTCP:${DEEPSEEK_COMPAT_PORT} -sTCP:LISTEN >/dev/null 2>&1 \
&& ! lsof -nP -iTCP:18081 -sTCP:LISTEN >/dev/null 2>&1 \
&& ! lsof -nP -iTCP:18015 -sTCP:LISTEN >/dev/null 2>&1 \
&& /opt/homebrew/bin/docker inspect goosed-prod-1 >/dev/null \
@@ -214,9 +220,11 @@ ssh -o BatchMode=yes "${HOST}" \
PORTAL_CANDIDATE_LABEL='${PORTAL_CANDIDATE_LABEL}' \
CANARY_PROXY_LABEL='${CANARY_PROXY_LABEL}' \
CANARY_TUNNEL_LABEL='${CANARY_TUNNEL_LABEL}' \
DEEPSEEK_COMPAT_LABEL='${DEEPSEEK_COMPAT_LABEL}' \
EDGE_HOST='${EDGE_HOST}' \
CANARY_PROXY_PORT='${CANARY_PROXY_PORT}' \
CANARY_TUNNEL_REMOTE_PORT='${CANARY_TUNNEL_REMOTE_PORT}' \
DEEPSEEK_COMPAT_PORT='${DEEPSEEK_COMPAT_PORT}' \
EDGE_MOBILE_CONFIG='${EDGE_MOBILE_CONFIG}' \
EDGE_WECHAT_CONFIG='${EDGE_WECHAT_CONFIG}' \
CANARY_USERNAMES='${CANARY_USERNAMES}' \
@@ -234,6 +242,7 @@ PERSIST_BACKUP_TAR="${BACKUP_DIR}/memind-persisted-${RELEASE_ID}-before-canary.t
CANDIDATE_PLIST="${HOME}/Library/LaunchAgents/${PORTAL_CANDIDATE_LABEL}.plist"
PROXY_PLIST="${HOME}/Library/LaunchAgents/${CANARY_PROXY_LABEL}.plist"
CANARY_TUNNEL_PLIST="${HOME}/Library/LaunchAgents/${CANARY_TUNNEL_LABEL}.plist"
DEEPSEEK_COMPAT_PLIST="${HOME}/Library/LaunchAgents/${DEEPSEEK_COMPAT_LABEL}.plist"
SECRET_FILE="${HOME}/.config/memind/canary-router.secret"
DOCKER_BIN="/opt/homebrew/bin/docker"
GOOSED_COMPOSE="${GOOSED_DIR}/docker-compose.prod.yml"
@@ -260,6 +269,22 @@ candidate_healthy() {
return 1
}
deepseek_compat_healthy() {
curl -fsS --max-time 5 "http://127.0.0.1:${DEEPSEEK_COMPAT_PORT}/health" \
| /opt/homebrew/opt/node@24/bin/node --input-type=module -e '
let input = "";
process.stdin.on("data", (chunk) => { input += chunk; });
process.stdin.on("end", () => {
const status = JSON.parse(input);
if (
status.ok !== true
|| status.contractVersion !== 1
|| status.deepseekThinking !== "disabled"
) process.exit(1);
});
'
}
edge_ssh() {
# This remote release script itself arrives on stdin. Ordinary nested SSH
# commands must not consume the unparsed remainder of that script.
@@ -326,6 +351,7 @@ stop_candidate_services() {
launchctl bootout "${LAUNCHD_GUI}/${CANARY_TUNNEL_LABEL}" >/dev/null 2>&1 || true
launchctl bootout "${LAUNCHD_GUI}/${CANARY_PROXY_LABEL}" >/dev/null 2>&1 || true
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_CANDIDATE_LABEL}" >/dev/null 2>&1 || true
launchctl bootout "${LAUNCHD_GUI}/${DEEPSEEK_COMPAT_LABEL}" >/dev/null 2>&1 || true
"${DOCKER_BIN}" rm -f goosed-prod-canary >/dev/null 2>&1 || true
}
@@ -405,7 +431,8 @@ tar -xzf "${BUNDLE}" -C "${CANDIDATE_DIR}"
cp "${MANIFEST}" "${CANDIDATE_DIR}/.release-manifest.txt"
chmod 755 \
"${CANDIDATE_DIR}/scripts/run-memind-portal-candidate.sh" \
"${CANDIDATE_DIR}/scripts/run-memind-canary-proxy-prod.sh"
"${CANDIDATE_DIR}/scripts/run-memind-canary-proxy-prod.sh" \
"${CANDIDATE_DIR}/scripts/run-deepseek-compat-proxy-candidate.sh"
say "Stop an older canary without touching stable Portal 8081"
restore_edge_to_stable
@@ -434,6 +461,39 @@ done
"${DOCKER_BIN}" exec goosed-prod-canary \
sh -lc 'test -x /usr/local/bin/node && test -f /opt/portal/mindspace-sandbox-mcp.mjs'
say "Start the DeepSeek tool-round compatibility proxy on 18036"
cat > "${DEEPSEEK_COMPAT_PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>${DEEPSEEK_COMPAT_LABEL}</string>
<key>ProgramArguments</key>
<array><string>${CANDIDATE_DIR}/scripts/run-deepseek-compat-proxy-candidate.sh</string></array>
<key>EnvironmentVariables</key>
<dict>
<key>MEMIND_CANARY_STABLE_ROOT</key><string>${STABLE_DIR}</string>
<key>MEMIND_CANARY_DEEPSEEK_PROXY_PORT</key><string>${DEEPSEEK_COMPAT_PORT}</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>5</integer>
<key>StandardOutPath</key><string>${HOME}/Library/Logs/memind-deepseek-compat-candidate.log</string>
<key>StandardErrorPath</key><string>${HOME}/Library/Logs/memind-deepseek-compat-candidate.log</string>
</dict>
</plist>
EOF
launchctl bootstrap "${LAUNCHD_GUI}" "${DEEPSEEK_COMPAT_PLIST}" >/dev/null
launchctl kickstart -k "${LAUNCHD_GUI}/${DEEPSEEK_COMPAT_LABEL}" >/dev/null
for _ in $(seq 1 30); do
deepseek_compat_healthy && break
sleep 1
done
deepseek_compat_healthy
"${DOCKER_BIN}" exec goosed-prod-canary \
sh -lc "curl -fsS --max-time 5 http://host.docker.internal:${DEEPSEEK_COMPAT_PORT}/health \
| grep -q '\"deepseekThinking\":\"disabled\"'"
say "Start the passive candidate Portal on 18081"
cat > "${CANDIDATE_PLIST}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
@@ -449,6 +509,7 @@ cat > "${CANDIDATE_PLIST}" <<EOF
<key>MEMIND_CANARY_RELEASE_ID</key><string>${RELEASE_ID}</string>
<key>MEMIND_CANARY_CANDIDATE_PORT</key><string>18081</string>
<key>MEMIND_CANARY_GOOSED_URL</key><string>https://127.0.0.1:18015</string>
<key>MEMIND_CANARY_DEEPSEEK_PROXY_PORT</key><string>${DEEPSEEK_COMPAT_PORT}</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
@@ -500,6 +561,7 @@ cat > "${PROXY_PLIST}" <<EOF
<key>MEMIND_CANARY_PROXY_PORT</key><string>${CANARY_PROXY_PORT}</string>
<key>MEMIND_RELEASE_CANARY_USERNAMES</key><string>${CANARY_USERNAMES}</string>
<key>MEMIND_RELEASE_CANARY_WECHAT_USER_IDS</key><string>${CANARY_WECHAT_USER_IDS}</string>
<key>MEMIND_CANARY_CANDIDATE_HEALTH_URLS</key><string>http://127.0.0.1:${DEEPSEEK_COMPAT_PORT}/health</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
@@ -675,6 +737,7 @@ printf 'proxy_health=http://127.0.0.1:%s/__memind_canary/health\n' "${CANARY_PRO
printf 'edge_tunnel=http://127.0.0.1:%s/api/status\n' "${CANARY_TUNNEL_REMOTE_PORT}"
printf 'candidate_health=http://127.0.0.1:18081/api/status\n'
printf 'candidate_goosed=https://127.0.0.1:18015/status\n'
printf 'deepseek_compat_health=http://127.0.0.1:%s/health\n' "${DEEPSEEK_COMPAT_PORT}"
REMOTE_SCRIPT
say "103 canary release completed"
+1 -1
View File
@@ -166,7 +166,7 @@ fi
verify_runtime_artifact() {
local missing=0
for required in server.mjs memind-canary-proxy.mjs wechat-mp.bundle.mjs mindspace-sandbox-mcp.mjs tkmind-search-mcp.mjs tkmind-excel-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/run-memind-portal-candidate.sh scripts/run-memind-canary-proxy-prod.sh scripts/goosed-canary.compose.yml scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
for required in server.mjs memind-canary-proxy.mjs deepseek-no-think-proxy.mjs wechat-mp.bundle.mjs mindspace-sandbox-mcp.mjs tkmind-search-mcp.mjs tkmind-excel-mcp.mjs mindspace-public-links.mjs dist package.json scripts/run-memind-portal-prod.sh scripts/run-memind-portal-candidate.sh scripts/run-memind-canary-proxy-prod.sh scripts/run-deepseek-compat-proxy-candidate.sh scripts/goosed-canary.compose.yml scripts/check-mindspace-public-links.mjs scripts/load-env.mjs scripts/wechat-mp-menu.mjs scripts/memind-portal-tunnel.sh; do
if [[ ! -e "${RUNTIME_ROOT}/${required}" ]]; then
echo "runtime 产物缺失: ${RUNTIME_ROOT}/${required}" >&2
missing=1
+3
View File
@@ -6,6 +6,7 @@ STABLE_DIR="${STUDIO_REMOTE_ROOT:-/Users/john/Project}/Memind"
PORTAL_CANDIDATE_LABEL="cn.tkmind.memind-portal-candidate"
CANARY_PROXY_LABEL="cn.tkmind.memind-canary-proxy"
CANARY_TUNNEL_LABEL="cn.tkmind.memind-canary-tunnel"
DEEPSEEK_COMPAT_LABEL="cn.tkmind.memind-deepseek-compat-candidate"
EDGE_HOST="${MEMIND_CANARY_EDGE_HOST:-ssh105-public}"
CANARY_TUNNEL_REMOTE_PORT=19082
EDGE_MOBILE_CONFIG="/etc/nginx/conf.d/m.tkmind.cn.conf"
@@ -30,6 +31,7 @@ ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" \
PORTAL_CANDIDATE_LABEL='${PORTAL_CANDIDATE_LABEL}' \
CANARY_PROXY_LABEL='${CANARY_PROXY_LABEL}' \
CANARY_TUNNEL_LABEL='${CANARY_TUNNEL_LABEL}' \
DEEPSEEK_COMPAT_LABEL='${DEEPSEEK_COMPAT_LABEL}' \
EDGE_HOST='${EDGE_HOST}' \
CANARY_TUNNEL_REMOTE_PORT='${CANARY_TUNNEL_REMOTE_PORT}' \
EDGE_MOBILE_CONFIG='${EDGE_MOBILE_CONFIG}' \
@@ -69,6 +71,7 @@ EDGE_ROLLBACK
launchctl bootout "${LAUNCHD_GUI}/${CANARY_TUNNEL_LABEL}" >/dev/null 2>&1 || true
launchctl bootout "${LAUNCHD_GUI}/${CANARY_PROXY_LABEL}" >/dev/null 2>&1 || true
launchctl bootout "${LAUNCHD_GUI}/${PORTAL_CANDIDATE_LABEL}" >/dev/null 2>&1 || true
launchctl bootout "${LAUNCHD_GUI}/${DEEPSEEK_COMPAT_LABEL}" >/dev/null 2>&1 || true
/opt/homebrew/bin/docker rm -f goosed-prod-canary >/dev/null 2>&1 || true
rm -f "${STABLE_DIR}/.release-drain"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STABLE_ROOT="${MEMIND_CANARY_STABLE_ROOT:-/Users/john/Project/Memind}"
if [[ ! -d "${STABLE_ROOT}" || ! -f "${STABLE_ROOT}/.env" ]]; then
echo "[deepseek-compat] stable runtime environment is unavailable: ${STABLE_ROOT}" >&2
exit 1
fi
cd "${ROOT}"
set -a
# shellcheck disable=SC1091
source "${STABLE_ROOT}/.env"
set +a
export NODE_ENV=production
export MEMIND_DEEPSEEK_PROXY_ENTRYPOINT=1
export MEMIND_DEEPSEEK_NO_THINK_PORT="${MEMIND_CANARY_DEEPSEEK_PROXY_PORT:-18036}"
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
if [[ ! -x "${NODE_BIN}" ]]; then
NODE_BIN="$(command -v node)"
fi
exec "${NODE_BIN}" "${ROOT}/deepseek-no-think-proxy.mjs"
+7
View File
@@ -33,6 +33,13 @@ export TKMIND_API_TARGET="${MEMIND_CANARY_GOOSED_URL:-https://127.0.0.1:18015}"
# mounted at /opt/portal. Never inherit host-only MCP paths from the stable .env.
export GOOSED_MCP_NODE_PATH=/usr/local/bin/node
export GOOSED_MCP_SERVER_PATH=/opt/portal/mindspace-sandbox-mcp.mjs
# DeepSeek V4 tool rounds must use the same compatibility contract exercised
# by the release Gate. Stable .env values cannot disable or redirect it.
export MEMIND_DEEPSEEK_DISABLE_THINKING=1
export MEMIND_DEEPSEEK_NO_THINK_PORT="${MEMIND_CANARY_DEEPSEEK_PROXY_PORT:-18036}"
export MEMIND_GOOSED_HOST_GATEWAY=host.docker.internal
unset MEMIND_DEEPSEEK_NO_THINK_BASE_URL
unset MEMIND_DEEPSEEK_NO_THINK_HOST
unset TKMIND_API_TARGET_1
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"