Separate WeChat chat, page refine, and task flows with explicit routing.
Memind CI / Test, build, and release guards (push) Successful in 4m40s

Add channel prefixes, pin edit targets in Cursor prompts, and force fresh sessions for page refine URLs so DeepSeek/Goose chat history does not pollute page tasks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-31 14:10:02 +08:00
parent b80584696d
commit 8f88eec2bd
10 changed files with 308 additions and 13 deletions
@@ -0,0 +1,17 @@
#!/usr/bin/env node
/** Print Tang copy-paste WeChat page-refine message. */
import { buildWechatPageRefineUserMessageTemplate } from '../wechat/page-refine-target.mjs';
const TANG_USER_ID = 'a70ff537-8908-486e-9b6c-042e07cc25db';
const PAGE = 'public/shengsi-3day-guide.html';
const BASE = 'https://m.tkmind.cn';
console.log(buildWechatPageRefineUserMessageTemplate({
publicHtmlPath: PAGE,
requirements: [
'按 Day1 / Day2 / Day3 列出具体时间节点(出发、交通、景点、用餐、住宿)',
'在原有内容基础上补充,不要删掉已有有效信息',
'直接 edit 原文件,不要新建页面',
].join(''),
publicUrl: `${BASE}/MindSpace/${TANG_USER_ID}/${PAGE}`,
}));
+12
View File
@@ -13,6 +13,18 @@ import {
defaultsFromEnv,
} from './wechat-intent-router-config.mjs';
import { isPageGenerateText } from './wechat/intent/patterns.mjs';
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
test('classifyWechatIntent honors channel prefix for chat vs page', () => {
assert.equal(
classifyWechatIntent({ msgType: 'text', agentText: '【聊天】帮我做一个游玩攻略页面' }).kind,
'chat.general',
);
assert.equal(
classifyWechatIntent({ msgType: 'text', agentText: '【改页】public/demo.html 更详细' }).kind,
'page.generate',
);
});
test('isPageGenerateText matches travel guide requests without explicit 页面', () => {
assert.equal(
+18 -4
View File
@@ -46,11 +46,16 @@ import {
resolveSyncReply,
shouldHandleSyncReply,
} from './wechat/handlers/sync-replies.mjs';
import { stripWechatChannelPrefix, WECHAT_CHANNEL_PREFIX } from './wechat/intent/channel-prefix.mjs';
import { classifyWechatIntent } from './wechat/intent/classifier.mjs';
import {
isWechatSessionResetAction,
resolveWechatSessionAction,
} from './wechat/intent/session-action.mjs';
import {
extractMindSpacePublicHtmlPath,
isWechatPageRefineRequestText,
} from './wechat/page-refine-target.mjs';
import { isPageGenerateText, isTopicResetText } from './wechat/intent/patterns.mjs';
import {
isPageDataDevIntent,
@@ -1078,12 +1083,21 @@ function isTopicResetIntent(text) {
}
export function shouldForceNewWechatAgentSession(wechatIntent, resetCandidate) {
return (
if (
wechatIntent?.kind === 'session.reset' ||
isWechatSessionResetAction(wechatIntent?.sessionActionResult) ||
isTopicResetIntent(resetCandidate) ||
isPageDataIntent(resetCandidate)
);
) {
return true;
}
if (wechatIntent?.kind !== 'page.generate') return false;
const text = String(resetCandidate ?? wechatIntent?.topic ?? '').trim();
if (isWechatSessionPageContinuation(wechatIntent, text)) return false;
if (wechatIntent?.channelPrefix === WECHAT_CHANNEL_PREFIX.PAGE) return true;
if (extractMindSpacePublicHtmlPath(text)) return true;
if (isWechatPageRefineRequestText(text)) return true;
return false;
}
export function isWechatImmediateContextFollowup(wechatIntent, text) {
@@ -1384,7 +1398,7 @@ function normalizeWechatInboundIntent(inbound) {
return {
...base,
displayText: content,
agentText: content,
agentText: stripWechatChannelPrefix(content),
};
}
@@ -1393,7 +1407,7 @@ function normalizeWechatInboundIntent(inbound) {
return {
...base,
displayText: recognition ? `语音:${recognition}` : '语音消息',
agentText: recognition,
agentText: stripWechatChannelPrefix(recognition),
media: {
mediaId: inbound?.mediaId || '',
format: inbound?.format || '',
+21
View File
@@ -343,6 +343,27 @@ test('Page Data requests always rotate away from an existing WeChat route', () =
true,
);
assert.equal(shouldForceNewWechatAgentSession({ kind: 'chat.general' }, '继续聊德川家康'), false);
assert.equal(
shouldForceNewWechatAgentSession(
{ kind: 'page.generate', channelPrefix: 'page' },
'public/shengsi-3day-guide.html 更详细',
),
true,
);
assert.equal(
shouldForceNewWechatAgentSession(
{ kind: 'page.generate' },
'https://m.tkmind.cn/MindSpace/u/public/demo.html 页面更详细',
),
true,
);
assert.equal(
shouldForceNewWechatAgentSession(
{ kind: 'page.generate' },
'把这个做成页面',
),
false,
);
assert.equal(shouldForceNewWechatAgentSession({ kind: 'session.reset' }, '继续聊德川家康'), true);
assert.equal(shouldForceNewWechatAgentSession({ kind: 'chat.general' }, '换新会话'), true);
});
+49
View File
@@ -0,0 +1,49 @@
/** Explicit WeChat channel prefixes so users can separate chat, page, and scheduled tasks. */
export const WECHAT_CHANNEL_PREFIX = Object.freeze({
CHAT: 'chat',
PAGE: 'page',
SCHEDULED_TASK: 'scheduled_task',
});
const PREFIX_ENTRIES = Object.freeze([
{
channel: WECHAT_CHANNEL_PREFIX.CHAT,
pattern: /^【?\s*聊天\s*】?[:\s]*/iu,
},
{
channel: WECHAT_CHANNEL_PREFIX.PAGE,
pattern: /^【?\s*(?:改页|做页|页面)\s*】?[:\s]*/iu,
},
{
channel: WECHAT_CHANNEL_PREFIX.SCHEDULED_TASK,
pattern: /^【?\s*定时任务\s*】?[:\s]*/iu,
},
]);
/**
* @returns {{ channel: string|null, body: string, hadPrefix: boolean }}
*/
export function parseWechatChannelPrefix(text) {
const normalized = String(text ?? '').trim();
if (!normalized) {
return { channel: null, body: '', hadPrefix: false };
}
for (const entry of PREFIX_ENTRIES) {
if (!entry.pattern.test(normalized)) continue;
return {
channel: entry.channel,
body: normalized.replace(entry.pattern, '').trim(),
hadPrefix: true,
};
}
return { channel: null, body: normalized, hadPrefix: false };
}
export function stripWechatChannelPrefix(text) {
return parseWechatChannelPrefix(text).body;
}
export function hasWechatPageChannelPrefix(text) {
return parseWechatChannelPrefix(text).channel === WECHAT_CHANNEL_PREFIX.PAGE;
}
+29
View File
@@ -0,0 +1,29 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
parseWechatChannelPrefix,
stripWechatChannelPrefix,
WECHAT_CHANNEL_PREFIX,
} from './channel-prefix.mjs';
test('parseWechatChannelPrefix strips chat prefix', () => {
const parsed = parseWechatChannelPrefix('【聊天】上海明天天气怎么样');
assert.equal(parsed.channel, WECHAT_CHANNEL_PREFIX.CHAT);
assert.equal(parsed.body, '上海明天天气怎么样');
});
test('parseWechatChannelPrefix strips page refine prefix', () => {
const parsed = parseWechatChannelPrefix('【改页】public/demo.html 补充时间节点');
assert.equal(parsed.channel, WECHAT_CHANNEL_PREFIX.PAGE);
assert.match(parsed.body, /public\/demo\.html/);
});
test('parseWechatChannelPrefix strips scheduled task prefix', () => {
const parsed = parseWechatChannelPrefix('【定时任务】列出我进行中的任务');
assert.equal(parsed.channel, WECHAT_CHANNEL_PREFIX.SCHEDULED_TASK);
assert.equal(parsed.body, '列出我进行中的任务');
});
test('stripWechatChannelPrefix leaves unprefixed text unchanged', () => {
assert.equal(stripWechatChannelPrefix('帮我做嵊泗攻略'), '帮我做嵊泗攻略');
});
+33 -9
View File
@@ -1,3 +1,4 @@
import { parseWechatChannelPrefix, WECHAT_CHANNEL_PREFIX } from './channel-prefix.mjs';
import {
CONNECTIVITY_TEST_PATTERN,
GREETING_PATTERN,
@@ -43,27 +44,50 @@ import { WECHAT_SESSION_ACTION } from './session-action.mjs';
* @param {{ msgType?: string, agentText?: string, displayText?: string }} intent
* @returns {WechatIntent}
*/
function withChannelMeta(result, channelPrefix) {
if (!channelPrefix) return result;
return { ...result, channelPrefix };
}
export function classifyWechatIntent(intent) {
const msgType = String(intent?.msgType ?? 'text').toLowerCase();
const text = String(intent?.agentText ?? intent?.displayText ?? '').trim();
const rawText = String(intent?.agentText ?? intent?.displayText ?? '').trim();
const { channel: channelPrefix, body: text } = parseWechatChannelPrefix(rawText);
if (msgType === 'text' || msgType === 'voice') {
if (isTopicResetText(text)) {
return { kind: 'session.reset', sessionAction: WECHAT_SESSION_ACTION.RESET };
return withChannelMeta(
{ kind: 'session.reset', sessionAction: WECHAT_SESSION_ACTION.RESET },
channelPrefix,
);
}
if (STATUS_PROBE_PATTERN.test(text)) {
return withChannelMeta({ kind: 'status.probe' }, channelPrefix);
}
if (GREETING_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) {
return withChannelMeta({ kind: 'greeting' }, channelPrefix);
}
if (STATUS_PROBE_PATTERN.test(text)) return { kind: 'status.probe' };
if (GREETING_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) return { kind: 'greeting' };
if (CONNECTIVITY_TEST_PATTERN.test(text.replace(/[!!。.\s]+$/g, ''))) {
return { kind: 'connectivity.test' };
return withChannelMeta({ kind: 'connectivity.test' }, channelPrefix);
}
if (isPageGenerateText(text)) {
return {
if (channelPrefix === WECHAT_CHANNEL_PREFIX.CHAT) {
return withChannelMeta({ kind: 'chat.general', text }, channelPrefix);
}
if (channelPrefix === WECHAT_CHANNEL_PREFIX.PAGE) {
return withChannelMeta({
kind: 'page.generate',
topic: text,
wantsDocx: wantsDocxDownload(text),
};
}, channelPrefix);
}
return { kind: 'chat.general', text };
if (isPageGenerateText(text)) {
return withChannelMeta({
kind: 'page.generate',
topic: text,
wantsDocx: wantsDocxDownload(text),
}, channelPrefix);
}
return withChannelMeta({ kind: 'chat.general', text }, channelPrefix);
}
if (msgType === 'event') {
+69
View File
@@ -0,0 +1,69 @@
import {
PAGE_MINDSPACE_PUBLIC_URL_PATTERN,
PAGE_REFINE_DETAIL_PATTERN,
} from './intent/patterns.mjs';
/** MindSpace public HTML path inside a full URL (userId or username slug). */
export const MINDSPACE_PUBLIC_HTML_PATH_PATTERN =
/(?:https?:\/\/[^\s/]+)?\/?mindspace\/[^/\s"'<>]+?\/(public\/[\w.-]+\.html)/iu;
export function extractMindSpacePublicHtmlPath(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return null;
const match = normalized.match(MINDSPACE_PUBLIC_HTML_PATH_PATTERN);
const relativePath = match?.[1]?.replace(/\\/g, '/').trim();
return relativePath || null;
}
export function isWechatPageRefineRequestText(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
if (PAGE_REFINE_DETAIL_PATTERN.test(normalized)) return true;
return (
PAGE_MINDSPACE_PUBLIC_URL_PATTERN.test(normalized)
&& /(?:页面|网页|更新|详细|改|完善|补充)/iu.test(normalized)
);
}
/**
* Extra Cursor/Goose page prompt block: pin edit target file when URL or refine intent is present.
*/
export function buildPageRefineTargetPromptBlock(text) {
const normalized = String(text ?? '').trim();
if (!normalized || !isWechatPageRefineRequestText(normalized)) return '';
const relativePath = extractMindSpacePublicHtmlPath(normalized);
if (relativePath) {
return [
'【改页目标文件(从用户消息解析,必须优先改此文件)】',
`- 相对路径:${relativePath}`,
'- 用 edit_file 更新该 HTML;禁止新建无关页面或只回复文字说明。',
'- 按用户要求补充/修改内容,保留其余仍有效的结构与样式。',
'',
].join('\n');
}
return [
'【改页任务】用户要求细化或更新已有页面。',
'- 在工作区查找最相关的 public/*.html,用 edit_file 更新原文件,不要新建无关页面。',
'- 禁止只回复文字说明而不落盘。',
'',
].join('\n');
}
/** Copy-paste WeChat message template for page refine (e.g. Tang). */
export function buildWechatPageRefineUserMessageTemplate({
publicHtmlPath = 'public/your-page.html',
requirements = '请把页面改更详细(示例:行程细化到每个时间节点)',
publicUrl = '',
} = {}) {
const pathLine = String(publicHtmlPath ?? '').trim();
const reqLine = String(requirements ?? '').trim();
const urlLine = String(publicUrl ?? '').trim();
return [
'改页任务(请直接改原文件,不要新建页面)',
pathLine ? `文件:${pathLine}` : '',
reqLine ? `要求:${reqLine}` : '',
urlLine ? `链接:${urlLine}` : '',
].filter(Boolean).join('\n');
}
+57
View File
@@ -0,0 +1,57 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { buildPageGenerateAgentPrompt } from './prompts/page-generate.mjs';
import {
buildPageRefineTargetPromptBlock,
buildWechatPageRefineUserMessageTemplate,
extractMindSpacePublicHtmlPath,
isWechatPageRefineRequestText,
} from './page-refine-target.mjs';
test('extractMindSpacePublicHtmlPath parses MindSpace public URLs', () => {
assert.equal(
extractMindSpacePublicHtmlPath(
'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/shengsi-3day-guide.html请改详细',
),
'public/shengsi-3day-guide.html',
);
assert.equal(extractMindSpacePublicHtmlPath('public/demo.html'), null);
});
test('buildPageRefineTargetPromptBlock pins relative path for URL refine', () => {
const block = buildPageRefineTargetPromptBlock(
'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/shengsi-3day-guide.html 页面更详细到时间节点',
);
assert.match(block, /public\/shengsi-3day-guide\.html/);
assert.match(block, /edit_file/);
});
test('buildPageRefineTargetPromptBlock covers refine without URL', () => {
const block = buildPageRefineTargetPromptBlock('页面还不够详细,必须详细到时间节点');
assert.match(block, /改页任务/);
assert.doesNotMatch(block, /相对路径:public\//);
});
test('buildPageGenerateAgentPrompt includes page refine target block', () => {
const prompt = buildPageGenerateAgentPrompt({
agentText: 'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/shengsi-3day-guide.html 把页面更新更详细',
});
assert.match(prompt, /改页目标文件/);
assert.match(prompt, /public\/shengsi-3day-guide\.html/);
});
test('buildWechatPageRefineUserMessageTemplate formats copy-paste message', () => {
const text = buildWechatPageRefineUserMessageTemplate({
publicHtmlPath: 'public/shengsi-3day-guide.html',
requirements: '按 Day1/Day2/Day3 细化到具体时间点',
publicUrl: 'https://m.tkmind.cn/MindSpace/u/public/shengsi-3day-guide.html',
});
assert.match(text, /public\/shengsi-3day-guide\.html/);
assert.match(text, /Day1/);
assert.match(text, /不要新建页面/);
});
test('isWechatPageRefineRequestText detects refine intents', () => {
assert.equal(isWechatPageRefineRequestText('页面还不够详细'), true);
assert.equal(isWechatPageRefineRequestText('帮我推荐跑步路线'), false);
});
+3
View File
@@ -4,6 +4,7 @@ import {
isPageDataIntent,
} from '../../chat-skills.mjs';
import { buildWechatPageImageInstruction } from '../image-generation-policy.mjs';
import { buildPageRefineTargetPromptBlock } from '../page-refine-target.mjs';
/**
* Service-account-only page generation prompt. Does not use H5 buildAutoChatSkillPrefix.
@@ -52,6 +53,7 @@ export function buildPageGenerateAgentPrompt(
const coverExample = imageBlock
? '<本轮 generate_image 返回的 asset.htmlSrc>'
: 'assets/hero.jpg';
const pageRefineBlock = buildPageRefineTargetPromptBlock(topic);
const pageDataBlock = pageDataTask
? [
'【Page Data 强制要求】这不是静态展示页。',
@@ -72,6 +74,7 @@ export function buildPageGenerateAgentPrompt(
imageBlock,
immediateContextBlock,
carriedSessionBlock,
pageRefineBlock,
pageDataBlock,
'步骤(必须全部完成):',
pageDataTask