fix: broaden WeChat session page continuation phrasing
Memind CI / Test, build, and release guards (push) Successful in 2m37s
Memind CI / Test, build, and release guards (push) Successful in 2m37s
Recognize retry, HTML page, page edits, and poem content edits as session continuations so delivery skips fresh-thumbnail forcing and retains agent context for multi-turn page work. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"id": "wechat-poem-page-modify",
|
||||||
|
"name": "写诗→做页面→修改页面(H5 多轮)",
|
||||||
|
"description": "模拟服务号常见三回合:写诗、做成页面、修改页面内容",
|
||||||
|
"account": {
|
||||||
|
"username": "john",
|
||||||
|
"password": "888888"
|
||||||
|
},
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"action": "login",
|
||||||
|
"label": "登录"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "chat",
|
||||||
|
"label": "请求写诗",
|
||||||
|
"message": "帮我写一首关于七月午后的短诗,四段即可",
|
||||||
|
"expect": {
|
||||||
|
"assistantMinChars": 40,
|
||||||
|
"timeoutMs": 180000,
|
||||||
|
"replyKeywords": ["七月", "午"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "chat",
|
||||||
|
"label": "做成精美 HTML 页面",
|
||||||
|
"message": "生成一个精美的 HTML 页面",
|
||||||
|
"expect": {
|
||||||
|
"assistantMinChars": 20,
|
||||||
|
"timeoutMs": 300000,
|
||||||
|
"replyKeywords": ["七月", "午"],
|
||||||
|
"page": {
|
||||||
|
"keywords": ["七月", "午"],
|
||||||
|
"requirePublicLink": true,
|
||||||
|
"requireHttp200": true,
|
||||||
|
"requireMindspaceCover": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "chat",
|
||||||
|
"label": "修改页面标题",
|
||||||
|
"message": "把页面标题改成《七月的午后》",
|
||||||
|
"expect": {
|
||||||
|
"assistantMinChars": 10,
|
||||||
|
"timeoutMs": 300000,
|
||||||
|
"replyKeywords": ["七月"],
|
||||||
|
"page": {
|
||||||
|
"keywords": ["七月的午后", "七月"],
|
||||||
|
"requirePublicLink": true,
|
||||||
|
"requireHttp200": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Matrix test for WeChat page phrasing: intent, session continuation, delivery policy.
|
||||||
|
* Run: node scripts/verify-wechat-page-phrasing.mjs
|
||||||
|
*/
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { classifyWechatIntent } from '../wechat/intent/classifier.mjs';
|
||||||
|
import {
|
||||||
|
isWechatSessionPageContinuation,
|
||||||
|
isWechatImmediateContextPageCreate,
|
||||||
|
} from '../wechat/intent/page-continuation.mjs';
|
||||||
|
import { resolveWechatImageGenerationPolicy, WECHAT_PAGE_THUMBNAIL_MODE } from '../wechat/image-generation-policy.mjs';
|
||||||
|
import {
|
||||||
|
isWechatImmediateContextFollowup,
|
||||||
|
shouldDeliverWechatHtmlArtifacts,
|
||||||
|
} from '../wechat-mp.mjs';
|
||||||
|
import { buildPageGenerateAgentPrompt } from '../wechat/prompts/page-generate.mjs';
|
||||||
|
import { buildWechatAgentPrompt } from '../wechat/prompts/chat-general.mjs';
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
label: '完整单条页面需求',
|
||||||
|
text: '帮我做一个上海旅游攻略页面',
|
||||||
|
expect: {
|
||||||
|
kind: 'page.generate',
|
||||||
|
continuation: false,
|
||||||
|
freshThumbnail: true,
|
||||||
|
deliverHtml: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '写诗(普通聊天)',
|
||||||
|
text: '帮我写一首诗吧',
|
||||||
|
expect: {
|
||||||
|
kind: 'chat.general',
|
||||||
|
continuation: false,
|
||||||
|
freshThumbnail: false,
|
||||||
|
deliverHtml: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '短指代做成页面',
|
||||||
|
text: '把刚才的诗做成页面',
|
||||||
|
expect: {
|
||||||
|
kind: 'page.generate',
|
||||||
|
continuation: true,
|
||||||
|
freshThumbnail: false,
|
||||||
|
deliverHtml: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '精美 HTML 页面',
|
||||||
|
text: '生成一个精美的 HTML 页面',
|
||||||
|
expect: {
|
||||||
|
kind: 'page.generate',
|
||||||
|
continuation: true,
|
||||||
|
freshThumbnail: false,
|
||||||
|
deliverHtml: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '重试上次页面',
|
||||||
|
text: '重试上次页面',
|
||||||
|
expect: {
|
||||||
|
kind: 'page.generate',
|
||||||
|
continuation: true,
|
||||||
|
freshThumbnail: false,
|
||||||
|
deliverHtml: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '修改刚才页面样式',
|
||||||
|
text: '修改刚才生成的页面,背景改成浅蓝色',
|
||||||
|
expect: {
|
||||||
|
kind: 'page.generate',
|
||||||
|
continuation: true,
|
||||||
|
freshThumbnail: false,
|
||||||
|
deliverHtml: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '改页面标题',
|
||||||
|
text: '把页面标题改成《七月清晨》',
|
||||||
|
expect: {
|
||||||
|
kind: 'page.generate',
|
||||||
|
continuation: true,
|
||||||
|
freshThumbnail: false,
|
||||||
|
deliverHtml: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '改诗内容(会话续作)',
|
||||||
|
text: '把诗里第三段改长一点',
|
||||||
|
expect: {
|
||||||
|
kind: 'chat.general',
|
||||||
|
continuation: true,
|
||||||
|
freshThumbnail: false,
|
||||||
|
deliverHtml: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const item of cases) {
|
||||||
|
const intent = { msgType: 'text', agentText: item.text, displayText: item.text };
|
||||||
|
const wechatIntent = classifyWechatIntent(intent);
|
||||||
|
const continuation = isWechatSessionPageContinuation(wechatIntent, item.text);
|
||||||
|
const policy = resolveWechatImageGenerationPolicy({
|
||||||
|
text: item.text,
|
||||||
|
isPageGenerate: wechatIntent.kind === 'page.generate',
|
||||||
|
requireFreshPageThumbnail: continuation ? false : true,
|
||||||
|
});
|
||||||
|
const deliverHtml = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent);
|
||||||
|
const errors = [];
|
||||||
|
if (wechatIntent.kind !== item.expect.kind) {
|
||||||
|
errors.push(`kind expected ${item.expect.kind}, got ${wechatIntent.kind}`);
|
||||||
|
}
|
||||||
|
if (continuation !== item.expect.continuation) {
|
||||||
|
errors.push(`continuation expected ${item.expect.continuation}, got ${continuation}`);
|
||||||
|
}
|
||||||
|
const freshRequired = policy.pageThumbnailMode === WECHAT_PAGE_THUMBNAIL_MODE.REQUIRED_FRESH;
|
||||||
|
if (freshRequired !== item.expect.freshThumbnail) {
|
||||||
|
errors.push(`freshThumbnail expected ${item.expect.freshThumbnail}, got ${freshRequired}`);
|
||||||
|
}
|
||||||
|
if (deliverHtml !== item.expect.deliverHtml) {
|
||||||
|
errors.push(`deliverHtml expected ${item.expect.deliverHtml}, got ${deliverHtml}`);
|
||||||
|
}
|
||||||
|
if (item.expect.continuation && wechatIntent.kind === 'page.generate') {
|
||||||
|
const prompt = buildPageGenerateAgentPrompt(intent, {
|
||||||
|
preferImmediateContext: continuation,
|
||||||
|
imagePolicy: policy,
|
||||||
|
});
|
||||||
|
if (!/即时上下文优先/.test(prompt)) errors.push('page prompt missing immediate context block');
|
||||||
|
if (!/write_file 硬门槛/.test(prompt)) errors.push('page prompt missing write_file gate');
|
||||||
|
}
|
||||||
|
if (item.expect.continuation && wechatIntent.kind === 'chat.general') {
|
||||||
|
const prompt = buildWechatAgentPrompt(intent, { preferSessionPageContinuation: continuation });
|
||||||
|
if (!/会话页面续作/.test(prompt)) errors.push('chat prompt missing session continuation block');
|
||||||
|
}
|
||||||
|
if (errors.length) {
|
||||||
|
failed += 1;
|
||||||
|
console.error(`✘ ${item.label}: ${errors.join('; ')}`);
|
||||||
|
} else {
|
||||||
|
passed += 1;
|
||||||
|
console.log(`✔ ${item.label}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(isWechatImmediateContextFollowup({ kind: 'page.generate' }, '把刚才的诗做成页面'), true);
|
||||||
|
assert.equal(isWechatImmediateContextPageCreate({ kind: 'page.generate' }, '生成一个精美的 HTML 页面'), true);
|
||||||
|
|
||||||
|
console.log(`\n=== 汇总: ${passed}/${cases.length} 话术矩阵通过 ===`);
|
||||||
|
if (failed > 0) process.exit(1);
|
||||||
+30
-36
@@ -31,6 +31,11 @@ import {
|
|||||||
isPageDataDevIntent,
|
isPageDataDevIntent,
|
||||||
isPageDataIntent,
|
isPageDataIntent,
|
||||||
} from './chat-skills.mjs';
|
} from './chat-skills.mjs';
|
||||||
|
import {
|
||||||
|
isWechatImmediateContextPageCreate,
|
||||||
|
isWechatSessionPageContinuation,
|
||||||
|
isWechatContentEditFollowup,
|
||||||
|
} from './wechat/intent/page-continuation.mjs';
|
||||||
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
|
import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs';
|
||||||
import {
|
import {
|
||||||
buildWechatImageRunMetadata,
|
buildWechatImageRunMetadata,
|
||||||
@@ -975,31 +980,12 @@ export function shouldForceNewWechatAgentSession(wechatIntent, resetCandidate) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const WECHAT_IMMEDIATE_CONTEXT_PAGE_DECORATORS =
|
|
||||||
'(?:精美(?:的)?|漂亮(?:的)?|好看(?:的)?|简约(?:的)?|优雅(?:的)?)';
|
|
||||||
const WECHAT_IMMEDIATE_CONTEXT_PAGE_PATTERN =
|
|
||||||
new RegExp(
|
|
||||||
'^'
|
|
||||||
+ '(?:(?:请|麻烦|可以|能不能)\\s*)?'
|
|
||||||
+ '(?:(?:帮我|帮忙|给我)\\s*)?'
|
|
||||||
+ '(?:(?:把\\s*)?(?:这个|刚才(?:的)?(?:诗|文章|内容)?|上面(?:的)?|前面(?:的)?|上一条|它|这首(?:诗)?|这篇(?:文章)?|这段(?:内容)?)\\s*)?'
|
|
||||||
+ '(?:继续\\s*)?'
|
|
||||||
+ '(?:做|弄|改|转|写|排版|生成|制作)(?:成|为)?\\s*'
|
|
||||||
+ '(?:一个|个)?'
|
|
||||||
+ '(?:(?:\\s*'
|
|
||||||
+ WECHAT_IMMEDIATE_CONTEXT_PAGE_DECORATORS
|
|
||||||
+ '){1,3}\\s*(?:h5|html)|(?:\\s*(?:h5|html)))?'
|
|
||||||
+ '\\s*(?:页面|网页)(?:吧|呀|呢|一下)?[。.!!\\s]*'
|
|
||||||
+ '$',
|
|
||||||
'iu',
|
|
||||||
);
|
|
||||||
|
|
||||||
export function isWechatImmediateContextFollowup(wechatIntent, text) {
|
export function isWechatImmediateContextFollowup(wechatIntent, text) {
|
||||||
if (wechatIntent?.kind !== 'page.generate') return false;
|
return isWechatImmediateContextPageCreate(wechatIntent, text);
|
||||||
const normalized = String(text ?? '').trim();
|
|
||||||
return Boolean(normalized && WECHAT_IMMEDIATE_CONTEXT_PAGE_PATTERN.test(normalized));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { isWechatSessionPageContinuation };
|
||||||
|
|
||||||
export function shouldRotateUnlinkedWechatRoute({
|
export function shouldRotateUnlinkedWechatRoute({
|
||||||
routeUpdatedAt,
|
routeUpdatedAt,
|
||||||
wechatMessageCount,
|
wechatMessageCount,
|
||||||
@@ -1035,10 +1021,12 @@ export function isRecoverableWechatAgentSessionError(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function shouldDeliverWechatHtmlArtifacts(wechatIntent, intent) {
|
export function shouldDeliverWechatHtmlArtifacts(wechatIntent, intent) {
|
||||||
|
const text = String(intent?.agentText ?? intent?.displayText ?? '').trim();
|
||||||
return (
|
return (
|
||||||
wechatIntent?.kind === 'page.generate'
|
wechatIntent?.kind === 'page.generate'
|
||||||
|| looksLikeHtmlGenerationIntent(intent?.agentText)
|
|| looksLikeHtmlGenerationIntent(text)
|
||||||
|| isPageDataIntent(intent?.agentText)
|
|| isPageDataIntent(text)
|
||||||
|
|| (wechatIntent?.kind === 'chat.general' && isWechatContentEditFollowup(text))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2443,13 +2431,13 @@ export function createWechatMpService({
|
|||||||
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
||||||
const isPageDataRequest = isWechatPageDataTask(resetCandidate);
|
const isPageDataRequest = isWechatPageDataTask(resetCandidate);
|
||||||
const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent);
|
const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent);
|
||||||
const immediateContextFollowup =
|
const sessionPageContinuation =
|
||||||
isWechatImmediateContextFollowup(wechatIntent, resetCandidate);
|
isWechatSessionPageContinuation(wechatIntent, resetCandidate);
|
||||||
const imagePolicy = resolveWechatImageGenerationPolicy({
|
const imagePolicy = resolveWechatImageGenerationPolicy({
|
||||||
text: resetCandidate,
|
text: resetCandidate,
|
||||||
isPageGenerate: wechatIntent.kind === 'page.generate',
|
isPageGenerate: wechatIntent.kind === 'page.generate',
|
||||||
requireFreshPageThumbnail:
|
requireFreshPageThumbnail:
|
||||||
config.requireFreshPageThumbnail && !immediateContextFollowup,
|
config.requireFreshPageThumbnail && !sessionPageContinuation,
|
||||||
});
|
});
|
||||||
// Page Data delivery owns persistent files, datasets and two publication
|
// Page Data delivery owns persistent files, datasets and two publication
|
||||||
// policies. Reusing a conversational route here can make a new request
|
// policies. Reusing a conversational route here can make a new request
|
||||||
@@ -2460,7 +2448,7 @@ export function createWechatMpService({
|
|||||||
openid: inbound.fromUserName,
|
openid: inbound.fromUserName,
|
||||||
userContext: user,
|
userContext: user,
|
||||||
forceNew,
|
forceNew,
|
||||||
retainUnlinkedRoute: immediateContextFollowup,
|
retainUnlinkedRoute: sessionPageContinuation,
|
||||||
});
|
});
|
||||||
let sessionId = route.sessionId;
|
let sessionId = route.sessionId;
|
||||||
await ensureSessionProvider(sessionId);
|
await ensureSessionProvider(sessionId);
|
||||||
@@ -2501,11 +2489,14 @@ export function createWechatMpService({
|
|||||||
? buildPageGenerateAgentPrompt(intent, {
|
? buildPageGenerateAgentPrompt(intent, {
|
||||||
wantsDocx: wechatIntent.wantsDocx,
|
wantsDocx: wechatIntent.wantsDocx,
|
||||||
imagePolicy,
|
imagePolicy,
|
||||||
preferImmediateContext: immediateContextFollowup,
|
preferImmediateContext: sessionPageContinuation,
|
||||||
})
|
})
|
||||||
: buildWechatAgentPrompt(intent, { imagePolicy });
|
: buildWechatAgentPrompt(intent, {
|
||||||
|
imagePolicy,
|
||||||
|
preferSessionPageContinuation: sessionPageContinuation,
|
||||||
|
});
|
||||||
const maxImmediateContextPageAttempts =
|
const maxImmediateContextPageAttempts =
|
||||||
wechatIntent.kind === 'page.generate' && immediateContextFollowup ? 2 : 1;
|
wechatIntent.kind === 'page.generate' && sessionPageContinuation ? 2 : 1;
|
||||||
let reply;
|
let reply;
|
||||||
let publishArtifacts = [];
|
let publishArtifacts = [];
|
||||||
let confirmedArtifacts = [];
|
let confirmedArtifacts = [];
|
||||||
@@ -2531,7 +2522,7 @@ export function createWechatMpService({
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
sessionId,
|
sessionId,
|
||||||
userMessage,
|
userMessage,
|
||||||
preserveAgentPrompt: immediateContextFollowup,
|
preserveAgentPrompt: sessionPageContinuation,
|
||||||
}),
|
}),
|
||||||
submitReply: submitSessionReply
|
submitReply: submitSessionReply
|
||||||
? ({ requestId: replyRequestId, userMessage }) =>
|
? ({ requestId: replyRequestId, userMessage }) =>
|
||||||
@@ -2773,7 +2764,7 @@ export function createWechatMpService({
|
|||||||
sessionId
|
sessionId
|
||||||
&& (isRecoverableWechatAgentSessionError(message) || isWechatAgentApiErrorText(message));
|
&& (isRecoverableWechatAgentSessionError(message) || isWechatAgentApiErrorText(message));
|
||||||
if (mayBeStaleSession) {
|
if (mayBeStaleSession) {
|
||||||
if (immediateContextFollowup) {
|
if (sessionPageContinuation) {
|
||||||
await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => {
|
await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => {
|
||||||
logger.warn?.('WeChat MP contextual follow-up route clear failed:', clearErr);
|
logger.warn?.('WeChat MP contextual follow-up route clear failed:', clearErr);
|
||||||
});
|
});
|
||||||
@@ -2804,9 +2795,12 @@ export function createWechatMpService({
|
|||||||
? buildPageGenerateAgentPrompt(intent, {
|
? buildPageGenerateAgentPrompt(intent, {
|
||||||
wantsDocx: wechatIntent.wantsDocx,
|
wantsDocx: wechatIntent.wantsDocx,
|
||||||
imagePolicy,
|
imagePolicy,
|
||||||
preferImmediateContext: immediateContextFollowup,
|
preferImmediateContext: sessionPageContinuation,
|
||||||
})
|
})
|
||||||
: buildWechatAgentPrompt(intent, { imagePolicy });
|
: buildWechatAgentPrompt(intent, {
|
||||||
|
imagePolicy,
|
||||||
|
preferSessionPageContinuation: sessionPageContinuation,
|
||||||
|
});
|
||||||
const reply = await executeSessionReply(
|
const reply = await executeSessionReply(
|
||||||
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
(pathname, init) => fetchForSession(sessionId, pathname, init),
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -2822,7 +2816,7 @@ export function createWechatMpService({
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
sessionId,
|
sessionId,
|
||||||
userMessage,
|
userMessage,
|
||||||
preserveAgentPrompt: immediateContextFollowup,
|
preserveAgentPrompt: sessionPageContinuation,
|
||||||
}),
|
}),
|
||||||
submitReply: submitSessionReply
|
submitReply: submitSessionReply
|
||||||
? ({ requestId: replyRequestId, userMessage }) =>
|
? ({ requestId: replyRequestId, userMessage }) =>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
loadWechatMpConfig,
|
loadWechatMpConfig,
|
||||||
maybeAttachPublishedHtmlLink,
|
maybeAttachPublishedHtmlLink,
|
||||||
isWechatImmediateContextFollowup,
|
isWechatImmediateContextFollowup,
|
||||||
|
isWechatSessionPageContinuation,
|
||||||
|
shouldDeliverWechatHtmlArtifacts,
|
||||||
isWechatPageDataTask,
|
isWechatPageDataTask,
|
||||||
shouldRotateUnlinkedWechatRoute,
|
shouldRotateUnlinkedWechatRoute,
|
||||||
shouldRetryHtmlGenerationReply,
|
shouldRetryHtmlGenerationReply,
|
||||||
@@ -29,6 +31,7 @@ import {
|
|||||||
WECHAT_CUSTOMER_TEXT_MAX_BYTES,
|
WECHAT_CUSTOMER_TEXT_MAX_BYTES,
|
||||||
} from './wechat-mp.mjs';
|
} from './wechat-mp.mjs';
|
||||||
import { createChatIntentRouter } from './chat-intent-router.mjs';
|
import { createChatIntentRouter } from './chat-intent-router.mjs';
|
||||||
|
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
|
||||||
import { buildPageGenerateAgentPrompt, buildImmediateContextPageRepairPrompt } from './wechat/prompts/page-generate.mjs';
|
import { buildPageGenerateAgentPrompt, buildImmediateContextPageRepairPrompt } from './wechat/prompts/page-generate.mjs';
|
||||||
import { resolveWechatImageGenerationPolicy, WECHAT_PAGE_THUMBNAIL_MODE } from './wechat/image-generation-policy.mjs';
|
import { resolveWechatImageGenerationPolicy, WECHAT_PAGE_THUMBNAIL_MODE } from './wechat/image-generation-policy.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -365,6 +368,22 @@ test('WeChat immediate-context page guard does not match complete page requests'
|
|||||||
assert.doesNotMatch(ordinaryPrompt, /write_file 硬门槛/);
|
assert.doesNotMatch(ordinaryPrompt, /write_file 硬门槛/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('WeChat session page continuation covers retry, edit, and poem edits', () => {
|
||||||
|
const retry = classifyWechatIntent({ msgType: 'text', agentText: '重试上次页面' });
|
||||||
|
assert.equal(retry.kind, 'page.generate');
|
||||||
|
assert.equal(isWechatSessionPageContinuation(retry, '重试上次页面'), true);
|
||||||
|
const edit = classifyWechatIntent({ msgType: 'text', agentText: '把页面标题改成《七月清晨》' });
|
||||||
|
assert.equal(edit.kind, 'page.generate');
|
||||||
|
assert.equal(isWechatSessionPageContinuation(edit, edit.topic), true);
|
||||||
|
const poemEdit = classifyWechatIntent({ msgType: 'text', agentText: '把诗里第三段改长一点' });
|
||||||
|
assert.equal(poemEdit.kind, 'chat.general');
|
||||||
|
assert.equal(isWechatSessionPageContinuation(poemEdit, '把诗里第三段改长一点'), true);
|
||||||
|
assert.equal(
|
||||||
|
shouldDeliverWechatHtmlArtifacts(poemEdit, { agentText: '把诗里第三段改长一点' }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('WeChat immediate-context page skips fresh thumbnail requirement', () => {
|
test('WeChat immediate-context page skips fresh thumbnail requirement', () => {
|
||||||
const policy = resolveWechatImageGenerationPolicy({
|
const policy = resolveWechatImageGenerationPolicy({
|
||||||
text: '把刚才的诗做成页面',
|
text: '把刚才的诗做成页面',
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/** WeChat short follow-ups that continue an in-session page or poem without a full new topic. */
|
||||||
|
|
||||||
|
const PAGE_DECORATORS =
|
||||||
|
'(?:精美(?:的)?|漂亮(?:的)?|好看(?:的)?|简约(?:的)?|优雅(?:的)?)';
|
||||||
|
|
||||||
|
export const PAGE_RETRY_PATTERN =
|
||||||
|
/^(?:(?:请|麻烦|可以|能不能)\s*)?(?:(?:帮我|帮忙|给我)\s*)?重试(?:上次|上一个)?页面[。.!!\s]*$/iu;
|
||||||
|
|
||||||
|
export const PAGE_EDIT_PATTERN =
|
||||||
|
/^(?:(?:请|麻烦|可以|能不能)\s*)?(?:(?:帮我|帮忙|给我)\s*)?(?:(?:把|将)\s*)?(?:(?:这个|刚才(?:的)?|上面(?:的)?|前面(?:的)?|上一条|它|刚才(?:生成|做|弄)(?:的)?|上面(?:生成|做|弄)(?:的)?)\s*)?(?:的)?\s*(?:页面|网页|html)?\s*(?:的)?\s*(?:内容|标题|背景|样式|排版|封面|页脚|字体|颜色)?\s*(?:改|修改|调整|更新|换|优化|美化)/iu;
|
||||||
|
|
||||||
|
export const PAGE_IMMEDIATE_CREATE_PATTERN =
|
||||||
|
new RegExp(
|
||||||
|
'^'
|
||||||
|
+ '(?:(?:请|麻烦|可以|能不能)\\s*)?'
|
||||||
|
+ '(?:(?:帮我|帮忙|给我)\\s*)?'
|
||||||
|
+ '(?:(?:把\\s*)?(?:这个|刚才(?:的)?(?:诗|文章|内容)?|上面(?:的)?|前面(?:的)?|上一条|它|这首(?:诗)?|这篇(?:文章)?|这段(?:内容)?)\\s*)?'
|
||||||
|
+ '(?:继续\\s*)?'
|
||||||
|
+ '(?:做|弄|改|转|写|排版|生成|制作)(?:成|为)?\\s*'
|
||||||
|
+ '(?:一个|个)?'
|
||||||
|
+ '(?:(?:\\s*'
|
||||||
|
+ PAGE_DECORATORS
|
||||||
|
+ '){1,3}\\s*(?:h5|html)|(?:\\s*(?:h5|html)))?'
|
||||||
|
+ '\\s*(?:页面|网页)(?:吧|呀|呢|一下)?[。.!!\\s]*'
|
||||||
|
+ '$',
|
||||||
|
'iu',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PAGE_CONTENT_EDIT_PATTERN =
|
||||||
|
/^(?:(?:请|麻烦|可以|能不能)\s*)?(?:(?:帮我|帮忙|给我)\s*)?(?:(?:把|将)\s*)?(?:(?:这个|刚才(?:的)?|上面(?:的)?|前面(?:的)?|它|这首(?:诗)?|这篇(?:文章)?|这段(?:内容)?|诗(?:里|中)?)\s*)?(?:的)?\s*(?:[\p{L}\p{N}]{0,12}\s*)?(?:改|修改|调整|更新|换|加长|缩短|润色)/iu;
|
||||||
|
|
||||||
|
export function isWechatPageRetryText(text) {
|
||||||
|
const normalized = String(text ?? '').trim();
|
||||||
|
return Boolean(normalized && PAGE_RETRY_PATTERN.test(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWechatPageEditText(text) {
|
||||||
|
const normalized = String(text ?? '').trim();
|
||||||
|
return Boolean(normalized && PAGE_EDIT_PATTERN.test(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWechatContentEditFollowup(text) {
|
||||||
|
const normalized = String(text ?? '').trim();
|
||||||
|
return Boolean(normalized && PAGE_CONTENT_EDIT_PATTERN.test(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWechatImmediateContextPageCreate(wechatIntent, text) {
|
||||||
|
if (wechatIntent?.kind !== 'page.generate') return false;
|
||||||
|
const normalized = String(text ?? '').trim();
|
||||||
|
return Boolean(normalized && PAGE_IMMEDIATE_CREATE_PATTERN.test(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWechatSessionPageContinuation(wechatIntent, text) {
|
||||||
|
const normalized = String(text ?? '').trim();
|
||||||
|
if (!normalized) return false;
|
||||||
|
if (isWechatPageRetryText(normalized)) return true;
|
||||||
|
if (isWechatImmediateContextPageCreate(wechatIntent, normalized)) return true;
|
||||||
|
if (wechatIntent?.kind === 'page.generate' && isWechatPageEditText(normalized)) return true;
|
||||||
|
if (wechatIntent?.kind === 'chat.general' && isWechatContentEditFollowup(normalized)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectsWechatHtmlArtifactDelivery(wechatIntent, text) {
|
||||||
|
const normalized = String(text ?? '').trim();
|
||||||
|
return (
|
||||||
|
wechatIntent?.kind === 'page.generate'
|
||||||
|
|| isWechatSessionPageContinuation(wechatIntent, normalized)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
/** WeChat channel intent patterns — not shared with H5 chat-skills routing. */
|
/** WeChat channel intent patterns — not shared with H5 chat-skills routing. */
|
||||||
|
|
||||||
|
import {
|
||||||
|
isWechatPageEditText,
|
||||||
|
isWechatPageRetryText,
|
||||||
|
} from './page-continuation.mjs';
|
||||||
|
|
||||||
export const PAGE_GENERATE_PATTERN =
|
export const PAGE_GENERATE_PATTERN =
|
||||||
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu;
|
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu;
|
||||||
|
|
||||||
@@ -25,7 +30,11 @@ export function isPageGenerateText(text) {
|
|||||||
const normalized = String(text ?? '').trim();
|
const normalized = String(text ?? '').trim();
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
|
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
|
||||||
return PAGE_GENERATE_PATTERN.test(normalized);
|
return (
|
||||||
|
PAGE_GENERATE_PATTERN.test(normalized)
|
||||||
|
|| isWechatPageRetryText(normalized)
|
||||||
|
|| isWechatPageEditText(normalized)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTopicResetText(text) {
|
export function isTopicResetText(text) {
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { buildCurrentTimeAgentPrefix } from '../../user-memory-profile.mjs';
|
|||||||
import { isPageGenerateText, wantsDocxDownload } from '../intent/patterns.mjs';
|
import { isPageGenerateText, wantsDocxDownload } from '../intent/patterns.mjs';
|
||||||
import { buildWechatStandaloneImageInstruction } from '../image-generation-policy.mjs';
|
import { buildWechatStandaloneImageInstruction } from '../image-generation-policy.mjs';
|
||||||
|
|
||||||
export function buildWechatAgentPrompt(intent, { grantedSkills = [], imagePolicy = null } = {}) {
|
export function buildWechatAgentPrompt(intent, {
|
||||||
|
grantedSkills = [],
|
||||||
|
imagePolicy = null,
|
||||||
|
preferSessionPageContinuation = false,
|
||||||
|
} = {}) {
|
||||||
const msgType = String(intent?.msgType ?? 'text');
|
const msgType = String(intent?.msgType ?? 'text');
|
||||||
const agentText = intent?.agentText ?? intent?.content ?? '';
|
const agentText = intent?.agentText ?? intent?.content ?? '';
|
||||||
const autoSkillPrefix =
|
const autoSkillPrefix =
|
||||||
@@ -56,6 +60,14 @@ export function buildWechatAgentPrompt(intent, { grantedSkills = [], imagePolicy
|
|||||||
'',
|
'',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
: '';
|
: '';
|
||||||
|
const sessionPageContinuationHint = preferSessionPageContinuation
|
||||||
|
? [
|
||||||
|
'【会话页面续作】用户在修改本会话刚生成的页面或诗/文章内容。',
|
||||||
|
'必须读取当前会话最近 assistant 回复或已落盘的 public/*.html,用 edit_file 更新目标 HTML。',
|
||||||
|
'禁止切换到无关主题;没有 edit_file/write_file 落盘就不要声称已修改或发送链接。',
|
||||||
|
'',
|
||||||
|
].join('\n')
|
||||||
|
: '';
|
||||||
const scheduleTimezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai';
|
const scheduleTimezone = process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai';
|
||||||
const currentTimeHint = buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone });
|
const currentTimeHint = buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone });
|
||||||
const scheduleAssistantHint = shouldUseScheduleAssistant(agentText)
|
const scheduleAssistantHint = shouldUseScheduleAssistant(agentText)
|
||||||
@@ -156,6 +168,7 @@ export function buildWechatAgentPrompt(intent, { grantedSkills = [], imagePolicy
|
|||||||
if (docxDownloadHint) lines.push(docxDownloadHint);
|
if (docxDownloadHint) lines.push(docxDownloadHint);
|
||||||
if (pageDataCollectHint) lines.push(pageDataCollectHint);
|
if (pageDataCollectHint) lines.push(pageDataCollectHint);
|
||||||
if (pageDataRepairHint) lines.push(pageDataRepairHint);
|
if (pageDataRepairHint) lines.push(pageDataRepairHint);
|
||||||
|
if (sessionPageContinuationHint) lines.push(sessionPageContinuationHint);
|
||||||
if (pagePublishHint) lines.push(pagePublishHint);
|
if (pagePublishHint) lines.push(pagePublishHint);
|
||||||
if (scheduleAssistantHint) lines.push(scheduleAssistantHint);
|
if (scheduleAssistantHint) lines.push(scheduleAssistantHint);
|
||||||
if (imageGenerationHint) lines.push(imageGenerationHint);
|
if (imageGenerationHint) lines.push(imageGenerationHint);
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ export function buildPageGenerateAgentPrompt(
|
|||||||
});
|
});
|
||||||
const immediateContextBlock = preferImmediateContext
|
const immediateContextBlock = preferImmediateContext
|
||||||
? [
|
? [
|
||||||
'【即时上下文优先】当前需求是对本会话紧邻内容的续作。',
|
'【即时上下文优先】当前需求是对本会话紧邻内容的续作(做成页面、修改页面、重试上次页面或改诗/文章内容)。',
|
||||||
'“这个/刚才/做成页面”等指代只能解析为当前会话最近一条 assistant 回复中的完整正文(如诗全文、文章全文),禁止用长期记忆、历史偏好或旧任务替换主题。',
|
'“这个/刚才/做成页面/修改页面/重试上次页面”等指代只能解析为当前会话最近一条 assistant 回复或最近落盘的 public/*.html,禁止用长期记忆、历史偏好或旧任务替换主题。',
|
||||||
'如果当前会话没有可辨识的紧邻内容,必须请用户补充主题,禁止猜测并生成其他页面。',
|
'如果当前会话没有可辨识的紧邻内容,必须请用户补充主题,禁止猜测并生成其他页面。',
|
||||||
'【write_file 硬门槛】必须先 `load_skill` → `static-page-publish`,再用 `write_file`/`edit_file` 把紧邻正文完整写入 `public/*.html` 后才能结束。',
|
'【write_file 硬门槛】必须先 `load_skill` → `static-page-publish`,再用 `write_file`/`edit_file` 写入或更新 `public/*.html` 后才能结束。',
|
||||||
'禁止只调用 load_skill、todo_write 或 generate_image 就回复;没有 write_file 落盘 HTML 视为未完成。',
|
'修改已有页面时必须 edit_file 更新同一 HTML 或明确的新文件,禁止只回复文字说明。',
|
||||||
|
'禁止只调用 load_skill、todo_write 或 generate_image 就回复;没有 write_file/edit_file 落盘 HTML 视为未完成。',
|
||||||
'缩略图/配图可选:优先完成 HTML 落盘与 mindspace-cover;没有 hero 图时 cover 可用 CSS 渐变或 omit,不要卡在 generate_image。',
|
'缩略图/配图可选:优先完成 HTML 落盘与 mindspace-cover;没有 hero 图时 cover 可用 CSS 渐变或 omit,不要卡在 generate_image。',
|
||||||
'',
|
'',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { classifyWechatIntent } from './intent/classifier.mjs';
|
||||||
|
import {
|
||||||
|
isWechatSessionPageContinuation,
|
||||||
|
isWechatPageRetryText,
|
||||||
|
isWechatPageEditText,
|
||||||
|
isWechatContentEditFollowup,
|
||||||
|
} from './intent/page-continuation.mjs';
|
||||||
|
|
||||||
|
test('WeChat page continuation detects retry and edit phrasings', () => {
|
||||||
|
assert.equal(isWechatPageRetryText('重试上次页面'), true);
|
||||||
|
assert.equal(isWechatPageEditText('修改刚才生成的页面,背景改成浅蓝色'), true);
|
||||||
|
assert.equal(isWechatContentEditFollowup('把诗里第三段改长一点'), true);
|
||||||
|
|
||||||
|
const retryIntent = classifyWechatIntent({ msgType: 'text', agentText: '重试上次页面' });
|
||||||
|
assert.equal(retryIntent.kind, 'page.generate');
|
||||||
|
assert.equal(isWechatSessionPageContinuation(retryIntent, '重试上次页面'), true);
|
||||||
|
|
||||||
|
const editIntent = classifyWechatIntent({
|
||||||
|
msgType: 'text',
|
||||||
|
agentText: '把页面标题改成《七月清晨》',
|
||||||
|
});
|
||||||
|
assert.equal(editIntent.kind, 'page.generate');
|
||||||
|
assert.equal(isWechatSessionPageContinuation(editIntent, editIntent.topic), true);
|
||||||
|
|
||||||
|
const poemEdit = classifyWechatIntent({ msgType: 'text', agentText: '把诗里第三段改长一点' });
|
||||||
|
assert.equal(poemEdit.kind, 'chat.general');
|
||||||
|
assert.equal(isWechatSessionPageContinuation(poemEdit, '把诗里第三段改长一点'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('WeChat page continuation rejects full new-topic page requests', () => {
|
||||||
|
const intent = classifyWechatIntent({
|
||||||
|
msgType: 'text',
|
||||||
|
agentText: '帮我做一个上海旅游攻略页面',
|
||||||
|
});
|
||||||
|
assert.equal(intent.kind, 'page.generate');
|
||||||
|
assert.equal(isWechatSessionPageContinuation(intent, intent.topic), false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user