From 5de58af9edb99eb1f5ab55fcf2418563987c787e Mon Sep 17 00:00:00 2001 From: john Date: Sat, 29 Aug 2026 13:45:28 +0800 Subject: [PATCH] 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 --- server.mjs | 4 + .../portal-integration-services-bootstrap.mjs | 2 + ...rtal-memory-session-services-bootstrap.mjs | 6 + skills/scheduled-task-automation/SKILL.md | 3 +- wechat-mp.mjs | 3 + wechat-scheduled-task-manage-llm-config.mjs | 128 ++++++++++++++++++ ...-scheduled-task-manage-llm-config.test.mjs | 56 ++++++++ wechat-scheduled-task-manage-llm.mjs | 109 +++++++++++++++ wechat-scheduled-task-manage-llm.test.mjs | 85 ++++++++++++ wechat/handlers/scheduled-task.mjs | 57 +++++++- wechat/handlers/scheduled-task.test.mjs | 89 +++++++++++- 11 files changed, 528 insertions(+), 14 deletions(-) create mode 100644 wechat-scheduled-task-manage-llm-config.mjs create mode 100644 wechat-scheduled-task-manage-llm-config.test.mjs create mode 100644 wechat-scheduled-task-manage-llm.mjs create mode 100644 wechat-scheduled-task-manage-llm.test.mjs diff --git a/server.mjs b/server.mjs index c608b95..f412aa7 100644 --- a/server.mjs +++ b/server.mjs @@ -285,6 +285,7 @@ let systemDisclosurePolicyService = null; let agentCodeRunPolicyService = null; let wechatCursorExecutorPolicyService = null; let wechatScheduleLlmConfigService = null; +let wechatScheduledTaskManageLlmConfigService = null; let wechatIntentRouter = null; let mindSpace = null; let mindSpaceAssets = null; @@ -481,6 +482,8 @@ async function bootstrapUserAuth() { memorySessionServices.wechatCursorExecutorPolicyService; wechatScheduleLlmConfigService = memorySessionServices.wechatScheduleLlmConfigService; + wechatScheduledTaskManageLlmConfigService = + memorySessionServices.wechatScheduledTaskManageLlmConfigService; wechatIntentRouter = memorySessionServices.wechatIntentRouter; conversationMemoryService = @@ -545,6 +548,7 @@ async function bootstrapUserAuth() { taskUnifiedService, sessionSnapshotService, wechatScheduleLlmConfigService, + wechatScheduledTaskManageLlmConfigService, wechatIntentRouter, wechatCursorExecutorPolicyService, agentRunGateway, diff --git a/server/portal-integration-services-bootstrap.mjs b/server/portal-integration-services-bootstrap.mjs index 2bd60df..1fbd863 100644 --- a/server/portal-integration-services-bootstrap.mjs +++ b/server/portal-integration-services-bootstrap.mjs @@ -33,6 +33,7 @@ export async function bootstrapPortalIntegrationServices({ taskUnifiedService = null, sessionSnapshotService = null, wechatScheduleLlmConfigService, + wechatScheduledTaskManageLlmConfigService = null, wechatIntentRouter = null, wechatCursorExecutorPolicyService = null, agentRunGateway = null, @@ -178,6 +179,7 @@ export async function bootstrapPortalIntegrationServices({ ? taskUnifiedService : null, wechatScheduleLlmConfigService, + wechatScheduledTaskManageLlmConfigService, llmProviderService, chatIntentRouter, wechatIntentRouter, diff --git a/server/portal-memory-session-services-bootstrap.mjs b/server/portal-memory-session-services-bootstrap.mjs index a5e7c8e..3a4a829 100644 --- a/server/portal-memory-session-services-bootstrap.mjs +++ b/server/portal-memory-session-services-bootstrap.mjs @@ -11,6 +11,7 @@ import { isSessionStreamReplayEnabled } from '../session-stream.mjs'; import { createSkillRuntimeAdminConfigService } from '../skill-runtime-admin-config.mjs'; import { createSystemDisclosurePolicyService } from '../system-disclosure-policy.mjs'; import { createWechatScheduleLlmConfigService } from '../wechat-schedule-llm-config.mjs'; +import { createWechatScheduledTaskManageLlmConfigService } from '../wechat-scheduled-task-manage-llm-config.mjs'; import { createWechatIntentRouterConfigService } from '../wechat-intent-router-config.mjs'; import { createManagedWechatIntentRouter } from '../wechat-intent-router.mjs'; import { createWechatCursorExecutorAdminConfigService } from '../wechat-cursor-executor-admin-config.mjs'; @@ -33,6 +34,8 @@ export async function bootstrapPortalMemorySessionServices({ createAgentCodeRunAdminConfigService, createWechatScheduleLlmConfigServiceFn = createWechatScheduleLlmConfigService, + createWechatScheduledTaskManageLlmConfigServiceFn = + createWechatScheduledTaskManageLlmConfigService, createWechatIntentRouterConfigServiceFn = createWechatIntentRouterConfigService, createWechatCursorExecutorAdminConfigServiceFn = @@ -80,6 +83,8 @@ export async function bootstrapPortalMemorySessionServices({ createAgentCodeRunAdminConfigServiceFn(pool, { env }); const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigServiceFn(pool); + const wechatScheduledTaskManageLlmConfigService = + createWechatScheduledTaskManageLlmConfigServiceFn(pool); const wechatIntentRouterConfigService = createWechatIntentRouterConfigServiceFn(pool, { env }); const wechatCursorExecutorPolicyService = @@ -160,6 +165,7 @@ export async function bootstrapPortalMemorySessionServices({ systemDisclosurePolicyService, agentCodeRunPolicyService, wechatScheduleLlmConfigService, + wechatScheduledTaskManageLlmConfigService, wechatIntentRouterConfigService, wechatCursorExecutorPolicyService, wechatIntentRouter, diff --git a/skills/scheduled-task-automation/SKILL.md b/skills/scheduled-task-automation/SKILL.md index abe011d..d63ed88 100644 --- a/skills/scheduled-task-automation/SKILL.md +++ b/skills/scheduled-task-automation/SKILL.md @@ -48,7 +48,8 @@ description: 处理定时自动任务(Scheduled Automation):澄清执行 1. 先判断用户是要**创建**、**查询**、**取消**还是**更新规范**。 2. 缺时间或缺任务内容时,一次只追问最小必要信息。 3. **取消 / 更新**前必须先 `scheduled_task_list`,用返回的 `taskId` 操作;由你理解用户指哪一条,不要猜 titleMatch。 - - 微信 inbound:若用户只有 1 个 active 任务,会直接取消;多个 active 任务时才进入 Goose 让你选 taskId。 + - 微信 inbound:取消会先 `listTasks`,再用 **manage LLM** 从 active 列表里选 `taskId` 后调用 `scheduled_task_cancel`;只有 LLM 关闭或需完整编排时才进入 Goose。 + - 用户在要求 **生成/修改 HTML 页面**(`page.generate`)时,不要走定时任务取消链路。 4. **更新规范**:用户补充格式标准、参考页面 URL、排版约束时,对目标任务调用 `scheduled_task_update_spec`,`mergeTaskSpec: true` 追加说明。 5. 创建成功后,用用户能理解的话说明何时执行、执行什么、recurrence、推送通道。 6. 若提示里给出了 `sourceMessageId`,调用 `scheduled_task_create` 时必须原样传入。 diff --git a/wechat-mp.mjs b/wechat-mp.mjs index 89b1f23..4b2d266 100644 --- a/wechat-mp.mjs +++ b/wechat-mp.mjs @@ -1634,6 +1634,7 @@ export function createWechatMpService({ intentDraftService = null, taskUnifiedService = null, wechatScheduleLlmConfigService = null, + wechatScheduledTaskManageLlmConfigService = null, llmProviderService = null, chatIntentRouter = null, wechatIntentRouter = null, @@ -4159,6 +4160,8 @@ export function createWechatMpService({ intent, user: boundUser, scheduledTaskService, + wechatScheduledTaskManageLlmConfigService, + llmProviderService, logger, }).catch((err) => { logger.warn?.( diff --git a/wechat-scheduled-task-manage-llm-config.mjs b/wechat-scheduled-task-manage-llm-config.mjs new file mode 100644 index 0000000..1b0c81c --- /dev/null +++ b/wechat-scheduled-task-manage-llm-config.mjs @@ -0,0 +1,128 @@ +const CONFIG_TABLE = 'h5_wechat_admin_config'; +const CONFIG_KEY = 'scheduled_task_manage_llm'; + +function normalizeBoolean(value, fallback = false) { + if (value == null || value === '') return fallback; + if (typeof value === 'boolean') return value; + const normalized = String(value).trim().toLowerCase(); + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; + if (['0', 'false', 'no', 'off'].includes(normalized)) return false; + return fallback; +} + +function parseConfigJson(value) { + if (!value) return {}; + if (typeof value === 'object') return value; + try { + return JSON.parse(String(value)); + } catch { + return {}; + } +} + +async function ensureConfigTable(pool) { + await pool.query(` + CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} ( + config_key VARCHAR(64) PRIMARY KEY, + config_json JSON NOT NULL, + updated_by CHAR(36) NULL, + updated_at BIGINT NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); +} + +function defaultsFromEnv(env = process.env) { + return { + manageLlmEnabled: normalizeBoolean(env.H5_WECHAT_SCHEDULED_TASK_MANAGE_LLM_ENABLED, true), + modelProviderKeyId: String(env.MEMIND_WECHAT_SCHEDULED_TASK_MANAGE_LLM_MODEL_PROVIDER_KEY_ID ?? '').trim() + || String(env.MEMIND_WECHAT_SCHEDULE_LLM_MODEL_PROVIDER_KEY_ID ?? '').trim() + || null, + model: String(env.MEMIND_WECHAT_SCHEDULED_TASK_MANAGE_LLM_MODEL ?? '').trim() + || String(env.MEMIND_WECHAT_SCHEDULE_LLM_MODEL ?? '').trim() + || null, + }; +} + +export function createWechatScheduledTaskManageLlmConfigService(pool, { env = process.env } = {}) { + let ensurePromise = null; + + async function ensureReady() { + if (!ensurePromise) ensurePromise = ensureConfigTable(pool); + await ensurePromise; + } + + async function readRow() { + await ensureReady(); + const [rows] = await pool.query( + `SELECT config_json, updated_by, updated_at + FROM ${CONFIG_TABLE} + WHERE config_key = ? + LIMIT 1`, + [CONFIG_KEY], + ); + return rows[0] ?? null; + } + + function mergeConfig(row) { + const defaults = defaultsFromEnv(env); + const stored = parseConfigJson(row?.config_json); + return { + manageLlmEnabled: normalizeBoolean( + stored.manageLlmEnabled, + defaults.manageLlmEnabled, + ), + modelProviderKeyId: String(stored.modelProviderKeyId ?? defaults.modelProviderKeyId ?? '').trim() || null, + model: String(stored.model ?? defaults.model ?? '').trim() || null, + }; + } + + return { + async getConfig() { + const row = await readRow(); + return { + ...mergeConfig(row), + updatedAt: row?.updated_at ? Number(row.updated_at) : null, + updatedBy: row?.updated_by ?? null, + }; + }, + + async isManageLlmEnabled() { + const config = await this.getConfig(); + return config.manageLlmEnabled; + }, + + async updateConfig(payload = {}, { updatedBy = null } = {}) { + const current = await this.getConfig(); + const next = { + manageLlmEnabled: + payload.manageLlmEnabled === undefined + ? current.manageLlmEnabled + : normalizeBoolean(payload.manageLlmEnabled, current.manageLlmEnabled), + modelProviderKeyId: + payload.modelProviderKeyId === undefined + ? (current.modelProviderKeyId ?? '') + : String(payload.modelProviderKeyId ?? '').trim(), + model: + payload.model === undefined + ? (current.model ?? '') + : String(payload.model ?? '').trim(), + }; + const now = Date.now(); + await pool.query( + `INSERT INTO ${CONFIG_TABLE} (config_key, config_json, updated_by, updated_at) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + config_json = VALUES(config_json), + updated_by = VALUES(updated_by), + updated_at = VALUES(updated_at)`, + [CONFIG_KEY, JSON.stringify(next), updatedBy, now], + ); + return this.getConfig(); + }, + }; +} + +export const wechatScheduledTaskManageLlmConfigInternals = { + normalizeBoolean, + defaultsFromEnv, +}; diff --git a/wechat-scheduled-task-manage-llm-config.test.mjs b/wechat-scheduled-task-manage-llm-config.test.mjs new file mode 100644 index 0000000..d2dafc9 --- /dev/null +++ b/wechat-scheduled-task-manage-llm-config.test.mjs @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createWechatScheduledTaskManageLlmConfigService, + wechatScheduledTaskManageLlmConfigInternals, +} from './wechat-scheduled-task-manage-llm-config.mjs'; + +function createPool(seedRow = null) { + const state = { row: seedRow }; + return { + async query(sql, params) { + if (sql.includes('CREATE TABLE')) return [[], []]; + if (sql.includes('SELECT config_json')) { + return [state.row ? [state.row] : [], []]; + } + if (sql.includes('INSERT INTO h5_wechat_admin_config')) { + state.row = { + config_json: params[1], + updated_by: params[2], + updated_at: params[3], + }; + return [[], []]; + } + throw new Error(`Unexpected query: ${sql}`); + }, + }; +} + +test('scheduled task manage llm config defaults to enabled', async () => { + const service = createWechatScheduledTaskManageLlmConfigService(createPool(), { + env: {}, + }); + const result = await service.getConfig(); + assert.equal(result.manageLlmEnabled, true); +}); + +test('scheduled task manage llm config persists admin toggle', async () => { + const service = createWechatScheduledTaskManageLlmConfigService(createPool(), { + env: { H5_WECHAT_SCHEDULED_TASK_MANAGE_LLM_ENABLED: '0' }, + }); + const updated = await service.updateConfig({ manageLlmEnabled: true }, { updatedBy: 'admin-1' }); + assert.equal(updated.manageLlmEnabled, true); + assert.equal(updated.updatedBy, 'admin-1'); + assert.equal(await service.isManageLlmEnabled(), true); +}); + +test('scheduled task manage llm config internals normalize booleans consistently', () => { + assert.equal(wechatScheduledTaskManageLlmConfigInternals.normalizeBoolean('1', false), true); + assert.equal(wechatScheduledTaskManageLlmConfigInternals.normalizeBoolean('off', true), false); + assert.equal( + wechatScheduledTaskManageLlmConfigInternals.defaultsFromEnv({ + H5_WECHAT_SCHEDULED_TASK_MANAGE_LLM_ENABLED: 'false', + }).manageLlmEnabled, + false, + ); +}); diff --git a/wechat-scheduled-task-manage-llm.mjs b/wechat-scheduled-task-manage-llm.mjs new file mode 100644 index 0000000..d6b726d --- /dev/null +++ b/wechat-scheduled-task-manage-llm.mjs @@ -0,0 +1,109 @@ +function parseJsonReply(reply) { + const text = String(reply ?? '').trim(); + if (!text) return null; + const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); + const candidate = fenced?.[1] ?? text; + try { + return JSON.parse(candidate); + } catch { + return null; + } +} + +export function buildScheduledTaskListPayload(tasks = []) { + return tasks.map((task) => ({ + taskId: task.id, + title: task.title, + recurrence: task.recurrence, + hour: task.hour, + minute: task.minute ?? 0, + taskSpec: String(task.taskSpec ?? '').slice(0, 240), + })); +} + +export function normalizeScheduledTaskManageLlmDecision(payload, tasks = []) { + const action = String(payload?.action ?? 'none').trim(); + const taskIds = new Set(tasks.map((task) => String(task.id ?? '').trim()).filter(Boolean)); + + if (action === 'clarify') { + const message = String(payload?.message ?? '').trim() + || '我找到多个定时任务,请告诉我要取消哪一个。'; + return { action: 'clarify', message }; + } + + if (action === 'cancel') { + const taskId = String(payload?.taskId ?? '').trim(); + if (taskId && taskIds.has(taskId)) { + return { action: 'cancel', taskId }; + } + return { action: 'none' }; + } + + return { action: 'none' }; +} + +export async function resolveScheduledTaskCancelWithLlm({ + text, + tasks = [], + llmProviderService, + modelProviderKeyId = null, + model = null, + logger = console, +} = {}) { + if (!llmProviderService || typeof llmProviderService.createChatCompletion !== 'function') { + return { action: 'none' }; + } + if (!Array.isArray(tasks) || tasks.length === 0) { + return { action: 'none' }; + } + + const taskList = buildScheduledTaskListPayload(tasks); + + try { + const result = await llmProviderService.createChatCompletion({ + ...(modelProviderKeyId ? { providerKeyId: modelProviderKeyId } : {}), + ...(model ? { model } : {}), + temperature: 0, + messages: [ + { + role: 'system', + content: [ + '你是微信服务号定时自动任务管理助手,只做任务选择,不做页面生成。', + '输入包含用户原话和当前 active 定时任务列表(JSON)。', + '请严格输出 JSON,不要输出解释。', + '允许 action:', + '- cancel:用户明确要取消某个任务,且能从列表中唯一确定 taskId', + '- clarify:用户想取消但无法唯一确定是哪一个,message 用中文简短追问', + '- none:不是取消任务,或信息不足且不应猜测', + 'cancel 时必须返回列表里存在的 taskId;禁止编造 taskId。', + '如果只有一条任务且用户明确要取消/停止定时任务,可以返回该 taskId。', + '如果用户在要求生成/修改 HTML 页面,而不是管理已有定时任务,返回 none。', + ].join('\n'), + }, + { + role: 'user', + content: JSON.stringify({ + userMessage: String(text ?? '').trim(), + activeTasks: taskList, + }), + }, + ], + }); + + if (!result?.ok) { + logger?.warn?.( + '[wechat-scheduled-task-manage-llm] parse skipped:', + result?.message ?? 'unknown', + ); + return { action: 'none' }; + } + + return normalizeScheduledTaskManageLlmDecision(parseJsonReply(result.reply), tasks); + } catch (err) { + logger?.warn?.( + '[wechat-scheduled-task-manage-llm] parse skipped:', + err instanceof Error ? err.message : err, + ); + return { action: 'none' }; + } +} diff --git a/wechat-scheduled-task-manage-llm.test.mjs b/wechat-scheduled-task-manage-llm.test.mjs new file mode 100644 index 0000000..b071fc9 --- /dev/null +++ b/wechat-scheduled-task-manage-llm.test.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + buildScheduledTaskListPayload, + normalizeScheduledTaskManageLlmDecision, + resolveScheduledTaskCancelWithLlm, +} from './wechat-scheduled-task-manage-llm.mjs'; + +const tasks = [ + { + id: 'task-suzhou', + title: '每天早上6:30推送苏州市天气预报', + recurrence: 'daily', + hour: 6, + minute: 30, + taskSpec: '查询苏州天气并推送', + }, + { + id: 'task-news', + title: '每日新闻早报页面(5:30)', + recurrence: 'daily', + hour: 5, + minute: 30, + taskSpec: '生成新闻早报页面', + }, +]; + +test('buildScheduledTaskListPayload maps active tasks for llm input', () => { + const payload = buildScheduledTaskListPayload(tasks); + assert.equal(payload.length, 2); + assert.equal(payload[0].taskId, 'task-suzhou'); + assert.match(payload[0].taskSpec, /苏州/u); +}); + +test('normalizeScheduledTaskManageLlmDecision accepts valid cancel taskId', () => { + const decision = normalizeScheduledTaskManageLlmDecision({ + action: 'cancel', + taskId: 'task-news', + }, tasks); + assert.deepEqual(decision, { action: 'cancel', taskId: 'task-news' }); +}); + +test('normalizeScheduledTaskManageLlmDecision rejects unknown taskId', () => { + const decision = normalizeScheduledTaskManageLlmDecision({ + action: 'cancel', + taskId: 'missing', + }, tasks); + assert.deepEqual(decision, { action: 'none' }); +}); + +test('resolveScheduledTaskCancelWithLlm can pick the matching task', async () => { + const reply = await resolveScheduledTaskCancelWithLlm({ + text: '取消定时推送苏州天气任务', + tasks, + llmProviderService: { + async createChatCompletion() { + return { + ok: true, + reply: JSON.stringify({ action: 'cancel', taskId: 'task-suzhou' }), + }; + }, + }, + }); + assert.deepEqual(reply, { action: 'cancel', taskId: 'task-suzhou' }); +}); + +test('resolveScheduledTaskCancelWithLlm returns clarify when ambiguous', async () => { + const reply = await resolveScheduledTaskCancelWithLlm({ + text: '帮我取消定时任务', + tasks, + llmProviderService: { + async createChatCompletion() { + return { + ok: true, + reply: JSON.stringify({ + action: 'clarify', + message: '你是想取消苏州天气,还是 5:30 新闻早报?', + }), + }; + }, + }, + }); + assert.equal(reply.action, 'clarify'); + assert.match(reply.message, /新闻早报/u); +}); diff --git a/wechat/handlers/scheduled-task.mjs b/wechat/handlers/scheduled-task.mjs index 2867372..6811598 100644 --- a/wechat/handlers/scheduled-task.mjs +++ b/wechat/handlers/scheduled-task.mjs @@ -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) { diff --git a/wechat/handlers/scheduled-task.test.mjs b/wechat/handlers/scheduled-task.test.mjs index bd469de..e510fad 100644 --- a/wechat/handlers/scheduled-task.test.mjs +++ b/wechat/handlers/scheduled-task.test.mjs @@ -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: {