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:
@@ -332,6 +332,11 @@ export function formatScheduledTaskListReply(tasks = []) {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatScheduledTaskCancelReply(task) {
|
||||||
|
const title = String(task?.title ?? '定时任务').trim();
|
||||||
|
return `已取消定时任务:${title}`;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
extractScheduledTaskSpec,
|
extractScheduledTaskSpec,
|
||||||
isMetaScheduledTaskSpec,
|
isMetaScheduledTaskSpec,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ description: 处理定时自动任务(Scheduled Automation):澄清执行
|
|||||||
1. 先判断用户是要**创建**、**查询**、**取消**还是**更新规范**。
|
1. 先判断用户是要**创建**、**查询**、**取消**还是**更新规范**。
|
||||||
2. 缺时间或缺任务内容时,一次只追问最小必要信息。
|
2. 缺时间或缺任务内容时,一次只追问最小必要信息。
|
||||||
3. **取消 / 更新**前必须先 `scheduled_task_list`,用返回的 `taskId` 操作;由你理解用户指哪一条,不要猜 titleMatch。
|
3. **取消 / 更新**前必须先 `scheduled_task_list`,用返回的 `taskId` 操作;由你理解用户指哪一条,不要猜 titleMatch。
|
||||||
|
- 微信 inbound:若用户只有 1 个 active 任务,会直接取消;多个 active 任务时才进入 Goose 让你选 taskId。
|
||||||
4. **更新规范**:用户补充格式标准、参考页面 URL、排版约束时,对目标任务调用 `scheduled_task_update_spec`,`mergeTaskSpec: true` 追加说明。
|
4. **更新规范**:用户补充格式标准、参考页面 URL、排版约束时,对目标任务调用 `scheduled_task_update_spec`,`mergeTaskSpec: true` 追加说明。
|
||||||
5. 创建成功后,用用户能理解的话说明何时执行、执行什么、recurrence、推送通道。
|
5. 创建成功后,用用户能理解的话说明何时执行、执行什么、recurrence、推送通道。
|
||||||
6. 若提示里给出了 `sourceMessageId`,调用 `scheduled_task_create` 时必须原样传入。
|
6. 若提示里给出了 `sourceMessageId`,调用 `scheduled_task_create` 时必须原样传入。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
formatScheduledTaskCancelReply,
|
||||||
formatScheduledTaskListReply,
|
formatScheduledTaskListReply,
|
||||||
isScheduledTaskIntent,
|
isScheduledTaskIntent,
|
||||||
parseScheduledTaskIntent,
|
parseScheduledTaskIntent,
|
||||||
@@ -29,10 +30,26 @@ export async function handleWechatScheduledTaskIntent({
|
|||||||
return formatScheduledTaskListReply(tasks);
|
return formatScheduledTaskListReply(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (taskIntent.action === 'create_scheduled_task') {
|
||||||
taskIntent.action === 'create_scheduled_task'
|
return null;
|
||||||
|| taskIntent.action === 'cancel_scheduled_task'
|
}
|
||||||
) {
|
|
||||||
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ test('handleWechatScheduledTaskIntent passes incomplete create requests to goose
|
|||||||
assert.equal(reply, null);
|
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({
|
const reply = await handleWechatScheduledTaskIntent({
|
||||||
intent: {
|
intent: {
|
||||||
msgType: 'text',
|
msgType: 'text',
|
||||||
@@ -45,8 +46,47 @@ test('handleWechatScheduledTaskIntent passes cancel requests to goose', async ()
|
|||||||
},
|
},
|
||||||
user: { userId: 'user-123' },
|
user: { userId: 'user-123' },
|
||||||
scheduledTaskService: {
|
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() {
|
async cancelTask() {
|
||||||
throw new Error('should not cancel from inbound regex');
|
throw new Error('should not cancel from inbound when ambiguous');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user