4382 lines
164 KiB
JavaScript
4382 lines
164 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import crypto from 'node:crypto';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import test from 'node:test';
|
||
import {
|
||
buildWechatAgentPrompt,
|
||
buildWechatTextReply,
|
||
createWechatMpService,
|
||
guardMissingPublicHtmlLinks,
|
||
assertWechatAgentReplyIsSendable,
|
||
findRecoverableWechatAgentErrorInReply,
|
||
isRecoverableWechatAgentSessionError,
|
||
isWechatAgentApiErrorText,
|
||
sanitizeWechatAgentOutboundText,
|
||
loadWechatMpConfig,
|
||
maybeAttachPublishedHtmlLink,
|
||
shouldRetryHtmlGenerationReply,
|
||
shouldForceNewWechatAgentSession,
|
||
splitWechatText,
|
||
verifyWechatMpSignature,
|
||
verifyWechatMpUrlChallenge,
|
||
decryptWechatMpPayload,
|
||
WECHAT_CUSTOMER_TEXT_MAX_BYTES,
|
||
} from './wechat-mp.mjs';
|
||
|
||
function signatureFor(token, timestamp, nonce) {
|
||
return crypto
|
||
.createHash('sha1')
|
||
.update([token, timestamp, nonce].sort().join(''))
|
||
.digest('hex');
|
||
}
|
||
|
||
function inboundXml({
|
||
toUserName = 'gh_test',
|
||
fromUserName = 'openid-1',
|
||
msgType = 'text',
|
||
content = 'hello',
|
||
event = '',
|
||
extraFields = {},
|
||
} = {}) {
|
||
const renderedExtras = Object.entries(extraFields)
|
||
.map(([key, value]) => {
|
||
if (value === '' || value === null || value === undefined) return '';
|
||
return typeof value === 'number'
|
||
? `<${key}>${value}</${key}>`
|
||
: `<${key}><![CDATA[${value}]]></${key}>`;
|
||
})
|
||
.filter(Boolean)
|
||
.join('');
|
||
return [
|
||
'<xml>',
|
||
`<ToUserName><![CDATA[${toUserName}]]></ToUserName>`,
|
||
`<FromUserName><![CDATA[${fromUserName}]]></FromUserName>`,
|
||
'<CreateTime>1710000000</CreateTime>',
|
||
`<MsgType><![CDATA[${msgType}]]></MsgType>`,
|
||
content ? `<Content><![CDATA[${content}]]></Content>` : '',
|
||
event ? `<Event><![CDATA[${event}]]></Event>` : '',
|
||
renderedExtras,
|
||
'<MsgId>10001</MsgId>',
|
||
'</xml>',
|
||
].join('');
|
||
}
|
||
|
||
function previewReadyPageHtml({ title = 'Page', subtitle = '测试页面' } = {}) {
|
||
return `<!doctype html><html><head><meta name="description" content="${subtitle}"><meta name="mindspace-cover" content='{"tag":"页面","accent":"#3366cc","accent2":"#112233","subtitle":"${subtitle}"}'><title>${title}</title></head><body><main>${'x'.repeat(600)}</main><p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p></body></html>`;
|
||
}
|
||
|
||
function jsonEscapedPreviewReadyPageHtml(options = {}) {
|
||
return previewReadyPageHtml(options).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||
}
|
||
|
||
function createBoundWechatService({
|
||
token = 'token',
|
||
wechatFetch,
|
||
sessionApiFetch,
|
||
startAgentSession = null,
|
||
userAuth = {},
|
||
config = {},
|
||
scheduleService = null,
|
||
applySessionLlmProvider = null,
|
||
}) {
|
||
return createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
mediaPublicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
progressDelayMs: 0,
|
||
maxImageBytes: 1024 * 1024,
|
||
...config,
|
||
},
|
||
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 '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
async insertWechatMpMessageDetail() {},
|
||
...userAuth,
|
||
},
|
||
startAgentSession,
|
||
sessionApiFetch,
|
||
scheduleService,
|
||
applySessionLlmProvider,
|
||
wechatFetch,
|
||
apiFetch: async () => new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }),
|
||
});
|
||
}
|
||
|
||
test('splitWechatText respects WeChat 2048-byte customer service limit', () => {
|
||
const longChinese = '中'.repeat(900);
|
||
const chunks = splitWechatText(longChinese);
|
||
assert.ok(chunks.length > 1);
|
||
for (const chunk of chunks) {
|
||
assert.ok(Buffer.byteLength(chunk, 'utf8') <= WECHAT_CUSTOMER_TEXT_MAX_BYTES);
|
||
}
|
||
assert.equal(chunks.join(''), longChinese);
|
||
});
|
||
|
||
test('Page Data requests always rotate away from an existing WeChat route', () => {
|
||
assert.equal(
|
||
shouldForceNewWechatAgentSession({ kind: 'chat.general' }, '做一个问卷,后台可以查看提交数据'),
|
||
true,
|
||
);
|
||
assert.equal(shouldForceNewWechatAgentSession({ kind: 'chat.general' }, '继续聊德川家康'), false);
|
||
assert.equal(shouldForceNewWechatAgentSession({ kind: 'session.reset' }, '继续聊德川家康'), true);
|
||
});
|
||
|
||
test('buildWechatAgentPrompt requires docx generation before html when Word download is requested', () => {
|
||
const prompt = buildWechatAgentPrompt({
|
||
msgType: 'text',
|
||
content: '帮我生成一个行业分布报告页面,提供链接word给我来下载',
|
||
});
|
||
assert.match(prompt, /docx-generate/);
|
||
assert.match(prompt, /public\/\*\.docx/);
|
||
assert.match(prompt, /static-page-publish/);
|
||
});
|
||
|
||
test('buildWechatAgentPrompt keeps normal html guidance without docx requirement for plain page requests', () => {
|
||
const prompt = buildWechatAgentPrompt({
|
||
msgType: 'text',
|
||
content: '帮我生成一个简单页面',
|
||
});
|
||
assert.match(prompt, /static-page-publish/);
|
||
assert.doesNotMatch(prompt, /docx-generate/);
|
||
});
|
||
|
||
test('guardMissingPublicHtmlLinks blocks missing MindSpace public html links', async () => {
|
||
const text =
|
||
'页面已创建:\nhttps://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/missing.html';
|
||
const guarded = await guardMissingPublicHtmlLinks(text, {
|
||
linkExists: async () => false,
|
||
});
|
||
assert.doesNotMatch(guarded, /https:\/\/g2\.tkmind\.cn\/MindSpace/);
|
||
assert.match(guarded, /页面生成未完成/);
|
||
assert.match(guarded, /missing\.html/);
|
||
});
|
||
|
||
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('shouldRetryHtmlGenerationReply allows normal H5 agent text without fake page links', () => {
|
||
const intent = { agentText: '帮我生成页面吧' };
|
||
const reply = {
|
||
text: '请告诉我要生成什么主题和内容,我再继续。',
|
||
messages: [
|
||
{
|
||
content: [
|
||
{
|
||
type: 'text',
|
||
text: '请告诉我要生成什么主题和内容,我再继续。',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
assert.equal(
|
||
shouldRetryHtmlGenerationReply({
|
||
reply,
|
||
intent,
|
||
confirmedArtifacts: [],
|
||
hasValidLinkInReply: false,
|
||
}),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test('wechat mp service rejects blocked publish claims that did not use the H5 page skill', async () => {
|
||
const wechatCalls = [];
|
||
const service = createBoundWechatService({
|
||
config: {
|
||
publicBaseUrl: 'https://m.tkmind.cn',
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-1","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"好消息!m.tkmind.cn 其实是可以访问的,页面都已经成功发布了!以下是您的夏日主题页面:\\n\\n🌴 夏日主题页面已发布\\n\\nhttps://m.tkmind.cn/MindSpace/john/public/summer-breeze-journal.html"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-1","token_state":{"inputTokens":2,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
return new Response(JSON.stringify({ status: 'ok' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') {
|
||
return new Response(JSON.stringify({ ok: true }), {
|
||
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}`);
|
||
},
|
||
userAuth: {
|
||
async resolveWorkingDir() {
|
||
return '/tmp/user-1';
|
||
},
|
||
},
|
||
});
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = () => 'req-1';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我生成一个夏日页面' }), {
|
||
timestamp: '1710000000',
|
||
nonce: 'nonce',
|
||
signature: signatureFor('token', '1710000000', '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, /成功发布|主题页面已发布/);
|
||
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) => {
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-public-');
|
||
const htmlPath = `${workspaceRoot}/hello.html`;
|
||
fs.writeFileSync(htmlPath, '<!doctype html><title>Hello</title>');
|
||
|
||
const text = await maybeAttachPublishedHtmlLink(
|
||
{
|
||
text: '已完成',
|
||
messages: [
|
||
{
|
||
role: 'assistant',
|
||
content: [
|
||
{
|
||
type: 'toolRequest',
|
||
toolCall: {
|
||
value: {
|
||
name: 'developer',
|
||
arguments: {
|
||
action: 'write',
|
||
path: htmlPath,
|
||
content: '<!doctype html><title>Hello</title>',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
workingDir: workspaceRoot,
|
||
publicBaseUrl: 'https://m.tkmind.cn',
|
||
},
|
||
);
|
||
|
||
assert.equal(fs.existsSync(`${workspaceRoot}/public/hello.html`), true);
|
||
assert.match(
|
||
text,
|
||
/https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/hello\.html/,
|
||
);
|
||
});
|
||
|
||
test('maybeAttachPublishedHtmlLink recognizes sandbox-fs write_file html outputs', async () => {
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-sandbox-fs-');
|
||
const htmlPath = `${workspaceRoot}/public/codex-verify.html`;
|
||
fs.mkdirSync(`${workspaceRoot}/public`, { recursive: true });
|
||
fs.writeFileSync(htmlPath, '<!doctype html><title>Codex Verify</title>');
|
||
|
||
const text = await maybeAttachPublishedHtmlLink(
|
||
{
|
||
text: '页面已生成 ✅',
|
||
messages: [
|
||
{
|
||
role: 'assistant',
|
||
content: [
|
||
{
|
||
type: 'toolRequest',
|
||
toolCall: {
|
||
value: {
|
||
name: 'sandbox-fs__write_file',
|
||
arguments: {
|
||
path: 'public/codex-verify.html',
|
||
content: '<!doctype html><title>Codex Verify</title>',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
workingDir: workspaceRoot,
|
||
publicBaseUrl: 'https://m.tkmind.cn',
|
||
},
|
||
);
|
||
|
||
assert.match(
|
||
text,
|
||
/https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/codex-verify\.html/,
|
||
);
|
||
});
|
||
|
||
test('maybeAttachPublishedHtmlLink rewrites wrong public html links to the canonical published url', async () => {
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-rewrite-link-');
|
||
const htmlPath = `${workspaceRoot}/public/summer-breeze-journal.html`;
|
||
fs.mkdirSync(`${workspaceRoot}/public`, { recursive: true });
|
||
fs.writeFileSync(htmlPath, '<!doctype html><title>Summer</title>');
|
||
|
||
const text = await maybeAttachPublishedHtmlLink(
|
||
{
|
||
text: '页面已发布:https://m.tkmind.cn/public/summer-breeze-journal.html',
|
||
messages: [
|
||
{
|
||
role: 'assistant',
|
||
content: [
|
||
{
|
||
type: 'toolRequest',
|
||
toolCall: {
|
||
value: {
|
||
name: 'sandbox-fs__write_file',
|
||
arguments: {
|
||
path: 'public/summer-breeze-journal.html',
|
||
content: '<!doctype html><title>Summer</title>',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
workingDir: workspaceRoot,
|
||
publicBaseUrl: 'https://m.tkmind.cn',
|
||
},
|
||
);
|
||
|
||
assert.doesNotMatch(text, /https:\/\/m\.tkmind\.cn\/public\/summer-breeze-journal\.html/);
|
||
assert.match(
|
||
text,
|
||
/https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/summer-breeze-journal\.html/,
|
||
);
|
||
});
|
||
|
||
test('maybeAttachPublishedHtmlLink rewrites tkmind domain variants to the canonical published url', async () => {
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-rewrite-domain-');
|
||
const htmlPath = `${workspaceRoot}/public/codex-prod-check-v6.html`;
|
||
fs.mkdirSync(`${workspaceRoot}/public`, { recursive: true });
|
||
fs.writeFileSync(htmlPath, '<!doctype html><title>Prod Check</title>');
|
||
|
||
const text = await maybeAttachPublishedHtmlLink(
|
||
{
|
||
text: '唯一可访问链接:https://mindspace.tkmind.cn/a70ff537-8908-486e-9b6c-042e07cc25db/codex-prod-check-v6.html',
|
||
messages: [
|
||
{
|
||
role: 'assistant',
|
||
content: [
|
||
{
|
||
type: 'toolRequest',
|
||
toolCall: {
|
||
value: {
|
||
name: 'sandbox-fs__write_file',
|
||
arguments: {
|
||
path: 'public/codex-prod-check-v6.html',
|
||
content: '<!doctype html><title>Prod Check</title>',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
workingDir: workspaceRoot,
|
||
publicBaseUrl: 'https://m.tkmind.cn',
|
||
},
|
||
);
|
||
|
||
assert.doesNotMatch(text, /https:\/\/mindspace\.tkmind\.cn\/.+\/codex-prod-check-v6\.html/);
|
||
assert.match(
|
||
text,
|
||
/https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/codex-prod-check-v6\.html/,
|
||
);
|
||
});
|
||
|
||
test('maybeAttachPublishedHtmlLink can attach a verified existing public html link from provided artifacts', async () => {
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-existing-public-');
|
||
const htmlPath = `${workspaceRoot}/public/thailand-guide.html`;
|
||
fs.mkdirSync(`${workspaceRoot}/public`, { recursive: true });
|
||
fs.writeFileSync(htmlPath, previewReadyPageHtml({ title: 'Thailand', subtitle: '泰国旅行攻略' }));
|
||
|
||
const text = await maybeAttachPublishedHtmlLink(
|
||
{
|
||
text: '页面已生成!点击下方链接查看:',
|
||
messages: [],
|
||
},
|
||
{
|
||
workingDir: workspaceRoot,
|
||
publicBaseUrl: 'https://m.tkmind.cn',
|
||
artifacts: [
|
||
{
|
||
localPath: htmlPath,
|
||
relativePath: 'public/thailand-guide.html',
|
||
url: `https://m.tkmind.cn/MindSpace/${path.basename(workspaceRoot)}/public/thailand-guide.html`,
|
||
},
|
||
],
|
||
},
|
||
);
|
||
|
||
assert.match(text, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/thailand-guide\.html/);
|
||
});
|
||
test('wechat mp service splits long agent replies into multiple customer messages', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const longReply = '答'.repeat(900);
|
||
const wechatCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
progressDelayMs: 0,
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
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 '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
`data: {"type":"Message","request_id":"req-long","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":${JSON.stringify(longReply)}}]}}\n\n`,
|
||
'data: {"type":"Finish","request_id":"req-long","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
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 api path: ${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')) {
|
||
const payload = JSON.parse(init.body);
|
||
assert.ok(Buffer.byteLength(payload.text.content, 'utf8') <= WECHAT_CUSTOMER_TEXT_MAX_BYTES);
|
||
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-long';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '给我长回复' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
const sendCalls = wechatCalls.filter(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||
assert.ok(sendCalls.length > 1);
|
||
const combined = sendCalls
|
||
.map(([, , body]) => JSON.parse(body).text.content)
|
||
.join('');
|
||
assert.equal(combined, longReply);
|
||
});
|
||
|
||
test('wechat mp service surfaces wechat customer-service errcode and errmsg', async () => {
|
||
const service = createBoundWechatService({
|
||
userAuth: {
|
||
async getWechatOpenidForUser() {
|
||
return 'openid-1';
|
||
},
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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' },
|
||
});
|
||
}
|
||
return new Response(JSON.stringify({ errcode: 45047, errmsg: 'out of response count limit rid: test-rid' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
},
|
||
});
|
||
|
||
await assert.rejects(
|
||
service.sendTextToUser('user-1', 'hello'),
|
||
/errcode=45047.*out of response count limit/,
|
||
);
|
||
});
|
||
|
||
test('wechat mp service strips markdown emphasis around outbound links', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
progressDelayMs: 0,
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
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 '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
`data: {"type":"Message","request_id":"req-link-md","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"👉 **跑步路书**\\nhttps://example.com/running-route.html**"}]}}\n\n`,
|
||
'data: {"type":"Finish","request_id":"req-link-md","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
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 api path: ${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}`);
|
||
},
|
||
linkExists: async (urlText) => {
|
||
if (!String(urlText).includes('/summer-breeze-journal.html')) return false;
|
||
return fs.existsSync(htmlPath);
|
||
},
|
||
});
|
||
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = () => 'req-link-md';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '给我链接' }), {
|
||
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.equal(payload.text.content.includes('**'), false);
|
||
assert.equal(payload.text.content.includes('https://example.com/running-route.html**'), false);
|
||
assert.match(payload.text.content, /https:\/\/example\.com\/running-route\.html/);
|
||
});
|
||
|
||
test('wechat mp service does not send stale page links from unscoped conversation snapshots', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
const service = createBoundWechatService({
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"UpdateConversation","conversation":[{"id":"user-old","role":"user","metadata":{"userVisible":true},"content":[{"type":"text","text":"帮我做餐厅推荐"}]},{"id":"assistant-old","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"页面已生成!餐厅推荐\\nhttps://g2.tkmind.cn/MindSpace/old/public/restaurant.html"}]}]}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-stale","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
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 api path: ${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}`);
|
||
},
|
||
linkExists: async (urlText) => {
|
||
if (!String(urlText).includes('/summer-breeze-journal.html')) return false;
|
||
return fs.existsSync(htmlPath);
|
||
},
|
||
});
|
||
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = () => 'req-stale';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我做上马计划' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
const sendCalls = wechatCalls.filter(([url]) => String(url).includes('/cgi-bin/message/custom/send'));
|
||
assert.equal(sendCalls.length, 1);
|
||
const payload = JSON.parse(sendCalls[0][2]);
|
||
assert.doesNotMatch(payload.text.content, /restaurant\.html/);
|
||
assert.match(payload.text.content, /本轮未收到可发送的新回复/);
|
||
});
|
||
|
||
test('loadWechatMpConfig requires full config and enable flag', () => {
|
||
const config = loadWechatMpConfig({
|
||
H5_WECHAT_MP_ENABLED: '1',
|
||
H5_WECHAT_MP_APP_ID: 'wx123',
|
||
H5_WECHAT_MP_APP_SECRET: 'secret',
|
||
H5_WECHAT_MP_TOKEN: 'token',
|
||
H5_PUBLIC_BASE_URL: 'https://example.com',
|
||
});
|
||
assert.equal(config.enabled, true);
|
||
assert.equal(config.bindPath, '/auth/wechat/authorize?intent=login');
|
||
});
|
||
|
||
test('verifyWechatMpSignature accepts valid signature', () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
assert.equal(
|
||
verifyWechatMpSignature({
|
||
token,
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
}),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('verifyWechatMpUrlChallenge supports aes mode echostr', () => {
|
||
const token = 'memindprodmp';
|
||
const appId = 'wxaa5f3cbb9ee009c2';
|
||
const encodingAesKey = 'oovBs3npy6NdflERl0LObynzVwH8UgwtCKrfngwfBod';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce123';
|
||
const plainEcho = '77889900';
|
||
const key = Buffer.from(`${encodingAesKey}=`, 'base64');
|
||
const iv = key.subarray(0, 16);
|
||
const random16 = Buffer.alloc(16, 0x61);
|
||
const msgBuf = Buffer.from(plainEcho, 'utf8');
|
||
const appIdBuf = Buffer.from(appId, 'utf8');
|
||
const lenBuf = Buffer.alloc(4);
|
||
lenBuf.writeUInt32BE(msgBuf.length, 0);
|
||
const raw = Buffer.concat([random16, lenBuf, msgBuf, appIdBuf]);
|
||
const pad = 32 - (raw.length % 32);
|
||
const padded = Buffer.concat([raw, Buffer.alloc(pad, pad)]);
|
||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||
cipher.setAutoPadding(false);
|
||
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]).toString('base64');
|
||
const msgSignature = crypto
|
||
.createHash('sha1')
|
||
.update([token, timestamp, nonce, encrypted].sort().join(''))
|
||
.digest('hex');
|
||
const result = verifyWechatMpUrlChallenge(
|
||
{
|
||
encrypt_type: 'aes',
|
||
msg_signature: msgSignature,
|
||
timestamp,
|
||
nonce,
|
||
echostr: encrypted,
|
||
},
|
||
{ token, appId, encodingAesKey },
|
||
);
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.body, plainEcho);
|
||
});
|
||
|
||
test('buildWechatTextReply includes both parties and content', () => {
|
||
const xml = buildWechatTextReply({
|
||
toUserName: 'openid-1',
|
||
fromUserName: 'gh_test',
|
||
content: '收到',
|
||
createTime: 123,
|
||
});
|
||
assert.match(xml, /<ToUserName><!\[CDATA\[openid-1\]\]><\/ToUserName>/);
|
||
assert.match(xml, /<Content><!\[CDATA\[收到\]\]><\/Content>/);
|
||
});
|
||
|
||
test('wechat mp service prompts binding for unknown openid', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return null;
|
||
},
|
||
},
|
||
apiFetch: async () => new Response('{}', { status: 200 }),
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(inboundXml(), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /请先绑定/);
|
||
assert.match(result.body, /https:\/\/example.com\/auth\/wechat\/authorize\?intent=login/);
|
||
});
|
||
|
||
test('wechat mp service routes text to dedicated session and sends customer service reply', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const apiCalls = [];
|
||
const wechatCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
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 '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage(userId, sessionId, tokenState, requestId) {
|
||
apiCalls.push(['bill', userId, sessionId, tokenState, requestId]);
|
||
},
|
||
},
|
||
apiFetch: async (pathname, init = {}) => {
|
||
apiCalls.push(['fallback', pathname, init.method ?? 'GET']);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
apiCalls.push([pathname, init.method ?? 'GET']);
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-1","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"wx_abcd1234,页面已创建完毕!\\n[标题](https://example.com/page.html)\\n\\n| 项 | 值 |\\n| --- | --- |\\n| A | B |"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-1","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
const body = JSON.parse(init.body);
|
||
assert.match(body.user_message.content[0].text, /用户消息:帮我做个页面/);
|
||
assert.match(body.user_message.content[0].text, /static-page-publish/);
|
||
body.request_id && (apiCalls[apiCalls.length - 1][2] = body.request_id);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/harness_remember') {
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.sessionId, 'session-1');
|
||
assert.match(body.content, /当前服务号用户名称/);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/session/reconcile') {
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.user_context.displayName, '毕升');
|
||
assert.equal(body.user_context.username, '毕升');
|
||
assert.equal(body.user_context.slug, null);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/harness_bootstrap') {
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.sessionId, 'session-1');
|
||
assert.equal(body.force, false);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
throw new Error(`unexpected api path: ${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')) {
|
||
const payload = JSON.parse(init.body);
|
||
assert.equal(payload.touser, 'openid-1');
|
||
assert.match(payload.text.content, /页面已创建完毕/);
|
||
assert.doesNotMatch(payload.text.content, /^wx_/);
|
||
assert.match(payload.text.content, /^毕升,页面已创建完毕/);
|
||
assert.doesNotMatch(payload.text.content, /\[标题\]\(/);
|
||
assert.match(payload.text.content, /标题\nhttps:\/\/example.com\/page.html/);
|
||
assert.match(payload.text.content, /项:A;值:B/);
|
||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected wechat url: ${url}`);
|
||
},
|
||
linkExists: async (urlText) => {
|
||
if (!String(urlText).includes('/summer-breeze-journal.html')) return false;
|
||
return fs.existsSync(htmlPath);
|
||
},
|
||
});
|
||
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
let uuidCalls = 0;
|
||
crypto.randomUUID = () => {
|
||
uuidCalls += 1;
|
||
return uuidCalls === 1 ? 'req-1' : `uuid-${uuidCalls}`;
|
||
};
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我做个页面' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /<Content>/);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
assert.equal(
|
||
apiCalls.some(([pathname]) => pathname === '/sessions/session-1/reply'),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
apiCalls.some(([kind]) => kind === 'fallback'),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
wechatCalls.some(([url]) => String(url).includes('/cgi-bin/message/custom/send')),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('wechat mp service rewrites internal wx username mentions that appear mid-message', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const sentPayloads = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
progressDelayMs: 0,
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: 'Lumi' };
|
||
},
|
||
async getWechatAgentRoute() {
|
||
return { agentSessionId: 'session-1' };
|
||
},
|
||
async clearWechatAgentRoute() {},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'Lumi', username: 'wx_ul610et8', slug: 'wx_ul610et8', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-mid","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"LumiMind 这个感觉很好。\\n\\nwx_ul610et8,这个感觉对了。\\n好的,wx_ul610et8,为你写了一首。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-mid","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url, init = {}) => {
|
||
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')) {
|
||
sentPayloads.push(JSON.parse(init.body));
|
||
return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected wechat url: ${url}`);
|
||
},
|
||
linkExists: async (urlText) => {
|
||
if (!String(urlText).includes('/summer-breeze-journal.html')) return false;
|
||
return fs.existsSync(htmlPath);
|
||
},
|
||
});
|
||
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = () => 'req-mid';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ msgType: 'text', content: '这个名字怎么样' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
assert.equal(sentPayloads.length, 1);
|
||
assert.doesNotMatch(sentPayloads[0].text.content, /wx_ul610et8/);
|
||
assert.match(sentPayloads[0].text.content, /\nLumi,这个感觉对了。/);
|
||
assert.match(sentPayloads[0].text.content, /好的,Lumi,为你写了一首。/);
|
||
});
|
||
|
||
test('wechat mp service applies provider before forwarding to dedicated session', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const appliedSessions = [];
|
||
const service = createBoundWechatService({
|
||
token,
|
||
applySessionLlmProvider: async (sessionId) => {
|
||
appliedSessions.push(sessionId);
|
||
return { ok: true, providerId: 'custom_tkmind_relay_deepseek', model: 'deepseek-chat' };
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-provider","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-provider","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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-provider';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我推荐一个跑步路线' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
assert.deepEqual(appliedSessions, ['session-1']);
|
||
});
|
||
|
||
test('wechat mp service rotates an idle dedicated session before replying', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const clearedRoutes = [];
|
||
const upsertedSessions = [];
|
||
const sessionApiCalls = [];
|
||
let routeCleared = false;
|
||
const service = createBoundWechatService({
|
||
token,
|
||
config: {
|
||
sessionIdleRotateMs: 1000,
|
||
},
|
||
startAgentSession: async () => ({ id: 'session-2' }),
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
sessionApiCalls.push([sessionId, pathname, init.body ?? null]);
|
||
assert.equal(sessionId, 'session-2');
|
||
if (pathname === '/sessions/session-2/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-rotate","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到,新的会话已接上。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-rotate","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-2/reply') {
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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}`);
|
||
},
|
||
userAuth: {
|
||
async getWechatAgentRoute() {
|
||
if (routeCleared) return null;
|
||
return {
|
||
agentSessionId: 'session-1',
|
||
status: 'active',
|
||
updatedAt: Date.now() - 60_000,
|
||
};
|
||
},
|
||
async clearWechatAgentRoute(appId, openid) {
|
||
routeCleared = true;
|
||
clearedRoutes.push([appId, openid]);
|
||
},
|
||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||
upsertedSessions.push(agentSessionId);
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
},
|
||
});
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = () => 'req-rotate';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '继续' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
assert.deepEqual(clearedRoutes, [['wx123', 'openid-1']]);
|
||
assert.deepEqual(upsertedSessions, ['session-2']);
|
||
assert.ok(sessionApiCalls.some(([sessionId, pathname]) => sessionId === 'session-2' && pathname === '/sessions/session-2/reply'));
|
||
});
|
||
|
||
test('wechat mp service rotates a dedicated session after the message-count threshold', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
let routeCleared = false;
|
||
const countedSessions = [];
|
||
const service = createBoundWechatService({
|
||
token,
|
||
config: {
|
||
sessionIdleRotateMs: 0,
|
||
sessionMessageRotateCount: 2,
|
||
},
|
||
startAgentSession: async () => ({ id: 'session-2' }),
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
assert.equal(sessionId, 'session-2');
|
||
if (pathname === '/sessions/session-2/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-count-rotate","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已换新会话。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-count-rotate","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-2/reply') {
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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}`);
|
||
},
|
||
userAuth: {
|
||
async getWechatAgentRoute() {
|
||
if (routeCleared) return null;
|
||
return {
|
||
agentSessionId: 'session-1',
|
||
status: 'active',
|
||
updatedAt: Date.now(),
|
||
};
|
||
},
|
||
async countWechatAgentSessionMessages({ agentSessionId }) {
|
||
countedSessions.push(agentSessionId);
|
||
return 2;
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
},
|
||
async upsertWechatAgentRoute() {},
|
||
async finishWechatMpMessage() {},
|
||
},
|
||
});
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = () => 'req-count-rotate';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '继续' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
assert.deepEqual(countedSessions, ['session-1']);
|
||
assert.equal(routeCleared, true);
|
||
});
|
||
|
||
test('wechat mp service rotates a dedicated session when snapshot message count exceeds threshold', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
let routeCleared = false;
|
||
const service = createBoundWechatService({
|
||
token,
|
||
config: {
|
||
sessionIdleRotateMs: 0,
|
||
sessionMessageRotateCount: 200,
|
||
},
|
||
startAgentSession: async () => ({ id: 'session-2' }),
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
assert.equal(sessionId, 'session-2');
|
||
if (pathname === '/sessions/session-2/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-snapshot-rotate","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已换新会话。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-snapshot-rotate","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-2/reply') {
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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}`);
|
||
},
|
||
userAuth: {
|
||
async getWechatAgentRoute() {
|
||
if (routeCleared) return null;
|
||
return {
|
||
agentSessionId: 'session-1',
|
||
status: 'active',
|
||
updatedAt: Date.now(),
|
||
};
|
||
},
|
||
async countWechatAgentSessionMessages() {
|
||
return 3;
|
||
},
|
||
async getWechatAgentSessionSnapshotMessageCount() {
|
||
return 616;
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
},
|
||
async upsertWechatAgentRoute() {},
|
||
async finishWechatMpMessage() {},
|
||
},
|
||
});
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = () => 'req-snapshot-rotate';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '继续' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
assert.equal(routeCleared, true);
|
||
});
|
||
|
||
test('wechat mp service skips repeated user-context bootstrap for the same session', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const bootstrapBodies = [];
|
||
const service = createBoundWechatService({
|
||
token,
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-bootstrap","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-bootstrap","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/harness_remember') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/harness_bootstrap') {
|
||
bootstrapBodies.push(JSON.parse(String(init.body ?? '{}')));
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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-bootstrap';
|
||
try {
|
||
for (const content of ['第一条', '第二条']) {
|
||
const result = await service.handleInboundMessage(inboundXml({ content }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
}
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
assert.deepEqual(bootstrapBodies, [{ sessionId: 'session-1', force: false }]);
|
||
});
|
||
|
||
test('wechat mp service injects static page publish instructions for html generation requests', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const service = createBoundWechatService({
|
||
token,
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async insertWechatMpMessageDetail() {},
|
||
async finishWechatMpMessage() {},
|
||
async resolveWorkingDir() {
|
||
return '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-page","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到,我去生成页面。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-page","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
const body = JSON.parse(init.body);
|
||
assert.match(body.user_message.content[0].text, /static-page-publish/);
|
||
assert.match(body.user_message.content[0].text, /必须先调用 `load_skill`/);
|
||
assert.match(body.user_message.content[0].text, /写入 `public\/\*\.html`/);
|
||
assert.match(body.user_message.content[0].text, /唯一正确链接/);
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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-page';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '请生成一个 html 页面,文件名 public/hello-world.html' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
});
|
||
|
||
test('wechat mp service injects schedule skill instructions for reminder-like requests', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const service = createBoundWechatService({
|
||
token,
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async insertWechatMpMessageDetail() {},
|
||
async finishWechatMpMessage() {},
|
||
async resolveWorkingDir() {
|
||
return '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-2","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到,我先帮你处理。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-2","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
const body = JSON.parse(init.body);
|
||
assert.match(body.user_message.content[0].text, /加载 `schedule-assistant` skill/);
|
||
assert.match(body.user_message.content[0].text, /schedule_create_item/);
|
||
assert.match(body.user_message.content[0].text, /sourceMessageId: 10001/);
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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-2';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ content: '明天早上六点去跑步,五点半提醒我' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
});
|
||
|
||
test('wechat mp service reconciles existing dedicated session before reply', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const calls = [];
|
||
let extensionsReadCount = 0;
|
||
const workingDir = '/Users/john/Project/Memind/MindSpace/user-1';
|
||
const extensionConfig = {
|
||
type: 'stdio',
|
||
name: 'sandbox-fs',
|
||
cmd: '/usr/local/bin/node',
|
||
args: ['/Users/john/Project/Memind/mindspace-sandbox-mcp.mjs', workingDir],
|
||
envs: { SANDBOX_ROOT: workingDir },
|
||
available_tools: ['list_dir'],
|
||
};
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
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 workingDir;
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return {
|
||
enableContextMemory: false,
|
||
extensionOverrides: [extensionConfig],
|
||
unrestricted: false,
|
||
gooseMode: 'auto',
|
||
};
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: 'base' };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
},
|
||
apiFetch: async (pathname, init = {}) => {
|
||
calls.push(['fallback', pathname, init.method ?? 'GET']);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
calls.push([pathname, init.method ?? 'GET']);
|
||
if (pathname === '/sessions/session-1') {
|
||
return new Response(JSON.stringify({ working_dir: '/root/tkmind_go/ui/h5/MindSpace/user-1' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === '/agent/update_working_dir') {
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.session_id, 'session-1');
|
||
assert.equal(body.working_dir, workingDir);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/update_session') {
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.session_id, 'session-1');
|
||
assert.equal(body.goose_mode, 'auto');
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/sessions/session-1/extensions') {
|
||
extensionsReadCount += 1;
|
||
const extensions =
|
||
extensionsReadCount > 1
|
||
? [{ name: 'sandbox-fs', available_tools: ['list_dir'] }]
|
||
: [];
|
||
return new Response(JSON.stringify({ extensions }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === '/agent/add_extension') {
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.session_id, 'session-1');
|
||
assert.equal(body.config.name, 'sandbox-fs');
|
||
assert.equal(body.config.args[1], workingDir);
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/restart') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/harness_remember') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/harness_bootstrap') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-1","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"收到"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-1","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url, init = {}) => {
|
||
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-1';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '继续' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
assert.equal(calls.some(([pathname]) => pathname === '/agent/update_working_dir'), true);
|
||
assert.equal(calls.some(([pathname]) => pathname === '/agent/add_extension'), true);
|
||
assert.equal(calls.some(([pathname]) => pathname === '/sessions/session-1/reply'), true);
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
});
|
||
|
||
test('wechat mp service recreates poisoned dedicated session after bare completion on html generation', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-poisoned-session-');
|
||
const htmlPath = `${workspaceRoot}/hello.html`;
|
||
let routeCleared = false;
|
||
let started = false;
|
||
|
||
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 routeCleared ? null : { agentSessionId: 'session-1' };
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return workspaceRoot;
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return {
|
||
enableContextMemory: false,
|
||
extensionOverrides: [
|
||
{
|
||
type: 'platform',
|
||
name: 'developer',
|
||
available_tools: ['write'],
|
||
},
|
||
],
|
||
unrestricted: false,
|
||
};
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { publishDir: workspaceRoot, displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute({ appId, openid, userId, agentSessionId }) {
|
||
assert.equal(appId, 'wx123');
|
||
assert.equal(openid, 'openid-1');
|
||
assert.equal(userId, 'user-1');
|
||
assert.equal(agentSessionId, 'session-2');
|
||
},
|
||
async billSessionUsage() {},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
async insertWechatMpMessageDetail() {},
|
||
},
|
||
apiFetch: async (pathname, init = {}) => {
|
||
if (pathname === '/agent/start') {
|
||
started = true;
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.working_dir, workspaceRoot);
|
||
return new Response(JSON.stringify({ id: 'session-2' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
if (pathname === `/sessions/${sessionId}/extensions`) {
|
||
return new Response(JSON.stringify({ extensions: [{ name: 'developer', available_tools: ['write'] }] }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === '/agent/update_working_dir' || pathname === '/agent/update_session' || pathname === '/agent/restart') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/add_extension') {
|
||
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' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}`) {
|
||
return new Response(JSON.stringify({ working_dir: workspaceRoot }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||
if (sessionId === 'session-2') {
|
||
fs.writeFileSync(htmlPath, previewReadyPageHtml({ title: 'Hello', subtitle: 'Hello 页面' }));
|
||
}
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/events`) {
|
||
if (sessionId === 'session-1') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-poisoned","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已完成"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-poisoned","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-poisoned-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-poisoned-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":"${jsonEscapedPreviewReadyPageHtml({ title: 'Hello', subtitle: 'Hello 页面' })}"}}}}]}}\n\n`,
|
||
'data: {"type":"Message","request_id":"req-poisoned-retry","message":{"id":"assistant-2","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已完成"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-poisoned-retry","token_state":{"inputTokens":2,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
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}`);
|
||
},
|
||
linkExists: async (urlText) => {
|
||
if (!String(urlText).includes('/summer-breeze-journal.html')) return false;
|
||
return fs.existsSync(htmlPath);
|
||
},
|
||
});
|
||
|
||
const originalRandomUuid = crypto.randomUUID;
|
||
crypto.randomUUID = (() => {
|
||
const ids = ['req-poisoned', 'req-poisoned-retry'];
|
||
return () => ids.shift() ?? 'req-poisoned-retry';
|
||
})();
|
||
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;
|
||
}
|
||
|
||
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.match(payload.text.content, /已完成/);
|
||
assert.match(payload.text.content, /页面生成未完成|hello\.html/);
|
||
});
|
||
|
||
test('wechat mp service forwards html link when write_file created a real page without static-page-publish skill', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-missing-skill-');
|
||
const htmlPath = path.join(workspaceRoot, 'public', 'hello-world.html');
|
||
let routeCleared = false;
|
||
let started = false;
|
||
|
||
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 routeCleared ? null : { agentSessionId: 'session-1' };
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return workspaceRoot;
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return {
|
||
enableContextMemory: false,
|
||
extensionOverrides: [
|
||
{
|
||
type: 'platform',
|
||
name: 'developer',
|
||
available_tools: ['write'],
|
||
},
|
||
],
|
||
unrestricted: false,
|
||
};
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { publishDir: workspaceRoot, displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||
assert.equal(agentSessionId, 'session-2');
|
||
},
|
||
async billSessionUsage() {},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
async insertWechatMpMessageDetail() {},
|
||
},
|
||
apiFetch: async (pathname, init = {}) => {
|
||
if (pathname === '/agent/start') {
|
||
started = true;
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.working_dir, workspaceRoot);
|
||
return new Response(JSON.stringify({ id: 'session-2' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
if (pathname === `/sessions/${sessionId}/extensions`) {
|
||
return new Response(JSON.stringify({ extensions: [{ name: 'developer', available_tools: ['write'] }] }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === '/agent/update_working_dir' || pathname === '/agent/update_session' || pathname === '/agent/restart') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/add_extension') {
|
||
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' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}`) {
|
||
return new Response(JSON.stringify({ working_dir: workspaceRoot }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||
if (sessionId === 'session-2') {
|
||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||
fs.writeFileSync(htmlPath, previewReadyPageHtml({ title: 'Hello', subtitle: 'Hello 页面' }));
|
||
}
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/events`) {
|
||
if (sessionId === 'session-1') {
|
||
return new Response(
|
||
[
|
||
`data: {"type":"Message","request_id":"req-skill-miss","message":{"id":"assistant-tool","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"toolRequest","toolCall":{"value":{"name":"sandbox-fs__write_file","arguments":{"path":"public/hello-world.html","content":"${jsonEscapedPreviewReadyPageHtml({ title: 'Hello World', subtitle: 'Hello World 页面' })}"}}}}]}}\n\n`,
|
||
'data: {"type":"Message","request_id":"req-skill-miss","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"页面已生成:https://m.tkmind.cn/MindSpace/john/public/hello-world.html"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-skill-miss","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-skill-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-skill-retry","message":{"id":"assistant-tool","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"toolRequest","toolCall":{"value":{"name":"sandbox-fs__write_file","arguments":{"path":"public/hello-world.html","content":"${jsonEscapedPreviewReadyPageHtml({ title: 'Hello World', subtitle: 'Hello World 页面' })}"}}}}]}}\n\n`,
|
||
'data: {"type":"Message","request_id":"req-skill-retry","message":{"id":"assistant-2","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"[Hello World](https://m.tkmind.cn/MindSpace/user-1/public/hello-world.html)"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-skill-retry","token_state":{"inputTokens":2,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
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 = (() => {
|
||
const ids = ['req-skill-miss', 'req-skill-retry'];
|
||
return () => ids.shift() ?? 'req-skill-retry';
|
||
})();
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '请生成一个页面 public/hello-world.html' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
assert.equal(routeCleared, false);
|
||
assert.equal(started, 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, /hello-world\.html/);
|
||
assert.match(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/hello-world\.html/);
|
||
});
|
||
|
||
test('wechat mp service trusts an existing requested public html file when the assistant only returns a title', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-existing-requested-');
|
||
const htmlPath = path.join(workspaceRoot, 'public', 'thailand-guide.html');
|
||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||
fs.writeFileSync(htmlPath, previewReadyPageHtml({ title: 'Thailand', subtitle: '泰国旅行攻略' }));
|
||
|
||
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-existing","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-existing","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"页面已生成!点击下方链接查看:\\n\\n🌴 泰国旅行攻略 · 说走就走"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-existing","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||
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-existing';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '请生成 thailand-guide.html 页面' }), {
|
||
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.match(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/thailand-guide\.html/);
|
||
assert.doesNotMatch(payload.text.content, /页面生成未完成/);
|
||
});
|
||
|
||
test('wechat mp service attaches the newly generated page link even when no html tool call is streamed', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-recent-public-');
|
||
const htmlPath = 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","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","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"页面已生成!点击下方链接查看:\\n\\n🌴 夏日主题页面"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-recent","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(htmlPath), { recursive: true });
|
||
fs.writeFileSync(htmlPath, previewReadyPageHtml({ title: 'Summer', subtitle: '夏日主题页面' }));
|
||
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';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我生成一个夏日页面' }), {
|
||
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.match(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/summer-breeze-journal\.html/);
|
||
assert.doesNotMatch(payload.text.content, /页面生成未完成|旧指令污染|稍后重试/);
|
||
});
|
||
|
||
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 forwards H5 agent text when page generation produced no file or fake link', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-fallback-page-');
|
||
|
||
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-fallback","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"🌴 泰国简易攻略"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-fallback","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||
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-fallback';
|
||
try {
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我做一个泰国攻略页面,稍微简单一点的' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
const htmlPath = path.join(workspaceRoot, 'public', 'thailand-guide.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, /🌴 泰国简易攻略/);
|
||
assert.doesNotMatch(payload.text.content, /没有按 H5 里的页面技能真正生成成功/);
|
||
assert.doesNotMatch(payload.text.content, /https:\/\/m\.tkmind\.cn\/MindSpace\/.+\/public\/thailand-guide\.html/);
|
||
});
|
||
|
||
test('isRecoverableWechatAgentSessionError detects poisoned tool_calls history', () => {
|
||
assert.equal(
|
||
isRecoverableWechatAgentSessionError(
|
||
"Request failed: Bad request (400): An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message).",
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(isRecoverableWechatAgentSessionError('stale_session_poisoned_completion'), true);
|
||
assert.equal(isRecoverableWechatAgentSessionError('无权访问该会话'), true);
|
||
assert.equal(isRecoverableWechatAgentSessionError('network timeout'), false);
|
||
});
|
||
|
||
test('findRecoverableWechatAgentErrorInReply scans all assistant messages', () => {
|
||
const toolCallsError =
|
||
"Request failed: Bad request (400): An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message).";
|
||
const reply = {
|
||
text: '唐,好的,请稍等片刻。',
|
||
messages: [
|
||
{
|
||
id: 'assistant-1',
|
||
role: 'assistant',
|
||
content: [{ type: 'text', text: '唐,好的,请稍等片刻。' }],
|
||
},
|
||
{
|
||
id: 'assistant-2',
|
||
role: 'assistant',
|
||
content: [{ type: 'text', text: `Ran into this error:\n${toolCallsError}` }],
|
||
},
|
||
],
|
||
};
|
||
assert.match(findRecoverableWechatAgentErrorInReply(reply), /tool_calls/);
|
||
assert.throws(() => assertWechatAgentReplyIsSendable(reply), /tool_calls/);
|
||
});
|
||
|
||
test('sanitizeWechatAgentOutboundText replaces raw api errors with friendly text', () => {
|
||
const raw =
|
||
"Ran into this error:\nRequest failed: Bad request (400): An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'.";
|
||
assert.match(sanitizeWechatAgentOutboundText(raw), /切换到新会话/);
|
||
assert.equal(isWechatAgentApiErrorText(raw), true);
|
||
assert.match(
|
||
sanitizeWechatAgentOutboundText('唐,好的。\n\nRan into this error:\nRequest failed: Bad request (400): tool_calls'),
|
||
/唐,好的/,
|
||
);
|
||
assert.doesNotMatch(
|
||
sanitizeWechatAgentOutboundText('唐,好的。\n\nRan into this error:\nRequest failed: Bad request (400): tool_calls'),
|
||
/Bad request/,
|
||
);
|
||
});
|
||
|
||
test('wechat mp service recreates dedicated session when tool_calls error arrives via Finish assistant text', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
let routeCleared = false;
|
||
let started = false;
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-tool-calls-finish-');
|
||
const toolCallsError =
|
||
"Request failed: Bad request (400): An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message).";
|
||
|
||
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 routeCleared ? null : { agentSessionId: 'session-1' };
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return workspaceRoot;
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return {
|
||
enableContextMemory: false,
|
||
extensionOverrides: [
|
||
{
|
||
type: 'platform',
|
||
name: 'developer',
|
||
available_tools: ['write'],
|
||
},
|
||
],
|
||
unrestricted: false,
|
||
};
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { publishDir: workspaceRoot, displayName: '唐', username: 'wx_ul610et8', slug: 'wx_ul610et8', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||
assert.equal(agentSessionId, 'session-2');
|
||
},
|
||
async billSessionUsage() {},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
async insertWechatMpMessageDetail() {},
|
||
},
|
||
apiFetch: async (pathname, init = {}) => {
|
||
if (pathname === '/agent/start') {
|
||
started = true;
|
||
return new Response(JSON.stringify({ id: 'session-2' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
if (pathname === `/sessions/${sessionId}/extensions`) {
|
||
return new Response(JSON.stringify({ extensions: [{ name: 'developer', available_tools: ['write'] }] }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (
|
||
pathname === '/agent/update_working_dir' ||
|
||
pathname === '/agent/update_session' ||
|
||
pathname === '/agent/restart' ||
|
||
pathname === '/agent/add_extension' ||
|
||
pathname === '/agent/harness_remember' ||
|
||
pathname === '/agent/harness_bootstrap'
|
||
) {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}`) {
|
||
return new Response(JSON.stringify({ working_dir: workspaceRoot }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/events`) {
|
||
if (sessionId === 'session-1') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-tool-finish","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"唐,好的,请稍等片刻。"}]}}\n\n',
|
||
`data: {"type":"Message","request_id":"req-tool-finish","message":{"id":"assistant-2","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"Ran into this error:\\n${toolCallsError.replace(/"/g, '\\"')}"}]}}\n\n`,
|
||
'data: {"type":"Finish","request_id":"req-tool-finish","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-tool-retry","message":{"id":"assistant-3","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已恢复,可以继续对话。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-tool-retry","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
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 = (() => {
|
||
const ids = ['req-tool-finish', 'req-tool-retry'];
|
||
return () => ids.shift() ?? 'req-tool-retry';
|
||
})();
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ content: '帮我生成页面查看吧' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
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.match(payload.text.content, /已恢复,可以继续对话/);
|
||
assert.doesNotMatch(payload.text.content, /Bad request/);
|
||
assert.doesNotMatch(payload.text.content, /tool_calls/);
|
||
});
|
||
|
||
test('wechat mp service recreates dedicated session after poisoned tool_calls history', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
let routeCleared = false;
|
||
let started = false;
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-tool-calls-poison-');
|
||
|
||
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 routeCleared ? null : { agentSessionId: 'session-1' };
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return workspaceRoot;
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return {
|
||
enableContextMemory: false,
|
||
extensionOverrides: [
|
||
{
|
||
type: 'platform',
|
||
name: 'developer',
|
||
available_tools: ['write'],
|
||
},
|
||
],
|
||
unrestricted: false,
|
||
};
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { publishDir: workspaceRoot, displayName: '唐', username: 'wx_ul610et8', slug: 'wx_ul610et8', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||
assert.equal(agentSessionId, 'session-2');
|
||
},
|
||
async billSessionUsage() {},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
async insertWechatMpMessageDetail() {},
|
||
},
|
||
apiFetch: async (pathname, init = {}) => {
|
||
if (pathname === '/agent/start') {
|
||
started = true;
|
||
return new Response(JSON.stringify({ id: 'session-2' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
if (pathname === `/sessions/${sessionId}/extensions`) {
|
||
return new Response(JSON.stringify({ extensions: [{ name: 'developer', available_tools: ['write'] }] }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (
|
||
pathname === '/agent/update_working_dir' ||
|
||
pathname === '/agent/update_session' ||
|
||
pathname === '/agent/restart' ||
|
||
pathname === '/agent/add_extension' ||
|
||
pathname === '/agent/harness_remember' ||
|
||
pathname === '/agent/harness_bootstrap'
|
||
) {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}`) {
|
||
return new Response(JSON.stringify({ working_dir: workspaceRoot }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/events`) {
|
||
if (sessionId === 'session-1') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Error","request_id":"req-tool-poison","error":"Request failed: Bad request (400): An assistant message with \'tool_calls\' must be followed by tool messages responding to each \'tool_call_id\'. (insufficient tool messages following tool_calls message)."}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-tool-retry","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已恢复,可以继续对话。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-tool-retry","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
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 = (() => {
|
||
const ids = ['req-tool-poison', 'req-tool-retry'];
|
||
return () => ids.shift() ?? 'req-tool-retry';
|
||
})();
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ content: '帮我查一下今天的待办' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
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.match(payload.text.content, /已恢复,可以继续对话/);
|
||
});
|
||
|
||
test('wechat mp service retries poisoned publish claims before forwarding H5 retry text', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const wechatCalls = [];
|
||
let routeCleared = false;
|
||
let started = false;
|
||
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-publish-claim-');
|
||
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 routeCleared ? null : { agentSessionId: 'session-1' };
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return workspaceRoot;
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return {
|
||
enableContextMemory: false,
|
||
extensionOverrides: [
|
||
{
|
||
type: 'platform',
|
||
name: 'developer',
|
||
available_tools: ['write'],
|
||
},
|
||
],
|
||
unrestricted: false,
|
||
};
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { publishDir: workspaceRoot, displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute({ agentSessionId }) {
|
||
assert.equal(agentSessionId, 'session-2');
|
||
},
|
||
async billSessionUsage() {},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
async insertWechatMpMessageDetail() {},
|
||
},
|
||
apiFetch: async (pathname, init = {}) => {
|
||
if (pathname === '/agent/start') {
|
||
started = true;
|
||
const body = JSON.parse(init.body);
|
||
assert.equal(body.working_dir, workspaceRoot);
|
||
return new Response(JSON.stringify({ id: 'session-2' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname) => {
|
||
if (pathname === `/sessions/${sessionId}/extensions`) {
|
||
return new Response(JSON.stringify({ extensions: [{ name: 'developer', available_tools: ['write'] }] }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === '/agent/update_working_dir' || pathname === '/agent/update_session' || pathname === '/agent/restart') {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === '/agent/add_extension') {
|
||
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' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}`) {
|
||
return new Response(JSON.stringify({ working_dir: workspaceRoot }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/reply`) {
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
}
|
||
if (pathname === `/sessions/${sessionId}/events`) {
|
||
if (sessionId === 'session-1') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-claim","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"好消息!m.tkmind.cn 其实是可以访问的,页面都已经成功发布了!以下是您的夏日主题页面:\\n\\n🌴 夏日主题页面已发布\\n\\nhttps://m.tkmind.cn/MindSpace/john/public/summer-breeze-journal.html"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-claim","token_state":{"inputTokens":1,"outputTokens":1}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
return new Response(
|
||
[
|
||
'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' } },
|
||
);
|
||
}
|
||
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 = (() => {
|
||
const ids = ['req-claim', 'req-claim-retry'];
|
||
return () => ids.shift() ?? 'req-claim-retry';
|
||
})();
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ content: '帮我生成一个夏日页面' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
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, /🌴 夏日主题页面/);
|
||
assert.doesNotMatch(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 () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const outbound = [];
|
||
const service = createBoundWechatService({
|
||
token,
|
||
scheduleService: {
|
||
async listItemsBySourceMessage(input) {
|
||
assert.equal(input.userId, 'user-1');
|
||
assert.equal(input.sourceMessageId, '10001');
|
||
return [];
|
||
},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-guard","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"已经设置好了!明早 5:30 提醒起床,6:00 去跑步。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-guard","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url, init = {}) => {
|
||
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')) {
|
||
outbound.push(JSON.parse(init.body).text.content);
|
||
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-guard';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ content: '明天早上六点去跑步,五点半提醒我' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
assert.equal(outbound.length, 1);
|
||
assert.match(outbound[0], /没有确认到待办\/提醒已经写入系统/);
|
||
assert.doesNotMatch(outbound[0], /已经设置好了/);
|
||
});
|
||
|
||
test('wechat mp service creates daily todo digest without routing to agent', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const apiCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage(input) {
|
||
apiCalls.push(['finish', input.status, input.agentSessionId]);
|
||
},
|
||
},
|
||
scheduleService: {
|
||
async createDailyTodoDigest(input) {
|
||
apiCalls.push(['digest', input.userId, input.hour, input.minute, input.channel]);
|
||
return {
|
||
id: 'sub-1',
|
||
userId: input.userId,
|
||
hour: input.hour,
|
||
minute: input.minute,
|
||
timezone: input.timezone,
|
||
channel: input.channel,
|
||
};
|
||
},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
apiCalls.push(['agent', pathname]);
|
||
return new Response('{}', { status: 200 });
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ content: '每天早上 7 点给我发一天的待办记录' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /每天早上 7点/);
|
||
assert.deepEqual(apiCalls, [
|
||
['digest', 'user-1', 7, 0, 'wechat'],
|
||
['finish', 'done', null],
|
||
]);
|
||
});
|
||
|
||
test('wechat mp service records plain todo without routing to agent', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const apiCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage(input) {
|
||
apiCalls.push(['finish', input.status, input.agentSessionId]);
|
||
},
|
||
},
|
||
scheduleService: {
|
||
async createItem(input) {
|
||
apiCalls.push(['todo', input.userId, input.kind, input.title, input.sourceChannel]);
|
||
return {
|
||
id: 'item-1',
|
||
title: input.title,
|
||
};
|
||
},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
apiCalls.push(['agent', pathname]);
|
||
return new Response('{}', { status: 200 });
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({ content: '帮我记一下「🍽️ 跟段吃饭」' }),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /已记录到待办列表/);
|
||
assert.deepEqual(apiCalls, [
|
||
['todo', 'user-1', 'task', '🍽️ 跟段吃饭', 'wechat'],
|
||
['finish', 'done', null],
|
||
]);
|
||
});
|
||
|
||
test('wechat mp service creates balance low alert without routing to agent', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const apiCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage(input) {
|
||
apiCalls.push(['finish', input.status, input.agentSessionId]);
|
||
},
|
||
},
|
||
scheduleService: {
|
||
async createBalanceLowAlert(input) {
|
||
apiCalls.push(['balance', input.userId, input.thresholdCents, input.sourceChannel]);
|
||
return {
|
||
id: 'alert-1',
|
||
thresholdCents: input.thresholdCents,
|
||
};
|
||
},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
apiCalls.push(['agent', pathname]);
|
||
return new Response('{}', { status: 200 });
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '余额低于 20 元提醒我' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /已设置:当余额低于 20\.00 元时,我会通过服务号提醒你。/);
|
||
assert.deepEqual(apiCalls, [
|
||
['balance', 'user-1', 2000, 'wechat'],
|
||
['finish', 'done', null],
|
||
]);
|
||
});
|
||
|
||
test('wechat mp service asks user to retry when voice recognition is empty', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
let replyCalled = false;
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
async insertWechatMpMessageDetail() {},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
if (pathname.includes('/reply')) replyCalled = true;
|
||
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({
|
||
msgType: 'voice',
|
||
content: '',
|
||
extraFields: { Format: 'amr' },
|
||
}),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /未识别到语音文字/);
|
||
assert.equal(replyCalled, false);
|
||
});
|
||
|
||
test('wechat mp service falls back to ASR when voice recognition is empty', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
let replyCalled = false;
|
||
const prompts = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
asrTarget: 'https://asr.example.com',
|
||
},
|
||
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 '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
async insertWechatMpMessageDetail() {},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-voice-fallback","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"我收到你的语音了。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-voice-fallback","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
replyCalled = true;
|
||
const body = JSON.parse(init.body);
|
||
prompts.push(body.user_message.content[0].text);
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url, init = {}) => {
|
||
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/media/get')) {
|
||
return new Response(Buffer.from('fake-amr-audio'), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'audio/amr' },
|
||
});
|
||
}
|
||
if (String(url).includes('https://asr.example.com/asr/oneshot')) {
|
||
assert.equal(init.method, 'POST');
|
||
return new Response(JSON.stringify({ code: 200, data: { text: '帮我整理今天的会议纪要' } }), {
|
||
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-voice-fallback';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({
|
||
msgType: 'voice',
|
||
content: '',
|
||
extraFields: { MediaId: 'media-1', Format: 'amr' },
|
||
}),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, 200);
|
||
assert.doesNotMatch(result.body, /未识别到语音文字/);
|
||
await result.task;
|
||
assert.equal(replyCalled, true);
|
||
assert.match(prompts[0], /帮我整理今天的会议纪要/);
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
});
|
||
|
||
test('wechat mp service persists image and routes image url into agent prompt', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const testUserId = 'test-user-image';
|
||
const prompts = [];
|
||
const detailCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
mediaPublicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
progressDelayMs: 0,
|
||
maxImageBytes: 1024 * 1024,
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: testUserId, status: 'active', nickname: '毕升' };
|
||
},
|
||
async getWechatAgentRoute() {
|
||
return { agentSessionId: 'session-1' };
|
||
},
|
||
async clearWechatAgentRoute() {},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession() {},
|
||
async upsertWechatAgentRoute() {},
|
||
async billSessionUsage() {},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: true };
|
||
},
|
||
async finishWechatMpMessage() {},
|
||
async insertWechatMpMessageDetail(input) {
|
||
detailCalls.push(input);
|
||
},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-image","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"请告诉我希望怎么处理这张图片。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-image","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
const body = JSON.parse(init.body);
|
||
prompts.push(body.user_message.content[0].text);
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url, init = {}) => {
|
||
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/media/get')) {
|
||
return new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'image/png' },
|
||
});
|
||
}
|
||
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-image';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({
|
||
msgType: 'image',
|
||
content: '',
|
||
extraFields: { MediaId: 'media-1', PicUrl: 'https://wx.example.com/image.png' },
|
||
}),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /<Content>/);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), {
|
||
recursive: true,
|
||
force: true,
|
||
});
|
||
}
|
||
|
||
assert.equal(prompts.length, 1);
|
||
assert.match(prompts[0], /【微信服务号图片消息】/);
|
||
assert.match(
|
||
prompts[0],
|
||
/\[图片1\]: https:\/\/example\.com\/MindSpace\/test-user-image\/public\/wechat-mp\//,
|
||
);
|
||
assert.equal(detailCalls.length, 1);
|
||
assert.match(detailCalls[0].mediaPublicUrl, /\/wechat-mp\//);
|
||
});
|
||
|
||
test('wechat mp service accepts full voice xml payload and routes recognition text into agent', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const prompts = [];
|
||
const metadataCalls = [];
|
||
const wechatCalls = [];
|
||
const service = createBoundWechatService({
|
||
token,
|
||
userAuth: {
|
||
async insertWechatMpMessageDetail(input) {
|
||
metadataCalls.push(input);
|
||
},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-voice","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"我收到你的语音了。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-voice","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
const body = JSON.parse(init.body);
|
||
prompts.push(body.user_message.content[0].text);
|
||
metadataCalls.push(body.user_message.metadata);
|
||
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 api path: ${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-voice';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({
|
||
msgType: 'voice',
|
||
content: '',
|
||
extraFields: {
|
||
MediaId: 'voice-media-1',
|
||
Format: 'amr',
|
||
Recognition: '帮我整理今天的会议纪要',
|
||
},
|
||
}),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /<Content>/);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
assert.equal(prompts.length, 1);
|
||
assert.match(prompts[0], /【微信服务号语音消息】/);
|
||
assert.match(prompts[0], /用户语音识别文本:帮我整理今天的会议纪要/);
|
||
assert.equal(metadataCalls.some((item) => item?.msgType === 'voice'), true);
|
||
assert.equal(
|
||
wechatCalls.some(([, , body]) => String(body ?? '').includes('我收到你的语音了')),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test('wechat mp service accepts full location xml payload and routes structured location prompt', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const prompts = [];
|
||
const detailCalls = [];
|
||
const service = createBoundWechatService({
|
||
token,
|
||
userAuth: {
|
||
async insertWechatMpMessageDetail(input) {
|
||
detailCalls.push(input);
|
||
},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-location","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"你附近有几家咖啡馆。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-location","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
const body = JSON.parse(init.body);
|
||
prompts.push(body.user_message.content[0].text);
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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-location';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({
|
||
msgType: 'location',
|
||
content: '',
|
||
extraFields: {
|
||
Location_X: '31.2304',
|
||
Location_Y: '121.4737',
|
||
Scale: '15',
|
||
Label: '上海市黄浦区人民广场',
|
||
},
|
||
}),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /<Content>/);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
assert.equal(prompts.length, 1);
|
||
assert.match(prompts[0], /【微信服务号位置消息】/);
|
||
assert.match(prompts[0], /地址:上海市黄浦区人民广场/);
|
||
assert.match(prompts[0], /纬度:31.2304/);
|
||
assert.match(prompts[0], /经度:121.4737/);
|
||
assert.equal(detailCalls.length, 1);
|
||
assert.equal(detailCalls[0].locationLabel, '上海市黄浦区人民广场');
|
||
});
|
||
|
||
test('wechat mp service accepts full link xml payload and routes structured link prompt', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const prompts = [];
|
||
const detailCalls = [];
|
||
const service = createBoundWechatService({
|
||
token,
|
||
userAuth: {
|
||
async insertWechatMpMessageDetail(input) {
|
||
detailCalls.push(input);
|
||
},
|
||
},
|
||
sessionApiFetch: async (sessionId, pathname, init = {}) => {
|
||
assert.equal(sessionId, 'session-1');
|
||
if (pathname === '/sessions/session-1/events') {
|
||
return new Response(
|
||
[
|
||
'data: {"type":"Message","request_id":"req-link","message":{"id":"assistant-1","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"这个链接看起来是在介绍 TKMind 的新功能。"}]}}\n\n',
|
||
'data: {"type":"Finish","request_id":"req-link","token_state":{"inputTokens":1,"outputTokens":2}}\n\n',
|
||
].join(''),
|
||
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
|
||
);
|
||
}
|
||
if (pathname === '/sessions/session-1/reply') {
|
||
const body = JSON.parse(init.body);
|
||
prompts.push(body.user_message.content[0].text);
|
||
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 api path: ${pathname}`);
|
||
},
|
||
wechatFetch: async (url) => {
|
||
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-link';
|
||
try {
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({
|
||
msgType: 'link',
|
||
content: '',
|
||
extraFields: {
|
||
Title: 'TKMind 发布说明',
|
||
Description: '一篇关于多模态服务号入口的介绍',
|
||
Url: 'https://example.com/articles/wechat-multimodal',
|
||
},
|
||
}),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /<Content>/);
|
||
await result.task;
|
||
} finally {
|
||
crypto.randomUUID = originalRandomUuid;
|
||
}
|
||
|
||
assert.equal(prompts.length, 1);
|
||
assert.match(prompts[0], /【微信服务号链接消息】/);
|
||
assert.match(prompts[0], /标题:TKMind 发布说明/);
|
||
assert.match(prompts[0], /描述:一篇关于多模态服务号入口的介绍/);
|
||
assert.match(prompts[0], /URL:https:\/\/example\.com\/articles\/wechat-multimodal/);
|
||
assert.equal(detailCalls.length, 1);
|
||
assert.equal(detailCalls[0].linkTitle, 'TKMind 发布说明');
|
||
});
|
||
|
||
test('wechat mp service records location events without routing to agent', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const detailCalls = [];
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async insertWechatMpMessageDetail(input) {
|
||
detailCalls.push(input);
|
||
},
|
||
},
|
||
apiFetch: async () => {
|
||
throw new Error('should not call agent api');
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(
|
||
inboundXml({
|
||
msgType: 'event',
|
||
content: '',
|
||
event: 'LOCATION',
|
||
extraFields: { Latitude: 31.2, Longitude: 121.5, Precision: 15 },
|
||
}),
|
||
{
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, 200);
|
||
assert.equal(result.body, 'success');
|
||
assert.equal(detailCalls.length, 1);
|
||
assert.equal(detailCalls[0].msgType, 'event');
|
||
});
|
||
|
||
test('wechat mp service returns status text for question mark probes', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
statusText: '我在这边',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
},
|
||
apiFetch: async () => new Response('{}', { status: 200 }),
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '?' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /我在这边/);
|
||
assert.match(result.body, /毕升/);
|
||
});
|
||
|
||
test('wechat mp service greets bound users with stored wechat nickname', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
let replyCalled = false;
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
if (pathname === '/sessions/session-1/reply') replyCalled = true;
|
||
return new Response('{}', { status: 200 });
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '你好' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /你好,毕升/);
|
||
assert.equal(replyCalled, false);
|
||
});
|
||
|
||
test('wechat mp service replies to connectivity test without agent', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
let replyCalled = false;
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active', nickname: '毕升' };
|
||
},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
if (pathname.includes('/reply')) replyCalled = true;
|
||
return new Response('{}', { status: 200 });
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '测试999' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /公众号消息通道正常/);
|
||
assert.equal(replyCalled, false);
|
||
});
|
||
|
||
test('wechat mp service ignores duplicated message ids', async () => {
|
||
const token = 'token';
|
||
const timestamp = '1710000000';
|
||
const nonce = 'nonce';
|
||
let replyCalled = false;
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token,
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async findWechatUserByOpenid() {
|
||
return { userId: 'user-1', status: 'active' };
|
||
},
|
||
async recordWechatMpMessage() {
|
||
return { inserted: false };
|
||
},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
if (pathname === '/sessions/session-1/reply') replyCalled = true;
|
||
return new Response('{}', { status: 200 });
|
||
},
|
||
});
|
||
|
||
const result = await service.handleInboundMessage(inboundXml({ content: '帮我继续处理' }), {
|
||
timestamp,
|
||
nonce,
|
||
signature: signatureFor(token, timestamp, nonce),
|
||
});
|
||
assert.equal(result.status, 200);
|
||
assert.match(result.body, /上一条如果还没完成/);
|
||
assert.equal(replyCalled, false);
|
||
});
|
||
|
||
test('wechat mp service exposes route status and can recreate route for bound user', async () => {
|
||
const calls = [];
|
||
let routeCleared = false;
|
||
let routeUpserted = false;
|
||
const service = createWechatMpService({
|
||
config: {
|
||
enabled: true,
|
||
appId: 'wx123',
|
||
appSecret: 'secret',
|
||
token: 'token',
|
||
publicBaseUrl: 'https://example.com',
|
||
bindPath: '/auth/wechat/authorize?intent=login',
|
||
ackText: 'ack',
|
||
unsupportedText: 'unsupported',
|
||
unboundTextPrefix: '请先绑定',
|
||
},
|
||
userAuth: {
|
||
async getWechatOpenidForUser() {
|
||
return 'openid-1';
|
||
},
|
||
async getWechatAgentRoute(_appId, _openid) {
|
||
if (routeCleared && !routeUpserted) return null;
|
||
return {
|
||
agentSessionId: routeUpserted ? 'session-new' : 'session-old',
|
||
status: 'active',
|
||
updatedAt: 1710000000000,
|
||
};
|
||
},
|
||
async clearWechatAgentRoute() {
|
||
routeCleared = true;
|
||
calls.push('cleared');
|
||
},
|
||
async canUseChat() {
|
||
return { ok: true };
|
||
},
|
||
async resolveWorkingDir() {
|
||
return '/tmp/user-1';
|
||
},
|
||
async getAgentSessionPolicy() {
|
||
return { enableContextMemory: false, extensionOverrides: [], unrestricted: true };
|
||
},
|
||
async getUserPublishLayout() {
|
||
return { displayName: 'John', username: 'john', slug: 'john', constraints: null };
|
||
},
|
||
async registerAgentSession(_userId, sessionId) {
|
||
calls.push(`register:${sessionId}`);
|
||
},
|
||
async upsertWechatAgentRoute() {
|
||
routeUpserted = true;
|
||
calls.push('upserted');
|
||
},
|
||
},
|
||
apiFetch: async (pathname) => {
|
||
if (pathname === '/agent/start') {
|
||
return new Response(JSON.stringify({ id: 'session-new' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected fallback api path: ${pathname}`);
|
||
},
|
||
sessionApiFetch: async (_sessionId, pathname) => {
|
||
if (pathname === '/sessions/session-new') {
|
||
return new Response(JSON.stringify({ working_dir: '/tmp/user-1' }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (pathname === '/sessions/session-new/extensions') {
|
||
return new Response(JSON.stringify({ extensions: [] }), {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
if (
|
||
pathname === '/agent/restart'
|
||
) {
|
||
return new Response('{}', {
|
||
status: 200,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
throw new Error(`unexpected api path: ${pathname}`);
|
||
},
|
||
});
|
||
|
||
const status = await service.getRouteStatusForUser('user-1');
|
||
assert.deepEqual(status, {
|
||
enabled: true,
|
||
bound: true,
|
||
appId: 'wx123',
|
||
openid: 'openid-1',
|
||
agentSessionId: 'session-old',
|
||
routeStatus: 'active',
|
||
updatedAt: 1710000000000,
|
||
});
|
||
|
||
const recreated = await service.recreateRouteForUser('user-1');
|
||
assert.equal(recreated.ok, true);
|
||
assert.equal(recreated.route.agentSessionId, 'session-new');
|
||
assert.equal(calls.includes('cleared'), true);
|
||
assert.equal(calls.some((item) => item === 'register:session-new'), true);
|
||
assert.equal(calls.includes('upserted'), true);
|
||
});
|