release: prepare 0629001 portal updates
This commit is contained in:
+190
-25
@@ -9,6 +9,7 @@ import {
|
||||
guardMissingPublicHtmlLinks,
|
||||
loadWechatMpConfig,
|
||||
maybeAttachPublishedHtmlLink,
|
||||
shouldRetryHtmlGenerationReply,
|
||||
splitWechatText,
|
||||
verifyWechatMpSignature,
|
||||
verifyWechatMpUrlChallenge,
|
||||
@@ -138,7 +139,74 @@ test('guardMissingPublicHtmlLinks blocks missing MindSpace public html links', a
|
||||
assert.match(guarded, /missing\.html/);
|
||||
});
|
||||
|
||||
test('wechat mp service replaces blocked publish claims with a directly accessible fallback page', async () => {
|
||||
test('shouldRetryHtmlGenerationReply retries when reply link is missing on disk', () => {
|
||||
const intent = { agentText: '帮我生成一个 html 页面 public/test.html' };
|
||||
const reply = {
|
||||
text: '[Test](https://m.tkmind.cn/MindSpace/user-1/public/test.html)',
|
||||
messages: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'toolRequest',
|
||||
toolCall: { value: { name: 'load_skill', arguments: { name: 'static-page-publish' } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(
|
||||
shouldRetryHtmlGenerationReply({
|
||||
reply,
|
||||
intent,
|
||||
confirmedArtifacts: [],
|
||||
hasValidLinkInReply: false,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldRetryHtmlGenerationReply does not skip retry because unrelated recent html exists', () => {
|
||||
const intent = { agentText: '帮我做一个小页面吧' };
|
||||
const reply = {
|
||||
text: '[Test](https://m.tkmind.cn/MindSpace/user-1/public/test.html)',
|
||||
messages: [
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: 'toolRequest',
|
||||
toolCall: { value: { name: 'load_skill', arguments: { name: 'static-page-publish' } } },
|
||||
},
|
||||
{
|
||||
type: 'toolRequest',
|
||||
toolCall: {
|
||||
value: {
|
||||
name: 'write_file',
|
||||
arguments: { path: 'public/other-page.html', content: '<html></html>' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(
|
||||
shouldRetryHtmlGenerationReply({
|
||||
reply,
|
||||
intent,
|
||||
confirmedArtifacts: [
|
||||
{
|
||||
localPath: '/tmp/user-1/public/other-page.html',
|
||||
relativePath: 'public/other-page.html',
|
||||
url: 'https://m.tkmind.cn/MindSpace/user-1/public/other-page.html',
|
||||
},
|
||||
],
|
||||
hasValidLinkInReply: false,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('wechat mp service rejects blocked publish claims that did not use the H5 page skill', async () => {
|
||||
const wechatCalls = [];
|
||||
const service = createBoundWechatService({
|
||||
config: {
|
||||
@@ -208,8 +276,8 @@ test('wechat mp service replaces blocked publish claims with a directly accessib
|
||||
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.match(payload.text.content, /summer-breeze-journal\.html/);
|
||||
assert.match(payload.text.content, /没有按 H5 里的页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /summer-breeze-journal\.html/);
|
||||
});
|
||||
|
||||
test('maybeAttachPublishedHtmlLink copies generated root html into public and returns link', async (t) => {
|
||||
@@ -2033,7 +2101,115 @@ test('wechat mp service attaches the newly generated page link even when no html
|
||||
assert.doesNotMatch(payload.text.content, /页面生成未完成|旧指令污染|稍后重试/);
|
||||
});
|
||||
|
||||
test('wechat mp service creates a simple fallback page when html generation reply produced no file', async () => {
|
||||
test('wechat mp service does not trust unrelated recent html artifacts when reply links a missing file', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
const wechatCalls = [];
|
||||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-recent-mismatch-');
|
||||
const unrelatedHtmlPath = path.join(workspaceRoot, 'public', 'summer-breeze-journal.html');
|
||||
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
enabled: true,
|
||||
appId: 'wx123',
|
||||
appSecret: 'secret',
|
||||
token,
|
||||
publicBaseUrl: 'https://m.tkmind.cn',
|
||||
bindPath: '/auth/wechat/authorize?intent=login',
|
||||
ackText: 'ack',
|
||||
unsupportedText: 'unsupported',
|
||||
unboundTextPrefix: '请先绑定',
|
||||
progressDelayMs: 0,
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||||
},
|
||||
async getWechatAgentRoute() {
|
||||
return { agentSessionId: 'session-1' };
|
||||
},
|
||||
async clearWechatAgentRoute() {},
|
||||
async canUseChat() {
|
||||
return { ok: true };
|
||||
},
|
||||
async resolveWorkingDir() {
|
||||
return workspaceRoot;
|
||||
},
|
||||
async getAgentSessionPolicy() {
|
||||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||||
},
|
||||
async getUserPublishLayout() {
|
||||
return { publishDir: workspaceRoot, displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||||
},
|
||||
async billSessionUsage() {},
|
||||
async recordWechatMpMessage() {
|
||||
return { inserted: true };
|
||||
},
|
||||
async finishWechatMpMessage() {},
|
||||
async insertWechatMpMessageDetail() {},
|
||||
},
|
||||
sessionApiFetch: async (sessionId, pathname) => {
|
||||
if (pathname === `/sessions/${sessionId}/events`) {
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","request_id":"req-recent-mismatch","message":{"id":"assistant-skill","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"toolRequest","toolCall":{"value":{"name":"load_skill","arguments":{"name":"static-page-publish"}}}}]}}\n\n',
|
||||
'data: {"type":"Message","request_id":"req-recent-mismatch","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"[Hello](https://m.tkmind.cn/MindSpace/user-1/public/hello.html)"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-recent-mismatch","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
}
|
||||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||||
fs.mkdirSync(path.dirname(unrelatedHtmlPath), { recursive: true });
|
||||
fs.writeFileSync(unrelatedHtmlPath, '<!doctype html><title>Summer</title>');
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
throw new Error(`unexpected session api path: ${sessionId} ${pathname}`);
|
||||
},
|
||||
wechatFetch: async (url, init = {}) => {
|
||||
wechatCalls.push([url, init.method ?? 'GET', init.body ?? null]);
|
||||
if (String(url).includes('/cgi-bin/stable_token')) {
|
||||
return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (String(url).includes('/cgi-bin/message/custom/send')) {
|
||||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
const originalRandomUuid = crypto.randomUUID;
|
||||
crypto.randomUUID = () => 'req-recent-mismatch';
|
||||
try {
|
||||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我生成一个简单的 hello 页面吧' }), {
|
||||
timestamp,
|
||||
nonce,
|
||||
signature: signatureFor(token, timestamp, nonce),
|
||||
});
|
||||
assert.equal(result.status, 200);
|
||||
await result.task;
|
||||
} finally {
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
}
|
||||
|
||||
const sendCall = wechatCalls.find(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||||
const payload = JSON.parse(sendCall[2]);
|
||||
assert.doesNotMatch(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/hello\.html/);
|
||||
assert.doesNotMatch(payload.text.content, /summer-breeze-journal\.html/);
|
||||
assert.match(payload.text.content, /没有按 H5 里的页面技能真正生成成功|页面生成未完成/);
|
||||
});
|
||||
|
||||
test('wechat mp service refuses to fabricate a fallback page when html generation produced no file', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
@@ -2131,17 +2307,14 @@ test('wechat mp service creates a simple fallback page when html generation repl
|
||||
}
|
||||
|
||||
const htmlPath = path.join(workspaceRoot, 'public', 'thailand-guide.html');
|
||||
assert.equal(fs.existsSync(htmlPath), true);
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
assert.match(html, /泰国简易攻略/);
|
||||
assert.match(html, /推荐路线/);
|
||||
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, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/thailand-guide\.html/);
|
||||
assert.doesNotMatch(payload.text.content, /页面生成未完成|稍后重试/);
|
||||
assert.match(payload.text.content, /没有按 H5 里的页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/thailand-guide\.html/);
|
||||
});
|
||||
|
||||
test('wechat mp service can deliver a fallback page without rebuilding the session on publish-success claims', async () => {
|
||||
test('wechat mp service retries poisoned publish claims and still refuses fallback output', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
const nonce = 'nonce';
|
||||
@@ -2149,8 +2322,6 @@ test('wechat mp service can deliver a fallback page without rebuilding the sessi
|
||||
let routeCleared = false;
|
||||
let started = false;
|
||||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-publish-claim-');
|
||||
const htmlPath = path.join(workspaceRoot, 'public', 'summer-breeze-journal.html');
|
||||
|
||||
const service = createWechatMpService({
|
||||
config: {
|
||||
enabled: true,
|
||||
@@ -2242,10 +2413,6 @@ test('wechat mp service can deliver a fallback page without rebuilding the sessi
|
||||
});
|
||||
}
|
||||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||||
if (sessionId === 'session-2') {
|
||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||
fs.writeFileSync(htmlPath, '<!doctype html><title>Summer</title>');
|
||||
}
|
||||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (pathname === `/sessions/${sessionId}/events`) {
|
||||
@@ -2260,10 +2427,8 @@ test('wechat mp service can deliver a fallback page without rebuilding the sessi
|
||||
}
|
||||
return new Response(
|
||||
[
|
||||
'data: {"type":"Message","request_id":"req-claim-retry","message":{"id":"assistant-skill","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"toolRequest","toolCall":{"value":{"name":"load_skill","arguments":{"name":"static-page-publish"}}}}]}}\n\n',
|
||||
`data: {"type":"Message","request_id":"req-claim-retry","message":{"id":"assistant-tool","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"toolRequest","toolCall":{"value":{"name":"developer","arguments":{"action":"write","path":"${htmlPath.replace(/\\/g, '\\\\')}","content":"<!doctype html><title>Summer</title>"}}}}]}}\n\n`,
|
||||
'data: {"type":"Message","request_id":"req-claim-retry","message":{"id":"assistant-2","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已完成"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-claim-retry","token_state":{"inputTokens":2,"outputTokens":2}}\n\n',
|
||||
'data: {"type":"Message","request_id":"req-claim-retry","message":{"id":"assistant-2","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"🌴 夏日主题页面"}]}}\n\n',
|
||||
'data: {"type":"Finish","request_id":"req-claim-retry","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||||
].join(''),
|
||||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||||
);
|
||||
@@ -2308,13 +2473,13 @@ test('wechat mp service can deliver a fallback page without rebuilding the sessi
|
||||
crypto.randomUUID = originalRandomUuid;
|
||||
}
|
||||
|
||||
assert.equal(routeCleared, false);
|
||||
assert.equal(started, false);
|
||||
assert.equal(routeCleared, true);
|
||||
assert.equal(started, true);
|
||||
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, /summer-breeze-journal\.html/);
|
||||
assert.match(payload.text.content, /可直接打开的简版页面|查看页面/);
|
||||
assert.match(payload.text.content, /没有按 H5 里的页面技能真正生成成功/);
|
||||
assert.doesNotMatch(payload.text.content, /summer-breeze-journal\.html/);
|
||||
});
|
||||
|
||||
test('wechat mp service blocks schedule confirmation when no schedule item was written', async () => {
|
||||
|
||||
Reference in New Issue
Block a user