fix(wechat): guard bare page-continuation follow-ups from session loss and memory drift
Memind CI / Test, build, and release guards (push) Failing after 3s

Prevent chat-to-page flows like writing a poem then saying "做成页面吧" from opening
a fresh session without carried content, failing with a vague error, or letting Memory
page-template preferences replace the immediate topic.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-02 18:49:42 +08:00
parent 55100f8945
commit c7dcc5d002
5 changed files with 439 additions and 7 deletions
+128
View File
@@ -0,0 +1,128 @@
/** Guards for WeChat “做成页面” follow-ups that must bind to in-session content. */
const PAGE_SOURCE_REFERENCE_PATTERN =
/(?:这个|刚才|上面|前面|上一条|它|这首|这篇|这段|以下内容|以上内容|刚才生成|上面生成)/iu;
const PAGE_TEMPLATE_MEMORY_PATTERN =
/(?:做成页面|新闻页|新闻页面|固定格式|daily[- ]?news|国际国内热门新闻)/iu;
const ORCHESTRATION_PREFIX_PATTERN =
/^【Memind 任务编排】[\s\S]*?用户任务:\s*/u;
const MIN_SUBSTANTIVE_ASSISTANT_CHARS = 48;
export function hasExplicitPageSourceReference(text) {
return PAGE_SOURCE_REFERENCE_PATTERN.test(String(text ?? '').trim());
}
export function isBareImmediatePageCreateRequest(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
if (!/(?:吧|呀|呢|一下)[。.!\s]*$/u.test(normalized)) return false;
return (
/^(?:(?:请|麻烦|可以|能不能)\s*)?(?:(?:帮我|帮忙|给我)\s*)?(?:做|弄|改|转|写|排版|生成|制作)(?:成|为)?\s*(?:一个|个)?\s*(?:页面|网页)/iu.test(
normalized,
)
&& !hasExplicitPageSourceReference(normalized)
);
}
export function stripOrchestrationFromAssistantText(text) {
return String(text ?? '')
.replace(ORCHESTRATION_PREFIX_PATTERN, '')
.trim();
}
export function extractSubstantiveAssistantText(text) {
const normalized = stripOrchestrationFromAssistantText(text);
if (normalized.length < MIN_SUBSTANTIVE_ASSISTANT_CHARS) return '';
if (/^(?:好的|收到|我先|让我|ok|let me)\b/iu.test(normalized) && normalized.length < 120) {
return '';
}
return normalized;
}
export function filterMemoryForImmediatePageContext(memories) {
if (!Array.isArray(memories) || memories.length === 0) return memories;
return memories.filter((entry) => {
const label = String(entry?.label ?? entry?.type ?? '').trim().toLowerCase();
const body = String(entry?.text ?? entry?.content ?? entry?.body ?? '').trim();
if (label === 'preference' && PAGE_TEMPLATE_MEMORY_PATTERN.test(body)) return false;
return true;
});
}
export function resolveWechatPageContinuationSource({
userText = '',
carriedSessionContent = '',
sessionAssistantContent = '',
} = {}) {
const carried = extractSubstantiveAssistantText(carriedSessionContent);
if (carried) {
return { ok: true, sourceText: carried, source: 'carried_session' };
}
const inSession = extractSubstantiveAssistantText(sessionAssistantContent);
if (inSession) {
return { ok: true, sourceText: inSession, source: 'session_assistant' };
}
const normalized = String(userText ?? '').trim();
if (hasExplicitPageSourceReference(normalized) && normalized.length >= MIN_SUBSTANTIVE_ASSISTANT_CHARS) {
return { ok: true, sourceText: normalized, source: 'inline_user' };
}
return { ok: false, sourceText: '', source: 'missing' };
}
export function shouldFailFastWechatPageContinuation({
sessionPageContinuation = false,
userText = '',
carriedSessionContent = '',
sessionAssistantContent = '',
} = {}) {
if (!sessionPageContinuation) return false;
if (!isBareImmediatePageCreateRequest(userText) && hasExplicitPageSourceReference(userText)) {
return !resolveWechatPageContinuationSource({
userText,
carriedSessionContent,
sessionAssistantContent,
}).ok;
}
if (!isBareImmediatePageCreateRequest(userText)) return false;
return !resolveWechatPageContinuationSource({
userText,
carriedSessionContent,
sessionAssistantContent,
}).ok;
}
export const WECHAT_PAGE_CONTINUATION_MISSING_SOURCE_TEXT =
'我没能确定您要把哪段内容做成页面。请一条消息发完整主题,或直接粘贴要做成页面的正文(例如诗词、文章全文)。';
export function isWechatPageContinuationRepairableError(message) {
const normalized = String(message ?? '').trim();
if (!normalized) return false;
return (
/stale_session_poisoned_completion/i.test(normalized)
|| /wechat_page_fresh_thumbnail_required:/i.test(normalized)
|| /wechat_agent_incomplete_reply/i.test(normalized)
|| /missing_page_artifact/i.test(normalized)
|| /poisoned_or_fake_claim/i.test(normalized)
|| /skill_or_stub/i.test(normalized)
);
}
export function extractLastSubstantiveAssistantFromConversation(messages = []) {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message?.role !== 'assistant') continue;
const text = extractSubstantiveAssistantText(
Array.isArray(message?.content)
? message.content
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
.map((item) => item.text)
.join('')
: String(message?.text ?? ''),
);
if (text) return text;
}
return '';
}
+78
View File
@@ -0,0 +1,78 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
extractLastSubstantiveAssistantFromConversation,
extractSubstantiveAssistantText,
filterMemoryForImmediatePageContext,
hasExplicitPageSourceReference,
isBareImmediatePageCreateRequest,
isWechatPageContinuationRepairableError,
resolveWechatPageContinuationSource,
shouldFailFastWechatPageContinuation,
} from './intent/page-continuation-context.mjs';
test('detects bare vs explicit page continuation phrasing', () => {
assert.equal(hasExplicitPageSourceReference('帮我把这首词做成页面'), true);
assert.equal(hasExplicitPageSourceReference('帮我吧以上内容做成页面'), true);
assert.equal(isBareImmediatePageCreateRequest('帮我做成页面吧'), true);
assert.equal(isBareImmediatePageCreateRequest('帮我把这首词做成页面'), false);
assert.equal(isBareImmediatePageCreateRequest('帮我做个页面'), false);
});
test('resolveWechatPageContinuationSource prefers carried session content', () => {
const poem = '《临江仙·秋思》'.padEnd(60, '词');
const resolved = resolveWechatPageContinuationSource({
userText: '帮我做成页面吧',
carriedSessionContent: poem,
});
assert.equal(resolved.ok, true);
assert.equal(resolved.source, 'carried_session');
});
test('shouldFailFastWechatPageContinuation blocks bare follow-up without source', () => {
assert.equal(
shouldFailFastWechatPageContinuation({
sessionPageContinuation: true,
userText: '帮我做成页面吧',
}),
true,
);
assert.equal(
shouldFailFastWechatPageContinuation({
sessionPageContinuation: true,
userText: '帮我做成页面吧',
sessionAssistantContent: '《临江仙·秋思》'.padEnd(60, '。'),
}),
false,
);
});
test('filterMemoryForImmediatePageContext removes page template preferences', () => {
const filtered = filterMemoryForImmediatePageContext([
{ label: 'preference', text: '用户要求每日新闻页面固定格式' },
{ label: 'fact', text: '用户名字是唐' },
]);
assert.equal(filtered.length, 1);
assert.match(filtered[0].text, /唐/);
});
test('extractLastSubstantiveAssistantFromConversation skips short acknowledgements', () => {
const text = extractLastSubstantiveAssistantFromConversation([
{ role: 'user', content: [{ type: 'text', text: '写首词' }] },
{ role: 'assistant', content: [{ type: 'text', text: '好的,我来写。' }] },
{
role: 'assistant',
content: [{
type: 'text',
text: `《临江仙·秋思》${'昨夜西风凋碧树,独上高楼,望尽天涯路。'.repeat(3)}`,
}],
},
]);
assert.match(text, /临江仙/);
});
test('isWechatPageContinuationRepairableError covers fake delivery markers', () => {
assert.equal(isWechatPageContinuationRepairableError('stale_session_poisoned_completion'), true);
assert.equal(isWechatPageContinuationRepairableError('unknown variant `image_url`'), false);
});
+9
View File
@@ -14,6 +14,7 @@ export function buildPageGenerateAgentPrompt(
wantsDocx = false,
imagePolicy = null,
preferImmediateContext = false,
carriedSessionContent = '',
} = {},
) {
const topic = String(intent?.agentText ?? intent?.displayText ?? '').trim();
@@ -41,6 +42,13 @@ export function buildPageGenerateAgentPrompt(
'',
].join('\n')
: '';
const carriedSessionBlock = String(carriedSessionContent ?? '').trim()
? [
'【待排版正文(来自上一轮会话,必须以此为主题,禁止改用 Memory 或其他任务)】',
String(carriedSessionContent).trim(),
'',
].join('\n')
: '';
const coverExample = imageBlock
? '<本轮 generate_image 返回的 asset.htmlSrc>'
: 'assets/hero.jpg';
@@ -62,6 +70,7 @@ export function buildPageGenerateAgentPrompt(
docxBlock,
imageBlock,
immediateContextBlock,
carriedSessionBlock,
pageDataBlock,
'步骤(必须全部完成):',
pageDataTask