feat(wechat): add image generation delivery policy
Memind CI / Test, build, and release guards (pull_request) Successful in 2m45s

This commit is contained in:
john
2026-07-21 23:21:44 +08:00
parent bfb1f6fea9
commit 28581310da
13 changed files with 1078 additions and 19 deletions
+316 -2
View File
@@ -3,6 +3,7 @@ import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import sharp from 'sharp';
import {
buildWechatAgentPrompt,
buildWechatTextReply,
@@ -62,8 +63,9 @@ function inboundXml({
].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 previewReadyPageHtml({ title = 'Page', subtitle = '测试页面', cover = '' } = {}) {
const coverField = cover ? `,"cover":"${cover}"` : '';
return `<!doctype html><html><head><meta name="description" content="${subtitle}"><meta name="mindspace-cover" content='{"tag":"页面","accent":"#3366cc","accent2":"#112233","subtitle":"${subtitle}"${coverField}}'><title>${title}</title></head><body><main>${'x'.repeat(600)}</main><p data-mindspace-page-tag="platform-brand">TKMind · 智趣</p></body></html>`;
}
function jsonEscapedPreviewReadyPageHtml(options = {}) {
@@ -95,6 +97,7 @@ function createBoundWechatService({
unboundTextPrefix: '请先绑定',
progressDelayMs: 0,
maxImageBytes: 1024 * 1024,
requireFreshPageThumbnail: false,
...config,
},
userAuth: {
@@ -859,6 +862,8 @@ test('loadWechatMpConfig requires full config and enable flag', () => {
});
assert.equal(config.enabled, true);
assert.equal(config.bindPath, '/auth/wechat/authorize?intent=login');
assert.equal(config.requireFreshPageThumbnail, true);
assert.equal(loadWechatMpConfig({ H5_WECHAT_MP_FRESH_PAGE_THUMBNAILS: '0' }).requireFreshPageThumbnail, false);
});
test('verifyWechatMpSignature accepts valid signature', () => {
@@ -4932,3 +4937,312 @@ test('wechat mp service exposes route status and can recreate route for bound us
assert.equal(calls.some((item) => item === 'register:session-new'), true);
assert.equal(calls.includes('upserted'), true);
});
test('wechat mp page delivery requires and verifies a current-run fresh thumbnail', async () => {
const token = 'token';
const timestamp = '1710000000';
const nonce = 'nonce';
const workspaceRoot = fs.mkdtempSync('/tmp/wechat-mp-fresh-thumbnail-');
const htmlPath = path.join(workspaceRoot, 'public', 'fresh.html');
const wechatPayloads = [];
const generated = {
ok: true,
jobId: 'job-fresh-page',
source: { mimeType: 'image/webp', width: 1280, height: 720 },
asset: {
id: 'asset-fresh-page',
htmlSrc: 'images/fresh-page.webp',
publicUrl: 'https://example.com/MindSpace/user-1/public/images/fresh-page.webp',
workspaceRelativePath: 'public/images/fresh-page.webp',
},
};
const pageHtml = previewReadyPageHtml({
title: 'Fresh',
subtitle: '本轮新缩略图',
cover: generated.asset.htmlSrc,
});
const pageImage = await sharp({
create: { width: 64, height: 64, channels: 3, background: '#3366cc' },
}).webp().toBuffer();
const eventFrame = (event) => `data: ${JSON.stringify(event)}\n\n`;
const service = createBoundWechatService({
token,
config: { requireFreshPageThumbnail: true },
userAuth: {
async resolveWorkingDir() {
return workspaceRoot;
},
async getUserPublishLayout() {
return {
publishDir: workspaceRoot,
displayName: 'John',
username: 'john',
slug: 'john',
constraints: null,
};
},
},
sessionApiFetch: async (sessionId, pathname, init = {}) => {
assert.equal(sessionId, 'session-1');
if (pathname === '/sessions/session-1/events') {
return new Response(
[
eventFrame({
type: 'Message',
request_id: 'req-fresh-page',
message: {
id: 'assistant-tools',
role: 'assistant',
metadata: { userVisible: true },
content: [
{
id: 'call-page-skill',
type: 'toolRequest',
toolCall: {
value: { name: 'load_skill', arguments: { name: 'static-page-publish' } },
},
},
{
id: 'call-generate',
type: 'toolRequest',
toolCall: {
value: {
name: 'sandbox-fs__generate_image',
arguments: { purpose: 'hero' },
},
},
},
{
id: 'call-generate',
type: 'toolResponse',
toolResult: {
status: 'success',
value: { content: [{ type: 'text', text: JSON.stringify(generated) }] },
},
},
{
id: 'call-write-page',
type: 'toolRequest',
toolCall: {
value: {
name: 'sandbox-fs__write_file',
arguments: { path: 'public/fresh.html', content: pageHtml },
},
},
},
],
},
}),
eventFrame({
type: 'Message',
request_id: 'req-fresh-page',
message: {
id: 'assistant-final',
role: 'assistant',
metadata: { userVisible: true },
content: [{
type: 'text',
text: '页面已完成:https://example.com/MindSpace/user-1/public/fresh.html',
}],
},
}),
eventFrame({
type: 'Finish',
request_id: 'req-fresh-page',
token_state: { inputTokens: 1, outputTokens: 2 },
}),
].join(''),
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
);
}
if (pathname === '/sessions/session-1/reply') {
const body = JSON.parse(init.body);
assert.equal(body.user_message.metadata.memindRun.channel, 'wechat_mp');
assert.equal(body.user_message.metadata.memindRun.pageThumbnailMode, 'required_fresh');
assert.equal(body.user_message.metadata.memindRun.inlineImageMode, 'auto');
assert.match(body.user_message.content[0].text, /服务号页面新缩略图硬要求/);
assert.match(body.user_message.content[0].text, /本轮新生成资产/);
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
fs.writeFileSync(
htmlPath,
pageHtml,
'utf8',
);
fs.mkdirSync(path.join(workspaceRoot, 'public', 'images'), { recursive: true });
fs.writeFileSync(path.join(workspaceRoot, 'public', generated.asset.htmlSrc), pageImage);
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 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')) {
wechatPayloads.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}`);
},
});
const originalRandomUuid = crypto.randomUUID;
crypto.randomUUID = () => 'req-fresh-page';
try {
const result = await service.handleInboundMessage(
inboundXml({ content: '请生成一个旅行 html 页面,文件名 public/fresh.html' }),
{ timestamp, nonce, signature: signatureFor(token, timestamp, nonce) },
);
assert.equal(result.status, 200);
await result.task;
assert.equal(fs.existsSync(path.join(workspaceRoot, 'public', 'fresh.thumbnail.svg')), true);
} finally {
crypto.randomUUID = originalRandomUuid;
fs.rmSync(workspaceRoot, { recursive: true, force: true });
}
assert.equal(wechatPayloads.length, 1);
assert.equal(wechatPayloads[0].msgtype, 'text');
assert.match(wechatPayloads[0].text.content, /fresh\.html/);
});
test('wechat mp standalone image intent sends a native image message and text', async () => {
const token = 'token';
const timestamp = '1710000000';
const nonce = 'nonce';
const sourceImage = await sharp({
create: { width: 48, height: 48, channels: 3, background: '#cc8844' },
}).webp().toBuffer();
const wechatPayloads = [];
const generated = {
ok: true,
jobId: 'job-chat-image',
source: { mimeType: 'image/webp', width: 1024, height: 1024 },
asset: {
id: 'asset-chat-image',
htmlSrc: 'images/chat-image.webp',
publicUrl: 'https://example.com/MindSpace/user-1/public/images/chat-image.webp',
workspaceRelativePath: 'public/images/chat-image.webp',
},
};
const eventFrame = (event) => `data: ${JSON.stringify(event)}\n\n`;
const service = createBoundWechatService({
token,
sessionApiFetch: async (sessionId, pathname, init = {}) => {
assert.equal(sessionId, 'session-1');
if (pathname === '/sessions/session-1/events') {
return new Response(
[
eventFrame({
type: 'Message',
request_id: 'req-chat-image',
message: {
id: 'assistant-tools',
role: 'assistant',
metadata: { userVisible: true },
content: [
{
id: 'call-image',
type: 'toolRequest',
toolCall: {
value: {
name: 'sandbox-fs__generate_image',
arguments: { purpose: 'inline_image' },
},
},
},
{
id: 'call-image',
type: 'toolResponse',
toolResult: {
status: 'success',
value: { content: [{ type: 'text', text: JSON.stringify(generated) }] },
},
},
],
},
}),
eventFrame({
type: 'Message',
request_id: 'req-chat-image',
message: {
id: 'assistant-final',
role: 'assistant',
metadata: { userVisible: true },
content: [{ type: 'text', text: '新图片已经生成。' }],
},
}),
eventFrame({
type: 'Finish',
request_id: 'req-chat-image',
token_state: { inputTokens: 1, outputTokens: 2 },
}),
].join(''),
{ status: 200, headers: { 'Content-Type': 'text/event-stream' } },
);
}
if (pathname === '/sessions/session-1/reply') {
const body = JSON.parse(init.body);
assert.equal(body.user_message.metadata.memindRun.imageGenerationMode, 'required');
assert.equal(body.user_message.metadata.memindRun.pageThumbnailMode, 'auto');
assert.match(body.user_message.content[0].text, /purpose=`inline_image`/);
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 path: ${pathname}`);
},
wechatFetch: async (url, init = {}) => {
const target = String(url);
if (target.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 (target === generated.asset.publicUrl) {
return new Response(sourceImage, { status: 200, headers: { 'Content-Type': 'image/webp' } });
}
if (target.includes('/cgi-bin/media/upload')) {
assert.ok(init.body instanceof FormData);
return new Response(JSON.stringify({ type: 'image', media_id: 'wx-generated-1' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
if (target.includes('/cgi-bin/message/custom/send')) {
wechatPayloads.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}`);
},
});
const originalRandomUuid = crypto.randomUUID;
crypto.randomUUID = () => 'req-chat-image';
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(wechatPayloads.map((payload) => payload.msgtype), ['image', 'text']);
assert.equal(wechatPayloads[0].image.media_id, 'wx-generated-1');
assert.match(wechatPayloads[1].text.content, /新图片已经生成/);
});