From b9f5720ce0ad12a09aea151f2eed5c7e4ce528c0 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 2 Aug 2026 06:46:33 +0800 Subject: [PATCH] fix(wechat): recognize news page intents and harden scheduled task delivery Expand page.generate rules for post-page verbs and link-missing retries, recover HTML artifacts when shadow LLM agrees, and stop marking scheduled tasks failed when WeChat notification hits rate limits. Co-authored-by: Cursor --- scheduled-task-worker.mjs | 6 ++- scheduled-task-worker.test.mjs | 59 +++++++++++++++++++++++++++++ wechat-mp.mjs | 10 ++++- wechat/intent/page-continuation.mjs | 14 ++++++- wechat/intent/patterns.mjs | 12 ++++++ wechat/wechat-channel.test.mjs | 17 +++++++++ 6 files changed, 114 insertions(+), 4 deletions(-) diff --git a/scheduled-task-worker.mjs b/scheduled-task-worker.mjs index b5bc61d..946ba34 100644 --- a/scheduled-task-worker.mjs +++ b/scheduled-task-worker.mjs @@ -49,13 +49,17 @@ export function startScheduledTaskWorker({ taskId: task.id, recurrence: task.recurrence, }, + }).catch((err) => { + logger.warn?.('Scheduled task web notification failed:', err); }); } if ( (notifyChannel === 'wechat' || notifyChannel === 'both') && typeof sendScheduleNotification === 'function' ) { - await sendScheduleNotification({ userId: task.userId, text }); + await sendScheduleNotification({ userId: task.userId, text }).catch((err) => { + logger.warn?.('Scheduled task wechat notification failed:', err); + }); } }; diff --git a/scheduled-task-worker.test.mjs b/scheduled-task-worker.test.mjs index b8cf693..829a206 100644 --- a/scheduled-task-worker.test.mjs +++ b/scheduled-task-worker.test.mjs @@ -175,3 +175,62 @@ test('scheduled task worker marks failure when agent only asks for clarification assert.deepEqual(calls, ['failed:SCHEDULED_TASK_NON_DELIVERY', 'notify:scheduled_task_failed']); }); + +test('scheduled task worker keeps success when wechat notification fails', async () => { + const calls = []; + const task = { + id: 'task-4', + userId: 'user-4', + title: '每日新闻页', + recurrence: 'daily', + notifyChannel: 'both', + 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(input) { + calls.push(`success:${input.id}`); + return { ...input, status: 'completed' }; + }, + async markTaskFailed() { + calls.push('failed'); + }, + }, + scheduleService: { + async createUserNotification() { + calls.push('notify:web'); + }, + }, + notificationDispatcher: { + async sendScheduleNotification() { + const err = new Error('out of response count limit'); + err.errcode = 45047; + throw err; + }, + }, + executeTask: async () => ({ + sessionId: 'session-4', + requestId: 'req-4', + deliveryText: '页面已生成:https://example.com/news.html', + }), + logger: { warn() {} }, + runOnStart: false, + }); + + await worker.runOnce(); + worker.stop(); + + assert.deepEqual(calls, ['notify:web', 'success:task-4']); +}); diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 53df969..66eddd5 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -2469,6 +2469,12 @@ export function createWechatMpService({ intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : ''; const isPageDataRequest = isWechatPageDataTask(resetCandidate); const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent); + const allowHtmlArtifactRecovery = + htmlArtifactDeliveryExpected + || ( + wechatIntent?.llmShadow?.kind === 'page.generate' + && wechatIntent?.llmShadow?.wouldChangeKind === true + ); const sessionPageContinuation = isWechatSessionPageContinuation(wechatIntent, resetCandidate); const imagePolicy = resolveWechatImageGenerationPolicy({ @@ -2596,7 +2602,7 @@ export function createWechatMpService({ userId: user.userId, sessionId, onPageGenerated, - allowRecentArtifacts: htmlArtifactDeliveryExpected, + allowRecentArtifacts: allowHtmlArtifactRecovery, htmlDeliveryAuthority, }); confirmedArtifacts = resolvedConfirmedArtifacts; @@ -2916,7 +2922,7 @@ export function createWechatMpService({ userId: user.userId, sessionId, onPageGenerated, - allowRecentArtifacts: htmlArtifactDeliveryExpected, + allowRecentArtifacts: allowHtmlArtifactRecovery, htmlDeliveryAuthority, }); const linkExistsForRequest = diff --git a/wechat/intent/page-continuation.mjs b/wechat/intent/page-continuation.mjs index eafe8a9..38c50e8 100644 --- a/wechat/intent/page-continuation.mjs +++ b/wechat/intent/page-continuation.mjs @@ -29,9 +29,20 @@ export const PAGE_IMMEDIATE_CREATE_PATTERN = export const PAGE_CONTENT_EDIT_PATTERN = /^(?:(?:请|麻烦|可以|能不能)\s*)?(?:(?:帮我|帮忙|给我)\s*)?(?:(?:把|将)\s*)?(?:(?:这个|刚才(?:的)?|上面(?:的)?|前面(?:的)?|它|这首(?:诗)?|这篇(?:文章)?|这段(?:内容)?|诗(?:里|中)?)\s*)?(?:的)?\s*(?:[\p{L}\p{N}]{0,12}\s*)?(?:改|修改|调整|更新|换|加长|缩短|润色)/iu; +const PAGE_LINK_MISSING_RETRY_PATTERN = + /(?:页面|链接|新闻页|新闻页面|html).{0,16}(?:没有生成|没生成|未生成|没发|未发)/iu; + +export function isWechatPageLinkRetryText(text) { + const normalized = String(text ?? '').trim(); + return Boolean(normalized && PAGE_LINK_MISSING_RETRY_PATTERN.test(normalized)); +} + export function isWechatPageRetryText(text) { const normalized = String(text ?? '').trim(); - return Boolean(normalized && PAGE_RETRY_PATTERN.test(normalized)); + return Boolean( + normalized + && (PAGE_RETRY_PATTERN.test(normalized) || isWechatPageLinkRetryText(normalized)), + ); } export function isWechatPageEditText(text) { @@ -53,6 +64,7 @@ export function isWechatImmediateContextPageCreate(wechatIntent, text) { export function isWechatSessionPageContinuation(wechatIntent, text) { const normalized = String(text ?? '').trim(); if (!normalized) return false; + if (isWechatPageLinkRetryText(normalized)) return true; if (isWechatPageRetryText(normalized)) return true; if (isWechatImmediateContextPageCreate(wechatIntent, normalized)) return true; if (wechatIntent?.kind === 'page.generate' && isWechatPageEditText(normalized)) return true; diff --git a/wechat/intent/patterns.mjs b/wechat/intent/patterns.mjs index d42e050..df5952d 100644 --- a/wechat/intent/patterns.mjs +++ b/wechat/intent/patterns.mjs @@ -9,6 +9,15 @@ import { shouldUseScheduledTaskAutomation } from '../../scheduled-task-intent.mj export const PAGE_GENERATE_PATTERN = /(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu; +export const PAGE_GENERATE_AFTER_PAGE_PATTERN = + /(?:html|页面|网页|page|文件).*(?:做出来|做好|做完|弄出来|生成好|制作好|发我|给我)/iu; + +export const PAGE_GENERATE_NEWS_PATTERN = + /(?:新闻|早报).{0,16}(?:页面|网页|html)/iu; + +export const PAGE_GENERATE_FORMAT_PATTERN = + /按照.{0,32}格式.{0,32}(?:页面|网页|html)/iu; + export const PAGE_GENERATE_NEGATION_PATTERN = /(?:不要|无需|不用|别|不需要)\s*(?:再\s*)?(?:生成|创建|制作|做|写)\s*(?:任何|一个|新的)?\s*(?:html|页面|网页|page|文件)/iu; @@ -34,6 +43,9 @@ export function isPageGenerateText(text) { if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false; return ( PAGE_GENERATE_PATTERN.test(normalized) + || PAGE_GENERATE_AFTER_PAGE_PATTERN.test(normalized) + || PAGE_GENERATE_NEWS_PATTERN.test(normalized) + || PAGE_GENERATE_FORMAT_PATTERN.test(normalized) || isWechatPageRetryText(normalized) || isWechatPageEditText(normalized) ); diff --git a/wechat/wechat-channel.test.mjs b/wechat/wechat-channel.test.mjs index 6c47a00..4d47dc6 100644 --- a/wechat/wechat-channel.test.mjs +++ b/wechat/wechat-channel.test.mjs @@ -42,6 +42,23 @@ test('classifyWechatIntent keeps scheduled automation on chat.general', () => { assert.equal(isPageGenerateIntent({ msgType: 'text', agentText: intent.text }), false); }); +test('classifyWechatIntent detects news page make requests', () => { + const intent = classifyWechatIntent({ + msgType: 'text', + agentText: '帮我按照7.20号新闻格式,把今天新闻页面做出来', + }); + assert.equal(intent.kind, 'page.generate'); + assert.match(intent.topic, /新闻页面/); +}); + +test('classifyWechatIntent detects missing page link retries', () => { + const intent = classifyWechatIntent({ + msgType: 'text', + agentText: '新闻页面链接没有生成', + }); + assert.equal(intent.kind, 'page.generate'); +}); + test('classifyWechatIntent detects session.reset', () => { assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换话题' }).kind, 'session.reset'); assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换新会话' }).kind, 'session.reset');