Separate WeChat chat, page refine, and task flows with explicit routing.
Memind CI / Test, build, and release guards (push) Successful in 4m40s
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:
@@ -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}`,
|
||||
}));
|
||||
@@ -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
@@ -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 || '',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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('帮我做嵊泗攻略'), '帮我做嵊泗攻略');
|
||||
});
|
||||
@@ -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') {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user