diff --git a/scheduled-task-intent.mjs b/scheduled-task-intent.mjs index 65d38e2..619dd9e 100644 --- a/scheduled-task-intent.mjs +++ b/scheduled-task-intent.mjs @@ -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, diff --git a/skills/scheduled-task-automation/SKILL.md b/skills/scheduled-task-automation/SKILL.md index 9edeedb..abe011d 100644 --- a/skills/scheduled-task-automation/SKILL.md +++ b/skills/scheduled-task-automation/SKILL.md @@ -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` 时必须原样传入。 diff --git a/wechat/handlers/scheduled-task.mjs b/wechat/handlers/scheduled-task.mjs index 6ee605a..2867372 100644 --- a/wechat/handlers/scheduled-task.mjs +++ b/wechat/handlers/scheduled-task.mjs @@ -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) { diff --git a/wechat/handlers/scheduled-task.test.mjs b/wechat/handlers/scheduled-task.test.mjs index bf163ec..bd469de 100644 --- a/wechat/handlers/scheduled-task.test.mjs +++ b/wechat/handlers/scheduled-task.test.mjs @@ -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'); }, }, });