Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d90e6113 | |||
| 3be5a04161 |
@@ -56,6 +56,7 @@ test('Gate, artifact, candidate routing and rollback share one compatibility con
|
||||
const [
|
||||
builder,
|
||||
localStack,
|
||||
stableRunner,
|
||||
candidateRunner,
|
||||
compatRunner,
|
||||
canaryRelease,
|
||||
@@ -63,6 +64,7 @@ test('Gate, artifact, candidate routing and rollback share one compatibility con
|
||||
] = 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-prod.sh'), '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'),
|
||||
@@ -77,6 +79,8 @@ test('Gate, artifact, candidate routing and rollback share one compatibility con
|
||||
localStack,
|
||||
/path\.join\(resolvedPortalRoot, 'deepseek-no-think-proxy\.mjs'\)/,
|
||||
);
|
||||
assert.match(stableRunner, /export MEMIND_DEEPSEEK_DISABLE_THINKING="\$\{MEMIND_DEEPSEEK_DISABLE_THINKING:-1\}"/);
|
||||
assert.match(stableRunner, /export MEMIND_GOOSED_HOST_GATEWAY="\$\{MEMIND_GOOSED_HOST_GATEWAY:-host\.docker\.internal\}"/);
|
||||
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"/);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Standalone recovery: point every stable goosed target at the 18036 no-think proxy.
|
||||
* Works on bundled 103 runtime (no db.mjs / llm-providers.mjs required).
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Agent, fetch as undiciFetch } from 'undici';
|
||||
|
||||
const GOOSED_PROVIDER_ID = 'custom_memind_deepseek_no_think';
|
||||
const DEFAULT_MODEL = 'deepseek-v4-pro';
|
||||
const MODELS = ['deepseek-v4-pro', 'deepseek-v4-flash'];
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
|
||||
function loadEnvFile(filePath) {
|
||||
const env = {};
|
||||
if (!fs.existsSync(filePath)) return env;
|
||||
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"'))
|
||||
|| (value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
env[key] = value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
const fileEnv = loadEnvFile(path.join(root, '.env'));
|
||||
const env = { ...fileEnv, ...process.env };
|
||||
const apiSecret = String(env.TKMIND_SERVER__SECRET_KEY ?? '').trim();
|
||||
const apiKey = String(env.DEEPSEEK_API_KEY ?? '').trim();
|
||||
const apiTargets = String(
|
||||
env.TKMIND_API_TARGETS
|
||||
?? env.TKMIND_API_TARGET
|
||||
?? 'https://127.0.0.1:18006',
|
||||
)
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!apiSecret) {
|
||||
console.error('missing TKMIND_SERVER__SECRET_KEY');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!apiKey) {
|
||||
console.error('missing DEEPSEEK_API_KEY');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const host = String(env.MEMIND_GOOSED_HOST_GATEWAY ?? 'host.docker.internal').trim() || 'host.docker.internal';
|
||||
const port = Number(env.MEMIND_DEEPSEEK_NO_THINK_PORT ?? 18036);
|
||||
const proxyBaseUrl = `http://${host}:${port}/v1`;
|
||||
|
||||
async function goosedFetch(apiTarget, pathname, init = {}) {
|
||||
const url = new URL(pathname, apiTarget);
|
||||
const headers = {
|
||||
...(init.headers ?? {}),
|
||||
'X-Secret-Key': apiSecret,
|
||||
};
|
||||
if (init.body && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
const dispatcher = apiTarget.startsWith('https://') ? insecureDispatcher : undefined;
|
||||
return undiciFetch(url, { ...init, headers, dispatcher });
|
||||
}
|
||||
|
||||
async function upsertConfig(apiTarget, key, value, isSecret = false) {
|
||||
const res = await goosedFetch(apiTarget, '/config/upsert', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key, value, is_secret: isSecret }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`upsert ${key} on ${apiTarget} failed: ${res.status} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertProvider(apiTarget) {
|
||||
const body = {
|
||||
engine: 'openai',
|
||||
display_name: 'memind_deepseek_no_think',
|
||||
api_url: proxyBaseUrl,
|
||||
api_key: apiKey,
|
||||
models: MODELS,
|
||||
supports_streaming: true,
|
||||
requires_auth: true,
|
||||
preserves_thinking: false,
|
||||
};
|
||||
|
||||
let res = await goosedFetch(
|
||||
apiTarget,
|
||||
`/config/custom-providers/${encodeURIComponent(GOOSED_PROVIDER_ID)}`,
|
||||
{ method: 'PUT', body: JSON.stringify(body) },
|
||||
);
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
const missing = res.status === 404 || /provider not found/i.test(text);
|
||||
if (!missing) {
|
||||
throw new Error(`upsert provider on ${apiTarget} failed: ${res.status} ${text}`);
|
||||
}
|
||||
res = await goosedFetch(apiTarget, '/config/custom-providers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`create provider on ${apiTarget} failed: ${res.status} ${text}`);
|
||||
}
|
||||
|
||||
await upsertConfig(apiTarget, 'DEEPSEEK_API_KEY', apiKey, true);
|
||||
for (const [key, value] of [
|
||||
['GOOSE_PROVIDER', GOOSED_PROVIDER_ID],
|
||||
['GOOSE_MODEL', DEFAULT_MODEL],
|
||||
['TKMIND_PROVIDER', GOOSED_PROVIDER_ID],
|
||||
['TKMIND_MODEL', DEFAULT_MODEL],
|
||||
['GOOSE_THINKING_EFFORT', 'off'],
|
||||
]) {
|
||||
await upsertConfig(apiTarget, key, value, false);
|
||||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const target of apiTargets) {
|
||||
await upsertProvider(target);
|
||||
results.push({ target, provider: GOOSED_PROVIDER_ID, model: DEFAULT_MODEL, proxyBaseUrl });
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ ok: true, results }, null, 2));
|
||||
@@ -26,6 +26,12 @@ fi
|
||||
export GOOSED_MCP_NODE_PATH="${GOOSED_MCP_NODE_PATH:-${NODE_BIN}}"
|
||||
export GOOSED_MCP_SERVER_PATH="${GOOSED_MCP_SERVER_PATH:-${ROOT}/mindspace-sandbox-mcp.mjs}"
|
||||
|
||||
# DeepSeek V4 tool rounds require reasoning_content replay; stable goosed must use the
|
||||
# host no-think compat proxy (see docs/发包必看.md §4.7).
|
||||
export MEMIND_DEEPSEEK_DISABLE_THINKING="${MEMIND_DEEPSEEK_DISABLE_THINKING:-1}"
|
||||
export MEMIND_DEEPSEEK_NO_THINK_PORT="${MEMIND_DEEPSEEK_NO_THINK_PORT:-18036}"
|
||||
export MEMIND_GOOSED_HOST_GATEWAY="${MEMIND_GOOSED_HOST_GATEWAY:-host.docker.internal}"
|
||||
|
||||
free_port_if_stale_memind_listener() {
|
||||
local port="${H5_PORT:-8081}"
|
||||
local pids
|
||||
|
||||
+6
-5
@@ -3288,8 +3288,8 @@ test('wechat mp service forwards H5 agent text when page generation produced no
|
||||
assert.equal(fs.existsSync(htmlPath), false);
|
||||
const sendCall = wechatCalls.find(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||||
const payload = JSON.parse(sendCall[2]);
|
||||
assert.match(payload.text.content, /🌴 泰国简易攻略/);
|
||||
assert.doesNotMatch(payload.text.content, /没有按 H5 里的页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /🌴 泰国简易攻略/);
|
||||
assert.match(payload.text.content, /没有按服务号页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/thailand-guide\.html/);
|
||||
});
|
||||
|
||||
@@ -3631,7 +3631,8 @@ test('wechat mp service recreates dedicated session when tool_calls error arrive
|
||||
assert.equal(started, true);
|
||||
const sendCall = wechatCalls.find(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||||
const payload = JSON.parse(sendCall[2]);
|
||||
assert.match(payload.text.content, /已恢复,可以继续对话/);
|
||||
assert.doesNotMatch(payload.text.content, /已恢复,可以继续对话/);
|
||||
assert.match(payload.text.content, /没有按服务号页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /Bad request/);
|
||||
assert.doesNotMatch(payload.text.content, /tool_calls/);
|
||||
});
|
||||
@@ -3965,8 +3966,8 @@ test('wechat mp service retries poisoned publish claims before forwarding H5 ret
|
||||
const sendCall = wechatCalls.find(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||||
const payload = JSON.parse(sendCall[2]);
|
||||
assert.doesNotMatch(payload.text.content, /页面都已经成功发布了|主题页面已发布/);
|
||||
assert.match(payload.text.content, /🌴 夏日主题页面/);
|
||||
assert.doesNotMatch(payload.text.content, /没有按 H5 里的页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /🌴 夏日主题页面/);
|
||||
assert.match(payload.text.content, /没有按服务号页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /summer-breeze-journal\.html/);
|
||||
});
|
||||
|
||||
|
||||
@@ -69,5 +69,11 @@ export function resolvePageGenerateOutcome({
|
||||
};
|
||||
}
|
||||
|
||||
return { action: 'send', artifacts: sendable };
|
||||
// Fail closed: page.generate must deliver a verified public HTML artifact.
|
||||
// Text-only planning replies ("我先搜索…") must not mark WeChat delivery done.
|
||||
return {
|
||||
action: 'fail',
|
||||
failureText: buildPagePublishFailureText(),
|
||||
reason: 'missing_page_artifact',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -218,3 +218,29 @@ test('resolvePageGenerateOutcome sends when share preview meta is present', () =
|
||||
assert.equal(outcome.action, 'send');
|
||||
assert.equal(outcome.artifacts.length, 1);
|
||||
});
|
||||
|
||||
test('resolvePageGenerateOutcome fails closed on planning text without html artifact', () => {
|
||||
const outcome = resolvePageGenerateOutcome({
|
||||
reply: {
|
||||
text: '找到了之前的新闻页面。让我先读取最新的页面格式作为参考,同时并行搜索今日新闻和天气。',
|
||||
},
|
||||
confirmedArtifacts: [],
|
||||
verifiedArtifacts: [],
|
||||
});
|
||||
assert.equal(outcome.action, 'fail');
|
||||
assert.equal(outcome.reason, 'missing_page_artifact');
|
||||
assert.match(outcome.failureText, /没有按服务号页面技能真正生成成功|static-page-publish/);
|
||||
});
|
||||
|
||||
test('resolvePageGenerateOutcome fails closed when reply links an old page but no artifact confirmed', () => {
|
||||
const outcome = resolvePageGenerateOutcome({
|
||||
reply: {
|
||||
text: '[每日新闻](https://m.tkmind.cn/MindSpace/u/public/daily-news-0728.html)',
|
||||
},
|
||||
confirmedArtifacts: [],
|
||||
verifiedArtifacts: [],
|
||||
replyHasPublicLinks: true,
|
||||
});
|
||||
assert.equal(outcome.action, 'fail');
|
||||
assert.equal(outcome.reason, 'missing_page_artifact');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user