fix(wechat): cancel sole active scheduled task in inbound without Goose

When a user has exactly one active automation task, cancel it by taskId
after listTasks instead of relying on Goose when the LLM provider fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-29 12:36:19 +08:00
parent 43ec144540
commit d8463829f7
4 changed files with 69 additions and 6 deletions
+5
View File
@@ -332,6 +332,11 @@ export function formatScheduledTaskListReply(tasks = []) {
return lines.join('\n');
}
export function formatScheduledTaskCancelReply(task) {
const title = String(task?.title ?? '定时任务').trim();
return `已取消定时任务:${title}`;
}
export {
extractScheduledTaskSpec,
isMetaScheduledTaskSpec,
@@ -48,6 +48,7 @@ description: 处理定时自动任务(Scheduled Automation):澄清执行
1. 先判断用户是要**创建**、**查询**、**取消**还是**更新规范**。
2. 缺时间或缺任务内容时,一次只追问最小必要信息。
3. **取消 / 更新**前必须先 `scheduled_task_list`,用返回的 `taskId` 操作;由你理解用户指哪一条,不要猜 titleMatch。
- 微信 inbound:若用户只有 1 个 active 任务,会直接取消;多个 active 任务时才进入 Goose 让你选 taskId。
4. **更新规范**:用户补充格式标准、参考页面 URL、排版约束时,对目标任务调用 `scheduled_task_update_spec``mergeTaskSpec: true` 追加说明。
5. 创建成功后,用用户能理解的话说明何时执行、执行什么、recurrence、推送通道。
6. 若提示里给出了 `sourceMessageId`,调用 `scheduled_task_create` 时必须原样传入。
+21 -4
View File
@@ -1,4 +1,5 @@
import {
formatScheduledTaskCancelReply,
formatScheduledTaskListReply,
isScheduledTaskIntent,
parseScheduledTaskIntent,
@@ -29,10 +30,26 @@ export async function handleWechatScheduledTaskIntent({
return formatScheduledTaskListReply(tasks);
}
if (
taskIntent.action === 'create_scheduled_task'
|| taskIntent.action === 'cancel_scheduled_task'
) {
if (taskIntent.action === 'create_scheduled_task') {
return null;
}
if (taskIntent.action === 'cancel_scheduled_task') {
const tasks = await scheduledTaskService.listTasks({
userId: user.userId,
status: 'active',
limit: 20,
});
if (!tasks.length) {
return '你当前没有进行中的定时自动任务。';
}
if (tasks.length === 1) {
const cancelled = await scheduledTaskService.cancelTask({
userId: user.userId,
taskId: tasks[0].id,
});
return formatScheduledTaskCancelReply(cancelled);
}
return null;
}
} catch (err) {
+42 -2
View File
@@ -36,7 +36,8 @@ test('handleWechatScheduledTaskIntent passes incomplete create requests to goose
assert.equal(reply, null);
});
test('handleWechatScheduledTaskIntent passes cancel requests to goose', async () => {
test('handleWechatScheduledTaskIntent cancels the only active task in inbound', async () => {
const calls = [];
const reply = await handleWechatScheduledTaskIntent({
intent: {
msgType: 'text',
@@ -45,8 +46,47 @@ test('handleWechatScheduledTaskIntent passes cancel requests to goose', async ()
},
user: { userId: 'user-123' },
scheduledTaskService: {
async listTasks() {
return [{
id: 'task-1',
title: '每天早上6:30推送苏州市天气预报',
recurrence: 'daily',
hour: 6,
minute: 30,
timezone: 'Asia/Shanghai',
}];
},
async cancelTask(payload) {
calls.push(payload);
return {
id: 'task-1',
title: '每天早上6:30推送苏州市天气预报',
status: 'cancelled',
};
},
},
});
assert.match(reply, /已取消定时任务/u);
assert.deepEqual(calls, [{ userId: 'user-123', taskId: 'task-1' }]);
});
test('handleWechatScheduledTaskIntent passes cancel to goose when multiple active tasks', async () => {
const reply = await handleWechatScheduledTaskIntent({
intent: {
msgType: 'text',
agentText: '取消定时推送苏州天气任务',
msgId: 'msg-123-2',
},
user: { userId: 'user-123' },
scheduledTaskService: {
async listTasks() {
return [
{ id: 'task-1', title: '苏州天气', recurrence: 'daily', hour: 6, minute: 30, timezone: 'Asia/Shanghai' },
{ id: 'task-2', title: '新闻早报', recurrence: 'daily', hour: 5, minute: 30, timezone: 'Asia/Shanghai' },
];
},
async cancelTask() {
throw new Error('should not cancel from inbound regex');
throw new Error('should not cancel from inbound when ambiguous');
},
},
});