diff --git a/scenarios/wechat-poem-page-modify.json b/scenarios/wechat-poem-page-modify.json new file mode 100644 index 0000000..29e72bc --- /dev/null +++ b/scenarios/wechat-poem-page-modify.json @@ -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 + } + } + } + ] +} diff --git a/scripts/verify-wechat-page-phrasing.mjs b/scripts/verify-wechat-page-phrasing.mjs new file mode 100644 index 0000000..3ff44b9 --- /dev/null +++ b/scripts/verify-wechat-page-phrasing.mjs @@ -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); diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 0f71fae..4edbb04 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -31,6 +31,11 @@ import { isPageDataDevIntent, isPageDataIntent, } from './chat-skills.mjs'; +import { + isWechatImmediateContextPageCreate, + isWechatSessionPageContinuation, + isWechatContentEditFollowup, +} from './wechat/intent/page-continuation.mjs'; import { resolvePageGenerateOutcome } from './wechat/handlers/page-generate.mjs'; import { 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) { - if (wechatIntent?.kind !== 'page.generate') return false; - const normalized = String(text ?? '').trim(); - return Boolean(normalized && WECHAT_IMMEDIATE_CONTEXT_PAGE_PATTERN.test(normalized)); + return isWechatImmediateContextPageCreate(wechatIntent, text); } +export { isWechatSessionPageContinuation }; + export function shouldRotateUnlinkedWechatRoute({ routeUpdatedAt, wechatMessageCount, @@ -1035,10 +1021,12 @@ export function isRecoverableWechatAgentSessionError(message) { } export function shouldDeliverWechatHtmlArtifacts(wechatIntent, intent) { + const text = String(intent?.agentText ?? intent?.displayText ?? '').trim(); return ( wechatIntent?.kind === 'page.generate' - || looksLikeHtmlGenerationIntent(intent?.agentText) - || isPageDataIntent(intent?.agentText) + || looksLikeHtmlGenerationIntent(text) + || isPageDataIntent(text) + || (wechatIntent?.kind === 'chat.general' && isWechatContentEditFollowup(text)) ); } @@ -2443,13 +2431,13 @@ export function createWechatMpService({ intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : ''; const isPageDataRequest = isWechatPageDataTask(resetCandidate); const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent); - const immediateContextFollowup = - isWechatImmediateContextFollowup(wechatIntent, resetCandidate); + const sessionPageContinuation = + isWechatSessionPageContinuation(wechatIntent, resetCandidate); const imagePolicy = resolveWechatImageGenerationPolicy({ text: resetCandidate, isPageGenerate: wechatIntent.kind === 'page.generate', requireFreshPageThumbnail: - config.requireFreshPageThumbnail && !immediateContextFollowup, + config.requireFreshPageThumbnail && !sessionPageContinuation, }); // Page Data delivery owns persistent files, datasets and two publication // policies. Reusing a conversational route here can make a new request @@ -2460,7 +2448,7 @@ export function createWechatMpService({ openid: inbound.fromUserName, userContext: user, forceNew, - retainUnlinkedRoute: immediateContextFollowup, + retainUnlinkedRoute: sessionPageContinuation, }); let sessionId = route.sessionId; await ensureSessionProvider(sessionId); @@ -2501,11 +2489,14 @@ export function createWechatMpService({ ? buildPageGenerateAgentPrompt(intent, { wantsDocx: wechatIntent.wantsDocx, imagePolicy, - preferImmediateContext: immediateContextFollowup, + preferImmediateContext: sessionPageContinuation, }) - : buildWechatAgentPrompt(intent, { imagePolicy }); + : buildWechatAgentPrompt(intent, { + imagePolicy, + preferSessionPageContinuation: sessionPageContinuation, + }); const maxImmediateContextPageAttempts = - wechatIntent.kind === 'page.generate' && immediateContextFollowup ? 2 : 1; + wechatIntent.kind === 'page.generate' && sessionPageContinuation ? 2 : 1; let reply; let publishArtifacts = []; let confirmedArtifacts = []; @@ -2531,7 +2522,7 @@ export function createWechatMpService({ userId: user.userId, sessionId, userMessage, - preserveAgentPrompt: immediateContextFollowup, + preserveAgentPrompt: sessionPageContinuation, }), submitReply: submitSessionReply ? ({ requestId: replyRequestId, userMessage }) => @@ -2773,7 +2764,7 @@ export function createWechatMpService({ sessionId && (isRecoverableWechatAgentSessionError(message) || isWechatAgentApiErrorText(message)); if (mayBeStaleSession) { - if (immediateContextFollowup) { + if (sessionPageContinuation) { await userAuth.clearWechatAgentRoute(config.appId, inbound.fromUserName).catch((clearErr) => { logger.warn?.('WeChat MP contextual follow-up route clear failed:', clearErr); }); @@ -2804,9 +2795,12 @@ export function createWechatMpService({ ? buildPageGenerateAgentPrompt(intent, { wantsDocx: wechatIntent.wantsDocx, imagePolicy, - preferImmediateContext: immediateContextFollowup, + preferImmediateContext: sessionPageContinuation, }) - : buildWechatAgentPrompt(intent, { imagePolicy }); + : buildWechatAgentPrompt(intent, { + imagePolicy, + preferSessionPageContinuation: sessionPageContinuation, + }); const reply = await executeSessionReply( (pathname, init) => fetchForSession(sessionId, pathname, init), sessionId, @@ -2822,7 +2816,7 @@ export function createWechatMpService({ userId: user.userId, sessionId, userMessage, - preserveAgentPrompt: immediateContextFollowup, + preserveAgentPrompt: sessionPageContinuation, }), submitReply: submitSessionReply ? ({ requestId: replyRequestId, userMessage }) => diff --git a/wechat-mp.test.mjs b/wechat-mp.test.mjs index efa9492..af15d85 100644 --- a/wechat-mp.test.mjs +++ b/wechat-mp.test.mjs @@ -17,6 +17,8 @@ import { loadWechatMpConfig, maybeAttachPublishedHtmlLink, isWechatImmediateContextFollowup, + isWechatSessionPageContinuation, + shouldDeliverWechatHtmlArtifacts, isWechatPageDataTask, shouldRotateUnlinkedWechatRoute, shouldRetryHtmlGenerationReply, @@ -29,6 +31,7 @@ import { WECHAT_CUSTOMER_TEXT_MAX_BYTES, } from './wechat-mp.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 { resolveWechatImageGenerationPolicy, WECHAT_PAGE_THUMBNAIL_MODE } from './wechat/image-generation-policy.mjs'; import { @@ -365,6 +368,22 @@ test('WeChat immediate-context page guard does not match complete page requests' 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', () => { const policy = resolveWechatImageGenerationPolicy({ text: '把刚才的诗做成页面', diff --git a/wechat/intent/page-continuation.mjs b/wechat/intent/page-continuation.mjs new file mode 100644 index 0000000..eafe8a9 --- /dev/null +++ b/wechat/intent/page-continuation.mjs @@ -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) + ); +} diff --git a/wechat/intent/patterns.mjs b/wechat/intent/patterns.mjs index 536fb7d..a7843b1 100644 --- a/wechat/intent/patterns.mjs +++ b/wechat/intent/patterns.mjs @@ -1,5 +1,10 @@ /** WeChat channel intent patterns — not shared with H5 chat-skills routing. */ +import { + isWechatPageEditText, + isWechatPageRetryText, +} from './page-continuation.mjs'; + export const PAGE_GENERATE_PATTERN = /(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu; @@ -25,7 +30,11 @@ export function isPageGenerateText(text) { const normalized = String(text ?? '').trim(); if (!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) { diff --git a/wechat/prompts/chat-general.mjs b/wechat/prompts/chat-general.mjs index aebf950..09046f1 100644 --- a/wechat/prompts/chat-general.mjs +++ b/wechat/prompts/chat-general.mjs @@ -8,7 +8,11 @@ import { buildCurrentTimeAgentPrefix } from '../../user-memory-profile.mjs'; import { isPageGenerateText, wantsDocxDownload } from '../intent/patterns.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 agentText = intent?.agentText ?? intent?.content ?? ''; const autoSkillPrefix = @@ -56,6 +60,14 @@ export function buildWechatAgentPrompt(intent, { grantedSkills = [], imagePolicy '', ].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 currentTimeHint = buildCurrentTimeAgentPrefix({ timezone: scheduleTimezone }); const scheduleAssistantHint = shouldUseScheduleAssistant(agentText) @@ -156,6 +168,7 @@ export function buildWechatAgentPrompt(intent, { grantedSkills = [], imagePolicy if (docxDownloadHint) lines.push(docxDownloadHint); if (pageDataCollectHint) lines.push(pageDataCollectHint); if (pageDataRepairHint) lines.push(pageDataRepairHint); + if (sessionPageContinuationHint) lines.push(sessionPageContinuationHint); if (pagePublishHint) lines.push(pagePublishHint); if (scheduleAssistantHint) lines.push(scheduleAssistantHint); if (imageGenerationHint) lines.push(imageGenerationHint); diff --git a/wechat/prompts/page-generate.mjs b/wechat/prompts/page-generate.mjs index 1cc01d7..2ee5e86 100644 --- a/wechat/prompts/page-generate.mjs +++ b/wechat/prompts/page-generate.mjs @@ -31,11 +31,12 @@ export function buildPageGenerateAgentPrompt( }); const immediateContextBlock = preferImmediateContext ? [ - '【即时上下文优先】当前需求是对本会话紧邻内容的续作。', - '“这个/刚才/做成页面”等指代只能解析为当前会话最近一条 assistant 回复中的完整正文(如诗全文、文章全文),禁止用长期记忆、历史偏好或旧任务替换主题。', + '【即时上下文优先】当前需求是对本会话紧邻内容的续作(做成页面、修改页面、重试上次页面或改诗/文章内容)。', + '“这个/刚才/做成页面/修改页面/重试上次页面”等指代只能解析为当前会话最近一条 assistant 回复或最近落盘的 public/*.html,禁止用长期记忆、历史偏好或旧任务替换主题。', '如果当前会话没有可辨识的紧邻内容,必须请用户补充主题,禁止猜测并生成其他页面。', - '【write_file 硬门槛】必须先 `load_skill` → `static-page-publish`,再用 `write_file`/`edit_file` 把紧邻正文完整写入 `public/*.html` 后才能结束。', - '禁止只调用 load_skill、todo_write 或 generate_image 就回复;没有 write_file 落盘 HTML 视为未完成。', + '【write_file 硬门槛】必须先 `load_skill` → `static-page-publish`,再用 `write_file`/`edit_file` 写入或更新 `public/*.html` 后才能结束。', + '修改已有页面时必须 edit_file 更新同一 HTML 或明确的新文件,禁止只回复文字说明。', + '禁止只调用 load_skill、todo_write 或 generate_image 就回复;没有 write_file/edit_file 落盘 HTML 视为未完成。', '缩略图/配图可选:优先完成 HTML 落盘与 mindspace-cover;没有 hero 图时 cover 可用 CSS 渐变或 omit,不要卡在 generate_image。', '', ].join('\n') diff --git a/wechat/wechat-page-continuation.test.mjs b/wechat/wechat-page-continuation.test.mjs new file mode 100644 index 0000000..5bdef02 --- /dev/null +++ b/wechat/wechat-page-continuation.test.mjs @@ -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); +});