fix(wechat): guard ambiguous memory recall and scheduled task execution

Skip memory injection for short filler messages, demote page-workflow memory
pollution, keep scheduled automation off page.generate, and fail scheduled
tasks that only return clarification instead of deliverables.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-01 22:49:52 +08:00
parent dcf83e9b6f
commit 26fe5912a3
9 changed files with 222 additions and 0 deletions
+46
View File
@@ -208,6 +208,40 @@ export function isMemoryRecallQuestion(text) {
return MEMORY_RECALL_PATTERNS.some((pattern) => pattern.test(normalized));
}
const PAGE_WORKFLOW_QUERY_PATTERNS = [
/(?:生成|做成|整理|排版|导出|发布).{0,8}(?:页面|网页|html|专题|报告页|推文)/iu,
/(?:mindspace|MindSpace)/u,
/每日新闻/u,
];
const PAGE_WORKFLOW_MEMORY_PATTERNS = [
/(?:做成|生成|整理|排版).{0,8}(?:页面|网页|html|专题|报告)/iu,
/每日新闻/u,
/(?:mindspace|MindSpace)/u,
/页面.{0,8}(?:偏好|习惯|模板)/u,
];
export function queryImpliesPageWorkflow(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
return PAGE_WORKFLOW_QUERY_PATTERNS.some((pattern) => pattern.test(normalized));
}
function isPageWorkflowMemory(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
return PAGE_WORKFLOW_MEMORY_PATTERNS.some((pattern) => pattern.test(normalized));
}
export function shouldSkipAmbiguousMemoryQuery(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return true;
if (isMemoryRecallQuestion(normalized)) return false;
if (pgvectorMemoryBackendInternals.extractKeywordTerms(normalized).length > 0) return false;
// Short acknowledgements/fillers without a topic anchor (e.g. 「可以」「先不聊了」).
return normalized.length <= 4;
}
function memoryItemText(item) {
return String(item?.text ?? item?.memory_text ?? item?.content ?? '').trim();
}
@@ -229,6 +263,13 @@ export function scoreAgentMemoryCandidate(item, query) {
}
let score = (lexical * 100) + keywordBoost;
if (isEpisodicMemoryItem(item) && score > 0) score += 5;
if (
isPageWorkflowMemory(text)
&& !queryImpliesPageWorkflow(query)
&& !isMemoryRecallQuestion(query)
) {
score -= 100;
}
return score;
}
@@ -1366,6 +1407,11 @@ export function createChatIntentRouter(options = {}) {
agentMemoryMetrics.lastReason = 'intervention_skip';
return { ...base, reason: 'intervention_skip' };
}
if (shouldSkipAmbiguousMemoryQuery(text)) {
agentMemoryMetrics.skipped += 1;
agentMemoryMetrics.lastReason = 'ambiguous_query_skip';
return { ...base, reason: 'ambiguous_query_skip' };
}
const startedAt = Date.now();
try {
let personalFailed = false;
+61
View File
@@ -31,6 +31,7 @@ import {
resolveChatIntentRouterPolicy,
mergeAgentMemoryCandidates,
scoreAgentMemoryCandidate,
shouldSkipAmbiguousMemoryQuery,
} from './chat-intent-router.mjs';
/** Ambiguous user text that should miss FAQ/rules and exercise LLM router paths in tests. */
@@ -1398,6 +1399,66 @@ test('scoreAgentMemoryCandidate boosts episodic matches when topic overlap exist
assert.ok(episodicScore > personalScore);
});
test('shouldSkipAmbiguousMemoryQuery skips filler without topic anchor', () => {
assert.equal(shouldSkipAmbiguousMemoryQuery('可以'), true);
assert.equal(shouldSkipAmbiguousMemoryQuery('我们继续聊聊德川家康'), false);
assert.equal(shouldSkipAmbiguousMemoryQuery('9岁小孩教育'), false);
assert.equal(shouldSkipAmbiguousMemoryQuery('你记得我们聊过什么吗'), false);
});
test('agent memory skips resolve for ambiguous short filler', async () => {
const calls = [];
const router = createChatIntentRouter({
env: {
MEMORY_AGENT_RESOLVE_ENABLED: '1',
MEMORY_AGENT_INJECTION_MODE: 'active',
},
memoryV2: {
async resolve(input) {
calls.push(input);
return { memories: [{ label: 'preference', text: '每日新闻页面偏好' }] };
},
},
});
const result = await router.resolveAgentMemoryContext({
userId: 'user-tang',
text: '可以',
});
assert.equal(result.skipped, true);
assert.equal(result.reason, 'ambiguous_query_skip');
assert.equal(result.memories.length, 0);
assert.equal(calls.length, 0);
});
test('agent memory merge demotes page workflow memories when query is general chat', () => {
const merged = mergeAgentMemoryCandidates({
query: '9岁小孩教育,孩子玩手机超时怎么办',
limit: 2,
personalMemories: [
{ id: 'personal:news', label: 'preference', text: '用户希望整理每日新闻并做成页面发布' },
{ id: 'personal:kid', label: 'experience', text: '用户关心9岁孩子教育与手机使用边界' },
],
});
assert.equal(merged[0].id, 'personal:kid');
});
test('scoreAgentMemoryCandidate penalizes page workflow memories on general chat', () => {
const pageScore = scoreAgentMemoryCandidate({
id: 'personal:news',
label: 'preference',
text: '用户希望整理每日新闻并做成页面发布',
}, '可以,我们继续聊孩子教育');
const topicScore = scoreAgentMemoryCandidate({
id: 'personal:kid',
label: 'experience',
text: '用户关心9岁孩子教育与手机使用边界',
}, '可以,我们继续聊孩子教育');
assert.ok(topicScore > pageScore);
});
test('active agent memory context is hidden from displayText but available to orchestration envelope', () => {
const enriched = applyAgentOrchestrationToUserMessage(
{
+27
View File
@@ -30,6 +30,8 @@ export function buildScheduledTaskExecutionPrompt(task, {
`任务要求:${task.taskSpec}`,
'执行约束:',
'- 这是系统自动触发的定时任务,请直接完成可交付结果,不要反问用户。',
'- 禁止向用户追问时间、频率或任务内容;taskSpec 已是完整执行说明。',
'- 禁止调用 scheduled_task_create / scheduled_task_list / scheduled_task_cancel;只执行 taskSpec。',
'- 若需要生成页面,必须先 load_skill → static-page-publish,再 write_file 到 public/*.html,并给出正式可访问 URL。',
'- 若只需摘要/文本,给出完整中文结果摘要。',
'- 完成后在回复中明确写出交付结果(链接或摘要)。',
@@ -69,6 +71,31 @@ export function formatScheduledTaskDeliveryMessage(task, deliveryText) {
return `${header}\n\n${body}`.trim();
}
const SCHEDULED_TASK_CLARIFICATION_PATTERNS = [
/需确认/u,
/请确认/u,
/请问/u,
/未指定/u,
/需要澄清/u,
/在创建前需要确认/u,
/信息不完整/u,
/具体几点/u,
/缺(?:少|失)/u,
];
export function looksLikeScheduledTaskNonDelivery(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return true;
if (SCHEDULED_TASK_CLARIFICATION_PATTERNS.some((pattern) => pattern.test(normalized))) {
return true;
}
if (/https?:\/\//i.test(normalized)) return false;
if (/public\/[^\s]+\.html/i.test(normalized)) return false;
if (/页面链接/u.test(normalized)) return false;
if (/(?:已生成|已完成|交付).{0,24}(?:页面|链接|结果)/u.test(normalized)) return false;
return normalized.length < 40;
}
export async function finalizeScheduledTaskPageDelivery({
pool,
userId,
+13
View File
@@ -4,6 +4,7 @@ import {
buildScheduledTaskExecutionPrompt,
extractScheduledTaskDeliveryText,
formatScheduledTaskDeliveryMessage,
looksLikeScheduledTaskNonDelivery,
} from './scheduled-task-executor.mjs';
test('buildScheduledTaskExecutionPrompt includes task spec and automation marker', () => {
@@ -19,6 +20,18 @@ test('buildScheduledTaskExecutionPrompt includes task spec and automation marker
assert.match(text, /每日新闻页/);
assert.match(text, /搜索今日新闻并生成 HTML 页面/);
assert.match(text, /Scheduled Automation/);
assert.match(text, /禁止调用 scheduled_task_create/);
});
test('looksLikeScheduledTaskNonDelivery detects clarification replies', () => {
assert.equal(
looksLikeScheduledTaskNonDelivery('请问 7月31日具体几点执行这个任务?'),
true,
);
assert.equal(
looksLikeScheduledTaskNonDelivery('页面已生成:https://example.com/news.html'),
false,
);
});
test('extractScheduledTaskDeliveryText reads last assistant message', () => {
+6
View File
@@ -1,6 +1,7 @@
import {
executeScheduledTask,
formatScheduledTaskDeliveryMessage,
looksLikeScheduledTaskNonDelivery,
} from './scheduled-task-executor.mjs';
export function startScheduledTaskWorker({
@@ -77,6 +78,11 @@ export function startScheduledTaskWorker({
timeoutMs: executionTimeoutMs,
logger,
});
if (looksLikeScheduledTaskNonDelivery(result.deliveryText)) {
const err = new Error('定时任务未产出可交付结果');
err.code = 'SCHEDULED_TASK_NON_DELIVERY';
throw err;
}
await deliverTaskResult(task, result.deliveryText);
await scheduledTaskService.markTaskSucceeded(task, {
result: {
+52
View File
@@ -123,3 +123,55 @@ test('scheduled task worker marks failure after execution error', async () => {
assert.deepEqual(calls, ['failed:3:agent timeout', 'notify:scheduled_task_failed']);
});
test('scheduled task worker marks failure when agent only asks for clarification', async () => {
const calls = [];
const task = {
id: 'task-3',
userId: 'user-3',
title: '每日诗意清晨',
recurrence: 'daily',
notifyChannel: 'web',
attempts: 1,
};
const worker = startScheduledTaskWorker({
intervalMs: 60_000,
userAuth: { id: 'user-auth' },
tkmindProxy: { id: 'proxy' },
scheduledTaskService: {
async listDueTasks() {
return [task];
},
async lockTask() {
return task;
},
async markTaskRunning(input) {
return input;
},
async markTaskSucceeded() {
calls.push('success');
},
async markTaskFailed(input, err) {
calls.push(`failed:${err.code}`);
return { ...input, status: 'failed', lastError: err.message };
},
},
scheduleService: {
async createUserNotification(input) {
calls.push(`notify:${input.notificationType}`);
},
},
executeTask: async () => ({
sessionId: 'session-3',
requestId: 'req-3',
deliveryText: '请问每天早上几点执行?',
}),
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(calls, ['failed:SCHEDULED_TASK_NON_DELIVERY', 'notify:scheduled_task_failed']);
});
+6
View File
@@ -3525,6 +3525,12 @@ export function createWechatMpService({
wechatScheduleLlmConfigService,
llmProviderService,
logger,
}).catch((err) => {
logger.warn?.(
'WeChat MP schedule intent handling failed open:',
err instanceof Error ? err.message : err,
);
return null;
})
: null;
if (scheduleReply) {
+2
View File
@@ -4,6 +4,7 @@ import {
isWechatPageEditText,
isWechatPageRetryText,
} from './page-continuation.mjs';
import { shouldUseScheduledTaskAutomation } from '../../scheduled-task-intent.mjs';
export const PAGE_GENERATE_PATTERN =
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu;
@@ -29,6 +30,7 @@ export const CONNECTIVITY_TEST_PATTERN = /^(测试\s*\d*|test\s*\d*)[!!。.\s]
export function isPageGenerateText(text) {
const normalized = String(text ?? '').trim();
if (!normalized) return false;
if (shouldUseScheduledTaskAutomation(normalized)) return false;
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
return (
PAGE_GENERATE_PATTERN.test(normalized)
+9
View File
@@ -33,6 +33,15 @@ test('classifyWechatIntent detects page.generate', () => {
assert.equal(isPageGenerateIntent({ msgType: 'text', agentText: intent.topic }), true);
});
test('classifyWechatIntent keeps scheduled automation on chat.general', () => {
const intent = classifyWechatIntent({
msgType: 'text',
agentText: '我想创建一个定时执行任务,今晚 21:45 分执行做新闻页面',
});
assert.equal(intent.kind, 'chat.general');
assert.equal(isPageGenerateIntent({ msgType: 'text', agentText: intent.text }), false);
});
test('classifyWechatIntent detects session.reset', () => {
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换话题' }).kind, 'session.reset');
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换新会话' }).kind, 'session.reset');