fix(wechat): resolve scheduled task cancel via manage LLM instead of hard rules
Use a lightweight LLM over active task lists to pick taskId for cancel, exclude page.generate from scheduled-task inbound, and remove single-task auto-cancel so Cursor page generation stays isolated. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,21 +1,30 @@
|
||||
import { isPageGenerateText } from '../intent/patterns.mjs';
|
||||
import {
|
||||
formatScheduledTaskCancelReply,
|
||||
formatScheduledTaskListReply,
|
||||
isScheduledTaskIntent,
|
||||
parseScheduledTaskIntent,
|
||||
} from '../../scheduled-task-intent.mjs';
|
||||
import { resolveScheduledTaskCancelWithLlm } from '../../wechat-scheduled-task-manage-llm.mjs';
|
||||
|
||||
export async function handleWechatScheduledTaskIntent({
|
||||
intent,
|
||||
user,
|
||||
scheduledTaskService,
|
||||
wechatScheduledTaskManageLlmConfigService = null,
|
||||
llmProviderService = null,
|
||||
env = process.env,
|
||||
logger = console,
|
||||
}) {
|
||||
if (!scheduledTaskService) return null;
|
||||
|
||||
const agentText = String(intent?.agentText ?? '').trim();
|
||||
if (isPageGenerateText(agentText)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timezone = env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai';
|
||||
const taskIntent = parseScheduledTaskIntent(intent.agentText, {
|
||||
const taskIntent = parseScheduledTaskIntent(agentText, {
|
||||
timezone,
|
||||
});
|
||||
if (!isScheduledTaskIntent(taskIntent)) return null;
|
||||
@@ -43,13 +52,47 @@ export async function handleWechatScheduledTaskIntent({
|
||||
if (!tasks.length) {
|
||||
return '你当前没有进行中的定时自动任务。';
|
||||
}
|
||||
if (tasks.length === 1) {
|
||||
const cancelled = await scheduledTaskService.cancelTask({
|
||||
userId: user.userId,
|
||||
taskId: tasks[0].id,
|
||||
});
|
||||
return formatScheduledTaskCancelReply(cancelled);
|
||||
|
||||
let manageLlmEnabled = false;
|
||||
let modelProviderKeyId = null;
|
||||
let model = null;
|
||||
if (wechatScheduledTaskManageLlmConfigService && llmProviderService) {
|
||||
try {
|
||||
manageLlmEnabled = await wechatScheduledTaskManageLlmConfigService.isManageLlmEnabled();
|
||||
if (manageLlmEnabled) {
|
||||
const config = await wechatScheduledTaskManageLlmConfigService.getConfig();
|
||||
modelProviderKeyId = config.modelProviderKeyId ?? null;
|
||||
model = config.model ?? null;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn?.(
|
||||
'[wechat-scheduled-task-manage-llm] config load failed:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (manageLlmEnabled) {
|
||||
const decision = await resolveScheduledTaskCancelWithLlm({
|
||||
text: agentText,
|
||||
tasks,
|
||||
llmProviderService,
|
||||
modelProviderKeyId,
|
||||
model,
|
||||
logger,
|
||||
});
|
||||
if (decision.action === 'cancel' && decision.taskId) {
|
||||
const cancelled = await scheduledTaskService.cancelTask({
|
||||
userId: user.userId,
|
||||
taskId: decision.taskId,
|
||||
});
|
||||
return formatScheduledTaskCancelReply(cancelled);
|
||||
}
|
||||
if (decision.action === 'clarify') {
|
||||
return decision.message;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -36,7 +36,24 @@ test('handleWechatScheduledTaskIntent passes incomplete create requests to goose
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent cancels the only active task in inbound', async () => {
|
||||
test('handleWechatScheduledTaskIntent yields page.generate requests to cursor/goose path', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
msgType: 'text',
|
||||
agentText: '帮我做一个苏州天气展示页面',
|
||||
msgId: 'msg-page-1',
|
||||
},
|
||||
user: { userId: 'user-tang' },
|
||||
scheduledTaskService: {
|
||||
async listTasks() {
|
||||
throw new Error('should not list for page.generate');
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent cancels via manage llm when task is identified', async () => {
|
||||
const calls = [];
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
@@ -48,7 +65,7 @@ test('handleWechatScheduledTaskIntent cancels the only active task in inbound',
|
||||
scheduledTaskService: {
|
||||
async listTasks() {
|
||||
return [{
|
||||
id: 'task-1',
|
||||
id: 'task-suzhou',
|
||||
title: '每天早上6:30推送苏州市天气预报',
|
||||
recurrence: 'daily',
|
||||
hour: 6,
|
||||
@@ -59,18 +76,34 @@ test('handleWechatScheduledTaskIntent cancels the only active task in inbound',
|
||||
async cancelTask(payload) {
|
||||
calls.push(payload);
|
||||
return {
|
||||
id: 'task-1',
|
||||
id: 'task-suzhou',
|
||||
title: '每天早上6:30推送苏州市天气预报',
|
||||
status: 'cancelled',
|
||||
};
|
||||
},
|
||||
},
|
||||
wechatScheduledTaskManageLlmConfigService: {
|
||||
async isManageLlmEnabled() {
|
||||
return true;
|
||||
},
|
||||
async getConfig() {
|
||||
return { modelProviderKeyId: 'key-1', model: 'deepseek-chat' };
|
||||
},
|
||||
},
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
return {
|
||||
ok: true,
|
||||
reply: JSON.stringify({ action: 'cancel', taskId: 'task-suzhou' }),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.match(reply, /已取消定时任务/u);
|
||||
assert.deepEqual(calls, [{ userId: 'user-123', taskId: 'task-1' }]);
|
||||
assert.deepEqual(calls, [{ userId: 'user-123', taskId: 'task-suzhou' }]);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent passes cancel to goose when multiple active tasks', async () => {
|
||||
test('handleWechatScheduledTaskIntent passes cancel to goose when manage llm is disabled', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
msgType: 'text',
|
||||
@@ -86,13 +119,57 @@ test('handleWechatScheduledTaskIntent passes cancel to goose when multiple activ
|
||||
];
|
||||
},
|
||||
async cancelTask() {
|
||||
throw new Error('should not cancel from inbound when ambiguous');
|
||||
throw new Error('should not cancel from inbound when llm disabled');
|
||||
},
|
||||
},
|
||||
wechatScheduledTaskManageLlmConfigService: {
|
||||
async isManageLlmEnabled() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent returns clarify message when manage llm is ambiguous', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
msgType: 'text',
|
||||
agentText: '帮我取消定时任务',
|
||||
msgId: 'msg-123-3',
|
||||
},
|
||||
user: { userId: 'user-tang' },
|
||||
scheduledTaskService: {
|
||||
async listTasks() {
|
||||
return [
|
||||
{ id: 'task-news', title: '每日新闻早报页面(5:30)', recurrence: 'daily', hour: 5, minute: 30 },
|
||||
{ id: 'task-checkin', title: '每日打卡提醒', recurrence: 'daily', hour: 18, minute: 0 },
|
||||
];
|
||||
},
|
||||
},
|
||||
wechatScheduledTaskManageLlmConfigService: {
|
||||
async isManageLlmEnabled() {
|
||||
return true;
|
||||
},
|
||||
async getConfig() {
|
||||
return { modelProviderKeyId: 'key-1', model: 'deepseek-chat' };
|
||||
},
|
||||
},
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
return {
|
||||
ok: true,
|
||||
reply: JSON.stringify({
|
||||
action: 'clarify',
|
||||
message: '你是想取消 5:30 新闻早报,还是 18:00 打卡提醒?',
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.match(reply, /新闻早报/u);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent ignores non automation text', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
|
||||
Reference in New Issue
Block a user