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:
john
2026-08-29 13:45:28 +08:00
parent d8463829f7
commit 5de58af9ed
11 changed files with 528 additions and 14 deletions
+4
View File
@@ -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,
@@ -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,
@@ -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,
+2 -1
View File
@@ -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` 时必须原样传入。
+3
View File
@@ -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?.(
+128
View File
@@ -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,
};
@@ -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,
);
});
+109
View File
@@ -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' };
}
}
+85
View File
@@ -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);
});
+50 -7
View File
@@ -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) {
+83 -6
View File
@@ -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: {