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:
@@ -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,
|
||||
|
||||
@@ -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` 时必须原样传入。
|
||||
|
||||
@@ -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?.(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user