fix(wechat): recognize news page intents and harden scheduled task delivery
Memind CI / Test, build, and release guards (push) Failing after 4s
Memind CI / Test, build, and release guards (push) Failing after 4s
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -49,13 +49,17 @@ export function startScheduledTaskWorker({
|
|||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
recurrence: task.recurrence,
|
recurrence: task.recurrence,
|
||||||
},
|
},
|
||||||
|
}).catch((err) => {
|
||||||
|
logger.warn?.('Scheduled task web notification failed:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(notifyChannel === 'wechat' || notifyChannel === 'both')
|
(notifyChannel === 'wechat' || notifyChannel === 'both')
|
||||||
&& typeof sendScheduleNotification === 'function'
|
&& 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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']);
|
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']);
|
||||||
|
});
|
||||||
|
|||||||
+8
-2
@@ -2469,6 +2469,12 @@ export function createWechatMpService({
|
|||||||
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
||||||
const isPageDataRequest = isWechatPageDataTask(resetCandidate);
|
const isPageDataRequest = isWechatPageDataTask(resetCandidate);
|
||||||
const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent);
|
const htmlArtifactDeliveryExpected = shouldDeliverWechatHtmlArtifacts(wechatIntent, intent);
|
||||||
|
const allowHtmlArtifactRecovery =
|
||||||
|
htmlArtifactDeliveryExpected
|
||||||
|
|| (
|
||||||
|
wechatIntent?.llmShadow?.kind === 'page.generate'
|
||||||
|
&& wechatIntent?.llmShadow?.wouldChangeKind === true
|
||||||
|
);
|
||||||
const sessionPageContinuation =
|
const sessionPageContinuation =
|
||||||
isWechatSessionPageContinuation(wechatIntent, resetCandidate);
|
isWechatSessionPageContinuation(wechatIntent, resetCandidate);
|
||||||
const imagePolicy = resolveWechatImageGenerationPolicy({
|
const imagePolicy = resolveWechatImageGenerationPolicy({
|
||||||
@@ -2596,7 +2602,7 @@ export function createWechatMpService({
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
sessionId,
|
sessionId,
|
||||||
onPageGenerated,
|
onPageGenerated,
|
||||||
allowRecentArtifacts: htmlArtifactDeliveryExpected,
|
allowRecentArtifacts: allowHtmlArtifactRecovery,
|
||||||
htmlDeliveryAuthority,
|
htmlDeliveryAuthority,
|
||||||
});
|
});
|
||||||
confirmedArtifacts = resolvedConfirmedArtifacts;
|
confirmedArtifacts = resolvedConfirmedArtifacts;
|
||||||
@@ -2916,7 +2922,7 @@ export function createWechatMpService({
|
|||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
sessionId,
|
sessionId,
|
||||||
onPageGenerated,
|
onPageGenerated,
|
||||||
allowRecentArtifacts: htmlArtifactDeliveryExpected,
|
allowRecentArtifacts: allowHtmlArtifactRecovery,
|
||||||
htmlDeliveryAuthority,
|
htmlDeliveryAuthority,
|
||||||
});
|
});
|
||||||
const linkExistsForRequest =
|
const linkExistsForRequest =
|
||||||
|
|||||||
@@ -29,9 +29,20 @@ export const PAGE_IMMEDIATE_CREATE_PATTERN =
|
|||||||
export const PAGE_CONTENT_EDIT_PATTERN =
|
export const PAGE_CONTENT_EDIT_PATTERN =
|
||||||
/^(?:(?:请|麻烦|可以|能不能)\s*)?(?:(?:帮我|帮忙|给我)\s*)?(?:(?:把|将)\s*)?(?:(?:这个|刚才(?:的)?|上面(?:的)?|前面(?:的)?|它|这首(?:诗)?|这篇(?:文章)?|这段(?:内容)?|诗(?:里|中)?)\s*)?(?:的)?\s*(?:[\p{L}\p{N}]{0,12}\s*)?(?:改|修改|调整|更新|换|加长|缩短|润色)/iu;
|
/^(?:(?:请|麻烦|可以|能不能)\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) {
|
export function isWechatPageRetryText(text) {
|
||||||
const normalized = String(text ?? '').trim();
|
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) {
|
export function isWechatPageEditText(text) {
|
||||||
@@ -53,6 +64,7 @@ export function isWechatImmediateContextPageCreate(wechatIntent, text) {
|
|||||||
export function isWechatSessionPageContinuation(wechatIntent, text) {
|
export function isWechatSessionPageContinuation(wechatIntent, text) {
|
||||||
const normalized = String(text ?? '').trim();
|
const normalized = String(text ?? '').trim();
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
|
if (isWechatPageLinkRetryText(normalized)) return true;
|
||||||
if (isWechatPageRetryText(normalized)) return true;
|
if (isWechatPageRetryText(normalized)) return true;
|
||||||
if (isWechatImmediateContextPageCreate(wechatIntent, normalized)) return true;
|
if (isWechatImmediateContextPageCreate(wechatIntent, normalized)) return true;
|
||||||
if (wechatIntent?.kind === 'page.generate' && isWechatPageEditText(normalized)) return true;
|
if (wechatIntent?.kind === 'page.generate' && isWechatPageEditText(normalized)) return true;
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import { shouldUseScheduledTaskAutomation } from '../../scheduled-task-intent.mj
|
|||||||
export const PAGE_GENERATE_PATTERN =
|
export const PAGE_GENERATE_PATTERN =
|
||||||
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu;
|
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?: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 =
|
export const PAGE_GENERATE_NEGATION_PATTERN =
|
||||||
/(?:不要|无需|不用|别|不需要)\s*(?:再\s*)?(?:生成|创建|制作|做|写)\s*(?:任何|一个|新的)?\s*(?:html|页面|网页|page|文件)/iu;
|
/(?:不要|无需|不用|别|不需要)\s*(?:再\s*)?(?:生成|创建|制作|做|写)\s*(?:任何|一个|新的)?\s*(?:html|页面|网页|page|文件)/iu;
|
||||||
|
|
||||||
@@ -34,6 +43,9 @@ export function isPageGenerateText(text) {
|
|||||||
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
|
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
|
||||||
return (
|
return (
|
||||||
PAGE_GENERATE_PATTERN.test(normalized)
|
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)
|
|| isWechatPageRetryText(normalized)
|
||||||
|| isWechatPageEditText(normalized)
|
|| isWechatPageEditText(normalized)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,6 +42,23 @@ test('classifyWechatIntent keeps scheduled automation on chat.general', () => {
|
|||||||
assert.equal(isPageGenerateIntent({ msgType: 'text', agentText: intent.text }), false);
|
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', () => {
|
test('classifyWechatIntent detects session.reset', () => {
|
||||||
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换话题' }).kind, 'session.reset');
|
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换话题' }).kind, 'session.reset');
|
||||||
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换新会话' }).kind, 'session.reset');
|
assert.equal(classifyWechatIntent({ msgType: 'text', agentText: '换新会话' }).kind, 'session.reset');
|
||||||
|
|||||||
Reference in New Issue
Block a user