fix(wechat): pass inbound messages to goose and gate side effects at MCP tools
Memind CI / Test, build, and release guards (push) Failing after 14m19s
Memind CI / Test, build, and release guards (push) Failing after 14m19s
Stop ITL regex from intercepting new messages before Agent execution; move scheduled task and schedule write confirmation to MCP tool calls with draft commit on 确认. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import { resolveScheduleTimestamp } from './schedule-time.mjs';
|
||||
import { shouldAutoCreateReminderAtStart } from './schedule-service.mjs';
|
||||
import {
|
||||
isScheduledTaskWorkerEnabled,
|
||||
scheduledTaskWorkerDisabledMessage,
|
||||
} from './scheduled-task-worker-config.mjs';
|
||||
|
||||
export async function executeScheduledTaskCreateTool(args, {
|
||||
userId,
|
||||
scheduledTaskService,
|
||||
timezone = 'Asia/Shanghai',
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
if (!scheduledTaskService) throw new Error('scheduledTaskService 不可用');
|
||||
const task = await scheduledTaskService.createTask({
|
||||
userId,
|
||||
title: args.title ?? null,
|
||||
taskSpec: args.taskSpec,
|
||||
recurrence: args.recurrence ?? 'daily',
|
||||
runAtLocal: args.runAtLocal ?? null,
|
||||
hour: args.hour ?? null,
|
||||
minute: args.minute ?? 0,
|
||||
weekday: args.weekday ?? null,
|
||||
timezone: args.timezone ?? timezone,
|
||||
notifyChannel: args.notifyChannel ?? 'both',
|
||||
sourceChannel: args.sourceChannel ?? 'agent',
|
||||
sourceMessageId: args.sourceMessageId ?? null,
|
||||
sourceText: args.sourceText ?? null,
|
||||
});
|
||||
const workerWarning = isScheduledTaskWorkerEnabled(env)
|
||||
? null
|
||||
: scheduledTaskWorkerDisabledMessage(env);
|
||||
return workerWarning ? { ...task, workerWarning } : task;
|
||||
}
|
||||
|
||||
export async function executeScheduleCreateItemTool(args, {
|
||||
userId,
|
||||
scheduleService,
|
||||
timezone = 'Asia/Shanghai',
|
||||
} = {}) {
|
||||
if (!scheduleService) throw new Error('scheduleService 不可用');
|
||||
const tz = args.timezone ?? timezone;
|
||||
const startAt = resolveScheduleTimestamp({
|
||||
epochMs: args.startAt,
|
||||
localString: args.startLocal,
|
||||
timezone: tz,
|
||||
fieldName: '开始时间',
|
||||
});
|
||||
const endAt = resolveScheduleTimestamp({
|
||||
epochMs: args.endAt,
|
||||
localString: args.endLocal,
|
||||
timezone: tz,
|
||||
fieldName: '结束时间',
|
||||
});
|
||||
const dueAt = resolveScheduleTimestamp({
|
||||
epochMs: args.dueAt,
|
||||
localString: args.dueLocal,
|
||||
timezone: tz,
|
||||
fieldName: '截止时间',
|
||||
});
|
||||
const item = await scheduleService.createItem({
|
||||
userId,
|
||||
kind: args.kind,
|
||||
title: args.title,
|
||||
description: args.description ?? null,
|
||||
startAt,
|
||||
endAt,
|
||||
dueAt,
|
||||
allDay: Boolean(args.allDay),
|
||||
timezone: tz,
|
||||
location: args.location ?? null,
|
||||
sourceChannel: args.sourceChannel ?? 'agent',
|
||||
sourceMessageId: args.sourceMessageId ?? null,
|
||||
sourceText: args.sourceText ?? null,
|
||||
metadata: args.metadata ?? { source: 'agent_tool' },
|
||||
});
|
||||
let remindAt = resolveScheduleTimestamp({
|
||||
epochMs: args.remindAt,
|
||||
localString: args.remindLocal,
|
||||
timezone: tz,
|
||||
fieldName: '提醒时间',
|
||||
});
|
||||
if (
|
||||
remindAt == null
|
||||
&& shouldAutoCreateReminderAtStart({
|
||||
title: args.title,
|
||||
description: args.description,
|
||||
startAt,
|
||||
noReminder: Boolean(args.noReminder),
|
||||
})
|
||||
) {
|
||||
remindAt = startAt;
|
||||
}
|
||||
if (remindAt != null) {
|
||||
const reminder = await scheduleService.createReminder({
|
||||
userId,
|
||||
itemId: item.id,
|
||||
remindAt,
|
||||
offsetMinutes: args.offsetMinutes ?? null,
|
||||
channel: args.channel ?? 'wechat',
|
||||
});
|
||||
return { item, reminder };
|
||||
}
|
||||
return { item };
|
||||
}
|
||||
|
||||
export async function executeScheduleCreateReminderTool(args, {
|
||||
userId,
|
||||
scheduleService,
|
||||
timezone = 'Asia/Shanghai',
|
||||
} = {}) {
|
||||
if (!scheduleService) throw new Error('scheduleService 不可用');
|
||||
const tz = args.timezone ?? timezone;
|
||||
const remindAt = resolveScheduleTimestamp({
|
||||
epochMs: args.remindAt,
|
||||
localString: args.remindLocal,
|
||||
timezone: tz,
|
||||
fieldName: '提醒时间',
|
||||
});
|
||||
if (remindAt == null) throw new Error('缺少提醒时间 remindLocal 或 remindAt');
|
||||
const reminder = await scheduleService.createReminder({
|
||||
userId,
|
||||
itemId: args.itemId,
|
||||
remindAt,
|
||||
offsetMinutes: args.offsetMinutes ?? null,
|
||||
channel: args.channel ?? 'wechat',
|
||||
});
|
||||
return reminder;
|
||||
}
|
||||
|
||||
export async function commitAgentSideEffectTool(toolName, toolArgs, services) {
|
||||
const name = String(toolName ?? '').trim();
|
||||
if (name === 'scheduled_task_create') {
|
||||
const task = await executeScheduledTaskCreateTool(toolArgs, services);
|
||||
return { kind: 'scheduled_task', task };
|
||||
}
|
||||
if (name === 'schedule_create_item') {
|
||||
const result = await executeScheduleCreateItemTool(toolArgs, services);
|
||||
return { kind: 'schedule_item', ...result };
|
||||
}
|
||||
if (name === 'schedule_create_reminder') {
|
||||
const reminder = await executeScheduleCreateReminderTool(toolArgs, services);
|
||||
return { kind: 'schedule_reminder', reminder };
|
||||
}
|
||||
throw new Error(`不支持的 agent 副作用工具:${name}`);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { formatIntentActionCard } from './intent-action-card.mjs';
|
||||
import { isIntentTransactionEnabled } from './intent-transaction-config.mjs';
|
||||
|
||||
export function isSideEffectConfirmGateEnabled(env = process.env) {
|
||||
return isIntentTransactionEnabled(env);
|
||||
}
|
||||
|
||||
function buildScheduledTaskConfirmCard(args) {
|
||||
const title = String(args.title ?? args.taskSpec ?? '').trim() || '定时自动任务';
|
||||
const trigger = args.recurrence === 'once'
|
||||
? { at: args.runAtLocal ?? null }
|
||||
: { hour: args.hour, minute: args.minute ?? 0 };
|
||||
const frequency = args.recurrence === 'weekly'
|
||||
? '每周自动执行'
|
||||
: args.recurrence === 'once'
|
||||
? null
|
||||
: '每天自动执行';
|
||||
return formatIntentActionCard({
|
||||
title: title.slice(0, 80),
|
||||
layer: 'L2',
|
||||
actionLevel: 2,
|
||||
trigger,
|
||||
actions: ['到点自动执行任务', '推送执行结果到微信'],
|
||||
frequency,
|
||||
});
|
||||
}
|
||||
|
||||
function buildScheduleItemConfirmCard(args) {
|
||||
const title = String(args.title ?? '').trim() || '待办/日程';
|
||||
const remindLocal = args.remindLocal ?? args.startLocal ?? null;
|
||||
const trigger = remindLocal ? { at: remindLocal } : { hour: null, minute: 0 };
|
||||
const actions = args.remindLocal || args.remindAt || args.startLocal || args.startAt
|
||||
? ['到点发送微信提醒']
|
||||
: ['记录到待办列表'];
|
||||
return formatIntentActionCard({
|
||||
title: title.slice(0, 80),
|
||||
layer: 'L1',
|
||||
actionLevel: 1,
|
||||
trigger,
|
||||
actions,
|
||||
});
|
||||
}
|
||||
|
||||
function buildScheduleReminderConfirmCard(args) {
|
||||
const trigger = args.remindLocal ? { at: args.remindLocal } : { hour: null, minute: 0 };
|
||||
return formatIntentActionCard({
|
||||
title: '日程提醒',
|
||||
layer: 'L1',
|
||||
actionLevel: 1,
|
||||
trigger,
|
||||
actions: ['到点发送微信提醒'],
|
||||
});
|
||||
}
|
||||
|
||||
export async function requireUserConfirmForSideEffectTool({
|
||||
userId,
|
||||
toolName,
|
||||
toolArgs,
|
||||
intentDraftService,
|
||||
env = process.env,
|
||||
layer = 'L2',
|
||||
actionLevel = 2,
|
||||
title = null,
|
||||
cardText = null,
|
||||
} = {}) {
|
||||
if (!isSideEffectConfirmGateEnabled(env)) {
|
||||
return { proceed: true };
|
||||
}
|
||||
if (!intentDraftService || !userId) {
|
||||
return { proceed: true };
|
||||
}
|
||||
|
||||
const safeToolName = String(toolName ?? '').trim();
|
||||
const args = { ...(toolArgs ?? {}) };
|
||||
const resolvedCardText = cardText
|
||||
?? (safeToolName === 'scheduled_task_create'
|
||||
? buildScheduledTaskConfirmCard(args)
|
||||
: safeToolName === 'schedule_create_item'
|
||||
? buildScheduleItemConfirmCard(args)
|
||||
: safeToolName === 'schedule_create_reminder'
|
||||
? buildScheduleReminderConfirmCard(args)
|
||||
: formatIntentActionCard({ title: title ?? '待确认操作', layer, actionLevel }));
|
||||
const resolvedTitle = String(title ?? args.title ?? args.taskSpec ?? resolvedCardText.split('\n')[0] ?? '待确认任务').trim();
|
||||
|
||||
const draft = await intentDraftService.createDraft({
|
||||
userId,
|
||||
layer,
|
||||
draftType: 'agent_tool',
|
||||
actionLevel,
|
||||
title: resolvedTitle.slice(0, 80),
|
||||
payload: {
|
||||
toolName: safeToolName,
|
||||
toolArgs: args,
|
||||
sourceChannel: 'agent',
|
||||
sourceMessageId: args.sourceMessageId ?? null,
|
||||
sourceText: args.sourceText ?? null,
|
||||
},
|
||||
cardText: resolvedCardText,
|
||||
sourceChannel: 'agent',
|
||||
sourceMessageId: args.sourceMessageId ?? null,
|
||||
sourceText: args.sourceText ?? null,
|
||||
});
|
||||
|
||||
return {
|
||||
proceed: false,
|
||||
response: {
|
||||
status: 'needs_user_confirmation',
|
||||
draftId: draft.id,
|
||||
cardText: resolvedCardText,
|
||||
instruction:
|
||||
'这是需要用户确认的副作用操作。请把 cardText 原样发给用户,并请用户回复「确认」后再写入;在用户确认前不要声称已成功设置。',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { commitIntentDraft } from './intent-transaction-commit.mjs';
|
||||
import { requireUserConfirmForSideEffectTool } from './intent-tool-confirm-gate.mjs';
|
||||
|
||||
function createDraftStore() {
|
||||
let pending = null;
|
||||
return {
|
||||
async getPendingDraft(userId) {
|
||||
return pending?.userId === userId ? pending : null;
|
||||
},
|
||||
async createDraft(payload) {
|
||||
pending = {
|
||||
id: 'draft-tool-1',
|
||||
status: 'draft',
|
||||
...payload,
|
||||
};
|
||||
return pending;
|
||||
},
|
||||
async cancelDraft(id, userId) {
|
||||
if (pending?.id === id && pending.userId === userId) pending = null;
|
||||
return { id, status: 'cancelled' };
|
||||
},
|
||||
async markDraftCommitted(id, userId, committedRef) {
|
||||
pending = { ...pending, id, userId, status: 'committed', committedRef };
|
||||
return pending;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('requireUserConfirmForSideEffectTool creates draft instead of writing', async () => {
|
||||
const drafts = createDraftStore();
|
||||
const gate = await requireUserConfirmForSideEffectTool({
|
||||
userId: 'user-1',
|
||||
toolName: 'scheduled_task_create',
|
||||
toolArgs: {
|
||||
taskSpec: '做今日新闻页面',
|
||||
recurrence: 'daily',
|
||||
hour: 6,
|
||||
minute: 0,
|
||||
},
|
||||
intentDraftService: drafts,
|
||||
env: { H5_INTENT_TRANSACTION_ENABLED: '1' },
|
||||
});
|
||||
assert.equal(gate.proceed, false);
|
||||
assert.equal(gate.response.status, 'needs_user_confirmation');
|
||||
assert.match(gate.response.cardText, /确认/u);
|
||||
const pending = await drafts.getPendingDraft('user-1');
|
||||
assert.equal(pending.draftType, 'agent_tool');
|
||||
assert.equal(pending.payload.toolName, 'scheduled_task_create');
|
||||
});
|
||||
|
||||
test('requireUserConfirmForSideEffectTool proceeds when feature disabled', async () => {
|
||||
const gate = await requireUserConfirmForSideEffectTool({
|
||||
userId: 'user-1',
|
||||
toolName: 'scheduled_task_create',
|
||||
toolArgs: { taskSpec: '做新闻页面', hour: 6 },
|
||||
intentDraftService: createDraftStore(),
|
||||
env: { H5_INTENT_TRANSACTION_ENABLED: '0' },
|
||||
});
|
||||
assert.equal(gate.proceed, true);
|
||||
});
|
||||
|
||||
test('commitIntentDraft commits agent_tool scheduled task draft', async () => {
|
||||
const created = [];
|
||||
const committed = await commitIntentDraft({
|
||||
draft: {
|
||||
status: 'draft',
|
||||
draftType: 'agent_tool',
|
||||
title: '新闻页面',
|
||||
payload: {
|
||||
toolName: 'scheduled_task_create',
|
||||
toolArgs: {
|
||||
taskSpec: '做今日新闻页面',
|
||||
recurrence: 'daily',
|
||||
hour: 6,
|
||||
minute: 0,
|
||||
timezone: 'Asia/Shanghai',
|
||||
},
|
||||
},
|
||||
},
|
||||
userId: 'user-1',
|
||||
scheduledTaskService: {
|
||||
async createTask(input) {
|
||||
created.push(input);
|
||||
return { id: 'task-1', ...input };
|
||||
},
|
||||
},
|
||||
scheduleService: null,
|
||||
timezone: 'Asia/Shanghai',
|
||||
env: { H5_INTENT_TRANSACTION_ENABLED: '1' },
|
||||
});
|
||||
assert.equal(committed.kind, 'scheduled_task');
|
||||
assert.equal(created.length, 1);
|
||||
assert.equal(created[0].hour, 6);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { commitAgentSideEffectTool } from './agent-side-effect-tools.mjs';
|
||||
import { resolveScheduleTimestamp } from './schedule-time.mjs';
|
||||
import { buildScheduledTaskCreatePayload } from './scheduled-task-intent.mjs';
|
||||
import { isUnifiedTasksEnabled } from './task-unified-config.mjs';
|
||||
@@ -136,5 +137,23 @@ export async function commitIntentDraft({
|
||||
return committed;
|
||||
}
|
||||
|
||||
if (kind === 'agent_tool') {
|
||||
const committed = await commitAgentSideEffectTool(payload.toolName, payload.toolArgs, {
|
||||
userId,
|
||||
scheduleService,
|
||||
scheduledTaskService,
|
||||
timezone,
|
||||
env,
|
||||
});
|
||||
await maybeSyncUnifiedTask({
|
||||
taskUnifiedService,
|
||||
userId,
|
||||
kind: committed.kind === 'scheduled_task' ? 'scheduled_task' : kind,
|
||||
committed,
|
||||
env,
|
||||
});
|
||||
return committed;
|
||||
}
|
||||
|
||||
throw new Error(`不支持的草稿类型:${kind}`);
|
||||
}
|
||||
|
||||
+69
-99
@@ -18,11 +18,12 @@ import mysql from 'mysql2/promise';
|
||||
import { createScheduleService } from './schedule-service.mjs';
|
||||
import { createScheduledTaskService } from './scheduled-task-service.mjs';
|
||||
import {
|
||||
isScheduledTaskWorkerEnabled,
|
||||
scheduledTaskWorkerDisabledMessage,
|
||||
} from './scheduled-task-worker-config.mjs';
|
||||
import { resolveScheduleTimestamp } from './schedule-time.mjs';
|
||||
import { shouldAutoCreateReminderAtStart } from './schedule-service.mjs';
|
||||
executeScheduleCreateItemTool,
|
||||
executeScheduleCreateReminderTool,
|
||||
executeScheduledTaskCreateTool,
|
||||
} from './agent-side-effect-tools.mjs';
|
||||
import { createIntentDraftService } from './intent-draft-service.mjs';
|
||||
import { requireUserConfirmForSideEffectTool } from './intent-tool-confirm-gate.mjs';
|
||||
import { renderLongImage } from './mindspace-long-image.mjs';
|
||||
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
||||
import { writePageAccessPolicy, readPageAccessPolicy } from './page-data-policy-store.mjs';
|
||||
@@ -490,6 +491,7 @@ const ALL_TOOLS = [
|
||||
let quotaPool = null;
|
||||
let scheduleService = null;
|
||||
let scheduledTaskService = null;
|
||||
let intentDraftService = null;
|
||||
let userDataSpaceService = null;
|
||||
|
||||
function isQuotaSyncConfigured() {
|
||||
@@ -549,6 +551,39 @@ function getScheduledTaskService() {
|
||||
return scheduledTaskService;
|
||||
}
|
||||
|
||||
function getIntentDraftService() {
|
||||
if (!PRIVATE_DATA_USER_ID) return null;
|
||||
const pool = getQuotaPool();
|
||||
if (!pool || !isScheduleConfigured()) return null;
|
||||
if (!intentDraftService) {
|
||||
intentDraftService = createIntentDraftService(pool);
|
||||
}
|
||||
return intentDraftService;
|
||||
}
|
||||
|
||||
async function gateSideEffectTool(toolName, args, {
|
||||
layer = 'L2',
|
||||
actionLevel = 2,
|
||||
title = null,
|
||||
cardText = null,
|
||||
} = {}) {
|
||||
const gate = await requireUserConfirmForSideEffectTool({
|
||||
userId: PRIVATE_DATA_USER_ID,
|
||||
toolName,
|
||||
toolArgs: args,
|
||||
intentDraftService: getIntentDraftService(),
|
||||
layer,
|
||||
actionLevel,
|
||||
title,
|
||||
cardText,
|
||||
});
|
||||
if (gate.proceed) return null;
|
||||
return [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(gate.response, null, 2),
|
||||
}];
|
||||
}
|
||||
|
||||
if (isScheduleConfigured()) {
|
||||
ALL_TOOLS.push(
|
||||
{
|
||||
@@ -1127,85 +1162,31 @@ async function callTool(name, args) {
|
||||
];
|
||||
}
|
||||
case 'schedule_create_item': {
|
||||
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
|
||||
const startAt = resolveScheduleTimestamp({
|
||||
epochMs: args.startAt,
|
||||
localString: args.startLocal,
|
||||
timezone,
|
||||
fieldName: '开始时间',
|
||||
});
|
||||
const endAt = resolveScheduleTimestamp({
|
||||
epochMs: args.endAt,
|
||||
localString: args.endLocal,
|
||||
timezone,
|
||||
fieldName: '结束时间',
|
||||
});
|
||||
const dueAt = resolveScheduleTimestamp({
|
||||
epochMs: args.dueAt,
|
||||
localString: args.dueLocal,
|
||||
timezone,
|
||||
fieldName: '截止时间',
|
||||
});
|
||||
const item = await getScheduleService().createItem({
|
||||
userId: PRIVATE_DATA_USER_ID,
|
||||
kind: args.kind,
|
||||
const gated = await gateSideEffectTool('schedule_create_item', args, {
|
||||
layer: 'L1',
|
||||
actionLevel: 1,
|
||||
title: args.title,
|
||||
description: args.description ?? null,
|
||||
startAt,
|
||||
endAt,
|
||||
dueAt,
|
||||
allDay: Boolean(args.allDay),
|
||||
timezone,
|
||||
location: args.location ?? null,
|
||||
sourceChannel: 'agent',
|
||||
sourceMessageId: args.sourceMessageId ?? null,
|
||||
sourceText: args.sourceText ?? null,
|
||||
metadata: { source: 'schedule_assistant_skill' },
|
||||
});
|
||||
let remindAt = resolveScheduleTimestamp({
|
||||
epochMs: args.remindAt,
|
||||
localString: args.remindLocal,
|
||||
if (gated) return gated;
|
||||
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
|
||||
const result = await executeScheduleCreateItemTool(args, {
|
||||
userId: PRIVATE_DATA_USER_ID,
|
||||
scheduleService: getScheduleService(),
|
||||
timezone,
|
||||
fieldName: '提醒时间',
|
||||
});
|
||||
if (
|
||||
remindAt == null
|
||||
&& shouldAutoCreateReminderAtStart({
|
||||
title: args.title,
|
||||
description: args.description,
|
||||
startAt,
|
||||
noReminder: Boolean(args.noReminder),
|
||||
})
|
||||
) {
|
||||
remindAt = startAt;
|
||||
}
|
||||
if (remindAt != null) {
|
||||
const reminder = await getScheduleService().createReminder({
|
||||
userId: PRIVATE_DATA_USER_ID,
|
||||
itemId: item.id,
|
||||
remindAt,
|
||||
offsetMinutes: args.offsetMinutes ?? null,
|
||||
channel: args.channel ?? 'wechat',
|
||||
});
|
||||
return [{ type: 'text', text: JSON.stringify({ item, reminder }, null, 2) }];
|
||||
}
|
||||
return [{ type: 'text', text: JSON.stringify(item, null, 2) }];
|
||||
return [{ type: 'text', text: JSON.stringify(result, null, 2) }];
|
||||
}
|
||||
case 'schedule_create_reminder': {
|
||||
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
|
||||
const remindAt = resolveScheduleTimestamp({
|
||||
epochMs: args.remindAt,
|
||||
localString: args.remindLocal,
|
||||
timezone,
|
||||
fieldName: '提醒时间',
|
||||
const gated = await gateSideEffectTool('schedule_create_reminder', args, {
|
||||
layer: 'L1',
|
||||
actionLevel: 1,
|
||||
});
|
||||
if (remindAt == null) throw new Error('缺少提醒时间 remindLocal 或 remindAt');
|
||||
const reminder = await getScheduleService().createReminder({
|
||||
if (gated) return gated;
|
||||
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
|
||||
const reminder = await executeScheduleCreateReminderTool(args, {
|
||||
userId: PRIVATE_DATA_USER_ID,
|
||||
itemId: args.itemId,
|
||||
remindAt,
|
||||
offsetMinutes: args.offsetMinutes ?? null,
|
||||
channel: args.channel ?? 'wechat',
|
||||
scheduleService: getScheduleService(),
|
||||
timezone,
|
||||
});
|
||||
return [{ type: 'text', text: JSON.stringify(reminder, null, 2) }];
|
||||
}
|
||||
@@ -1220,32 +1201,21 @@ async function callTool(name, args) {
|
||||
return [{ type: 'text', text: JSON.stringify(items, null, 2) }];
|
||||
}
|
||||
case 'scheduled_task_create': {
|
||||
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
|
||||
const task = await getScheduledTaskService().createTask({
|
||||
userId: PRIVATE_DATA_USER_ID,
|
||||
title: args.title ?? null,
|
||||
taskSpec: args.taskSpec,
|
||||
recurrence: args.recurrence ?? 'daily',
|
||||
runAtLocal: args.runAtLocal ?? null,
|
||||
hour: args.hour ?? null,
|
||||
minute: args.minute ?? 0,
|
||||
weekday: args.weekday ?? null,
|
||||
timezone,
|
||||
notifyChannel: args.notifyChannel ?? 'both',
|
||||
sourceChannel: 'agent',
|
||||
sourceMessageId: args.sourceMessageId ?? null,
|
||||
sourceText: args.sourceText ?? null,
|
||||
const gated = await gateSideEffectTool('scheduled_task_create', args, {
|
||||
layer: 'L2',
|
||||
actionLevel: 2,
|
||||
title: args.title ?? args.taskSpec,
|
||||
});
|
||||
if (gated) return gated;
|
||||
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
|
||||
const task = await executeScheduledTaskCreateTool(args, {
|
||||
userId: PRIVATE_DATA_USER_ID,
|
||||
scheduledTaskService: getScheduledTaskService(),
|
||||
timezone,
|
||||
});
|
||||
const workerWarning = isScheduledTaskWorkerEnabled()
|
||||
? null
|
||||
: scheduledTaskWorkerDisabledMessage();
|
||||
return [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
workerWarning ? { ...task, workerWarning } : task,
|
||||
null,
|
||||
2,
|
||||
),
|
||||
text: JSON.stringify(task, null, 2),
|
||||
}];
|
||||
}
|
||||
case 'scheduled_task_list': {
|
||||
|
||||
@@ -94,22 +94,35 @@ async function main() {
|
||||
|
||||
const drafts = createDraftStore();
|
||||
const scheduleService = createScheduleService();
|
||||
const card = await handleWechatIntentTransaction({
|
||||
const passthrough = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'verify-1', msgType: 'text' },
|
||||
user: { userId: 'verify-user' },
|
||||
intentDraftService: drafts,
|
||||
scheduleService,
|
||||
env,
|
||||
});
|
||||
if (card?.includes('我准备执行') && card.includes('确认')) {
|
||||
pass('creates action card for timed reminder');
|
||||
} else {
|
||||
fail('creates action card for timed reminder', card);
|
||||
}
|
||||
if (passthrough === null) pass('new messages pass through to goose');
|
||||
else fail('new messages pass through to goose', passthrough);
|
||||
|
||||
await drafts.createDraft({
|
||||
userId: 'verify-user',
|
||||
layer: 'L1',
|
||||
draftType: 'timed_reminder',
|
||||
actionLevel: 1,
|
||||
title: '项目计划例会',
|
||||
payload: {
|
||||
title: '项目计划例会',
|
||||
remindLocal: '2026-08-28 14:30',
|
||||
hour: 14,
|
||||
minute: 30,
|
||||
recurrence: 'once',
|
||||
sourceChannel: 'agent',
|
||||
},
|
||||
cardText: '确认卡片',
|
||||
});
|
||||
const pending = await drafts.getPendingDraft('verify-user');
|
||||
if (pending?.draftType === 'timed_reminder') pass('persists pending draft');
|
||||
else fail('persists pending draft', JSON.stringify(pending));
|
||||
if (pending?.draftType === 'timed_reminder') pass('pending draft can be committed after tool gate');
|
||||
else fail('pending draft can be committed after tool gate', JSON.stringify(pending));
|
||||
|
||||
const committed = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '确认', msgId: 'verify-2', msgType: 'text' },
|
||||
@@ -131,11 +144,18 @@ async function main() {
|
||||
scheduleService,
|
||||
env,
|
||||
});
|
||||
if (slotFill?.includes('补充') && !slotFill.includes('我准备执行')) {
|
||||
pass('slot fill does not create confirmable draft');
|
||||
} else {
|
||||
fail('slot fill does not create confirmable draft', slotFill);
|
||||
}
|
||||
if (slotFill === null) pass('incomplete requests pass through to goose');
|
||||
else fail('incomplete requests pass through to goose', slotFill);
|
||||
|
||||
const pagePassthrough = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '我想帮我做一个页面,页面上每天自动更新天气', msgId: 'verify-page', msgType: 'text' },
|
||||
user: { userId: 'verify-user-page' },
|
||||
intentDraftService: createDraftStore(),
|
||||
scheduleService,
|
||||
env,
|
||||
});
|
||||
if (pagePassthrough === null) pass('page delivery with auto refresh passes through to goose');
|
||||
else fail('page delivery with auto refresh passes through to goose', pagePassthrough);
|
||||
|
||||
const disabled = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '下午2点提醒我', msgId: 'verify-4', msgType: 'text' },
|
||||
|
||||
@@ -1,102 +1,13 @@
|
||||
import { classifyUserIntent, inferActionLevel } from '../../intent-classifier.mjs';
|
||||
import { commitIntentDraft } from '../../intent-transaction-commit.mjs';
|
||||
import {
|
||||
formatDraftCancelledReply,
|
||||
formatDraftCommittedReply,
|
||||
formatIntentActionCard,
|
||||
parseDraftUserReply,
|
||||
} from '../../intent-action-card.mjs';
|
||||
import { formatQueryGuardReply } from '../../intent-query-guard.mjs';
|
||||
import { isIntentTransactionEnabled } from '../../intent-transaction-config.mjs';
|
||||
import { buildScheduledTaskCreatePayload } from '../../scheduled-task-intent.mjs';
|
||||
import { formatScheduledTaskCreateReply } from '../../scheduled-task-intent.mjs';
|
||||
import { isIntentTransactionEnabled } from '../../intent-transaction-config.mjs';
|
||||
import { isScheduledTaskWorkerEnabled, scheduledTaskWorkerDisabledMessage } from '../../scheduled-task-worker-config.mjs';
|
||||
|
||||
function buildDraftPayload(classification, { intent, user, timezone }) {
|
||||
const { kind, detail } = classification;
|
||||
const base = {
|
||||
sourceChannel: 'wechat',
|
||||
sourceMessageId: intent.msgId ?? null,
|
||||
sourceText: intent.agentText,
|
||||
timezone,
|
||||
};
|
||||
|
||||
if (kind === 'timed_reminder') {
|
||||
return {
|
||||
...base,
|
||||
title: detail.title,
|
||||
remindLocal: detail.remindLocal,
|
||||
hour: detail.hour,
|
||||
minute: detail.minute,
|
||||
recurrence: detail.recurrence ?? 'once',
|
||||
};
|
||||
}
|
||||
if (kind === 'create_todo') {
|
||||
return { ...base, title: detail.title };
|
||||
}
|
||||
if (kind === 'create_daily_todo_digest') {
|
||||
return { ...base, hour: detail.hour, minute: detail.minute ?? 0 };
|
||||
}
|
||||
if (kind === 'create_balance_alert') {
|
||||
return { ...base, thresholdCents: detail.thresholdCents };
|
||||
}
|
||||
if (kind === 'scheduled_task') {
|
||||
return {
|
||||
...base,
|
||||
intentDetail: detail,
|
||||
createPayload: buildScheduledTaskCreatePayload(detail, {
|
||||
userId: user.userId,
|
||||
sourceChannel: 'wechat',
|
||||
sourceMessageId: intent.msgId ?? null,
|
||||
sourceText: intent.agentText,
|
||||
timezone,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function buildCard(classification, draftPayload, actionLevel, text) {
|
||||
const { layer, kind, detail, clarify } = classification;
|
||||
if (kind === 'clarify' || classification.layer === 'ambiguous') {
|
||||
return formatIntentActionCard({ clarify: ['notify_vs_act'] });
|
||||
}
|
||||
if (clarify?.length) {
|
||||
return formatIntentActionCard({ clarify });
|
||||
}
|
||||
|
||||
const title = detail?.title ?? detail?.taskSpec?.slice?.(0, 80) ?? draftPayload.title ?? '待确认任务';
|
||||
const trigger = detail?.remindLocal
|
||||
? { at: detail.remindLocal }
|
||||
: { hour: detail?.hour, minute: detail?.minute ?? 0 };
|
||||
const frequency = /(?:每天|每日)/u.test(text)
|
||||
? '每天自动执行'
|
||||
: detail?.recurrence === 'weekly'
|
||||
? '每周自动执行'
|
||||
: detail?.recurrence === 'daily'
|
||||
? '每天自动执行'
|
||||
: null;
|
||||
|
||||
const actions = kind === 'scheduled_task'
|
||||
? ['到点自动执行任务', '推送执行结果到微信']
|
||||
: kind === 'timed_reminder'
|
||||
? ['到点发送微信提醒']
|
||||
: kind === 'create_daily_todo_digest'
|
||||
? ['到点推送当天待办摘要']
|
||||
: kind === 'create_balance_alert'
|
||||
? ['余额低于阈值时发送微信提醒']
|
||||
: ['记录到待办列表'];
|
||||
|
||||
return formatIntentActionCard({
|
||||
title,
|
||||
layer,
|
||||
actionLevel,
|
||||
trigger,
|
||||
actions,
|
||||
frequency,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleWechatIntentTransaction({
|
||||
intent,
|
||||
user,
|
||||
@@ -114,108 +25,50 @@ export async function handleWechatIntentTransaction({
|
||||
try {
|
||||
const pending = await intentDraftService.getPendingDraft(user.userId);
|
||||
const replyKind = parseDraftUserReply(text);
|
||||
if (pending && replyKind) {
|
||||
if (replyKind === 'cancel') {
|
||||
await intentDraftService.cancelDraft(pending.id, user.userId);
|
||||
return formatDraftCancelledReply();
|
||||
}
|
||||
if (replyKind === 'modify') {
|
||||
await intentDraftService.cancelDraft(pending.id, user.userId, { reason: 'user_modify' });
|
||||
return '好的,请重新发送完整的安排,例如「下午 2 点半提醒我开项目计划例会」。';
|
||||
}
|
||||
if (replyKind === 'confirm') {
|
||||
if (pending.layer === 'ambiguous' || pending.draftType === 'clarify') {
|
||||
return '我还需要确认:你是要「到点提醒」还是「到点自动执行并交付结果」?请补充后再发「确认」。';
|
||||
}
|
||||
const committed = await commitIntentDraft({
|
||||
draft: pending,
|
||||
userId: user.userId,
|
||||
scheduleService,
|
||||
scheduledTaskService,
|
||||
taskUnifiedService,
|
||||
timezone,
|
||||
env,
|
||||
});
|
||||
await intentDraftService.markDraftCommitted(pending.id, user.userId, committed);
|
||||
if (committed.kind === 'scheduled_task') {
|
||||
const workerWarning = isScheduledTaskWorkerEnabled(env)
|
||||
? null
|
||||
: scheduledTaskWorkerDisabledMessage(env);
|
||||
let reply = formatScheduledTaskCreateReply(committed.task, { workerWarning });
|
||||
reply = `${reply}\n\n(已通过确认卡片写入)`;
|
||||
return reply;
|
||||
}
|
||||
return formatDraftCommittedReply({ title: pending.title, kind: pending.draftType });
|
||||
}
|
||||
if (!pending || !replyKind) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classification = classifyUserIntent(text, { timezone });
|
||||
const { layer, kind, action } = classification;
|
||||
|
||||
if (layer === 'L0') {
|
||||
return formatQueryGuardReply(text, {
|
||||
if (replyKind === 'cancel') {
|
||||
await intentDraftService.cancelDraft(pending.id, user.userId);
|
||||
return formatDraftCancelledReply();
|
||||
}
|
||||
if (replyKind === 'modify') {
|
||||
await intentDraftService.cancelDraft(pending.id, user.userId, { reason: 'user_modify' });
|
||||
return '好的,请重新描述你的需求。';
|
||||
}
|
||||
if (replyKind === 'confirm') {
|
||||
if (pending.layer === 'ambiguous' || pending.draftType === 'clarify') {
|
||||
return '我还需要确认:你是要「到点提醒」还是「到点自动执行并交付结果」?请补充后再发「确认」。';
|
||||
}
|
||||
const committed = await commitIntentDraft({
|
||||
draft: pending,
|
||||
userId: user.userId,
|
||||
scheduleService,
|
||||
scheduledTaskService,
|
||||
taskUnifiedService,
|
||||
userId: user.userId,
|
||||
timezone,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
if (layer === 'L3' || layer === null || kind === 'agent_schedule' || kind === 'agent_automation') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (kind === 'manage') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (layer === 'ambiguous' || kind === 'clarify') {
|
||||
if (classification.clarify?.length && kind !== 'clarify') {
|
||||
return buildCard(classification, {}, inferActionLevel(classification, text), text);
|
||||
await intentDraftService.markDraftCommitted(pending.id, user.userId, committed);
|
||||
if (committed.kind === 'scheduled_task') {
|
||||
const workerWarning = isScheduledTaskWorkerEnabled(env)
|
||||
? null
|
||||
: scheduledTaskWorkerDisabledMessage(env);
|
||||
let reply = formatScheduledTaskCreateReply(committed.task, { workerWarning });
|
||||
reply = `${reply}\n\n(已通过确认卡片写入)`;
|
||||
return reply;
|
||||
}
|
||||
await intentDraftService.createDraft({
|
||||
userId: user.userId,
|
||||
layer: 'ambiguous',
|
||||
draftType: 'clarify',
|
||||
actionLevel: 2,
|
||||
title: '待确认意图',
|
||||
payload: { clarify: ['notify_vs_act'], sourceText: text },
|
||||
cardText: buildCard({ layer: 'ambiguous', kind: 'clarify', clarify: ['notify_vs_act'] }, {}, 2, text),
|
||||
sourceMessageId: intent.msgId ?? null,
|
||||
sourceText: text,
|
||||
});
|
||||
return buildCard({ layer: 'ambiguous', kind: 'clarify', clarify: ['notify_vs_act'] }, {}, 2, text);
|
||||
if (committed.kind === 'schedule_item' || committed.kind === 'schedule_reminder') {
|
||||
return formatDraftCommittedReply({
|
||||
title: pending.title,
|
||||
kind: committed.kind === 'schedule_reminder' ? 'timed_reminder' : 'create_todo',
|
||||
});
|
||||
}
|
||||
return formatDraftCommittedReply({ title: pending.title, kind: pending.draftType });
|
||||
}
|
||||
|
||||
const draftableKinds = new Set([
|
||||
'timed_reminder',
|
||||
'create_todo',
|
||||
'create_daily_todo_digest',
|
||||
'create_balance_alert',
|
||||
'scheduled_task',
|
||||
]);
|
||||
if (!draftableKinds.has(kind)) return null;
|
||||
if (classification.clarify?.length) {
|
||||
return buildCard(classification, {}, inferActionLevel(classification, text), text);
|
||||
}
|
||||
|
||||
const actionLevel = inferActionLevel(classification, text);
|
||||
const payload = buildDraftPayload(classification, { intent, user, timezone });
|
||||
const cardText = buildCard(classification, payload, actionLevel, text);
|
||||
await intentDraftService.createDraft({
|
||||
userId: user.userId,
|
||||
layer,
|
||||
draftType: kind,
|
||||
actionLevel,
|
||||
title: payload.title ?? payload.createPayload?.title ?? classification.detail?.title ?? '待确认任务',
|
||||
payload,
|
||||
cardText,
|
||||
sourceMessageId: intent.msgId ?? null,
|
||||
sourceText: text,
|
||||
});
|
||||
return cardText;
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.warn?.(
|
||||
'[wechat-intent-transaction] failed:',
|
||||
|
||||
@@ -2,8 +2,8 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { handleWechatIntentTransaction } from './intent-transaction.mjs';
|
||||
|
||||
function createDraftStore() {
|
||||
let pending = null;
|
||||
function createDraftStore(initial = null) {
|
||||
let pending = initial;
|
||||
return {
|
||||
async getPendingDraft(userId) {
|
||||
return pending?.userId === userId ? pending : null;
|
||||
@@ -43,9 +43,6 @@ function createScheduleService() {
|
||||
async createReminder(payload) {
|
||||
return { id: 'reminder-1', ...payload };
|
||||
},
|
||||
buildTodoDigestText() {
|
||||
return '今天有 1 条待办。';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,7 +59,7 @@ test('intent transaction returns null when feature disabled', async () => {
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('intent transaction creates action card for simple timed reminder', async () => {
|
||||
test('intent transaction passes through new messages to goose', async () => {
|
||||
const drafts = createDraftStore();
|
||||
const reply = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'm2' },
|
||||
@@ -71,14 +68,41 @@ test('intent transaction creates action card for simple timed reminder', async (
|
||||
scheduleService: createScheduleService(),
|
||||
env: enabledEnv,
|
||||
});
|
||||
assert.match(reply, /我准备执行/);
|
||||
assert.match(reply, /确认/);
|
||||
const pending = await drafts.getPendingDraft('user-1');
|
||||
assert.equal(pending.draftType, 'timed_reminder');
|
||||
assert.equal(reply, null);
|
||||
assert.equal(await drafts.getPendingDraft('user-1'), null);
|
||||
});
|
||||
|
||||
test('intent transaction commits draft after user confirms', async () => {
|
||||
const drafts = createDraftStore();
|
||||
test('intent transaction passes through page generation with auto refresh wording', async () => {
|
||||
const reply = await handleWechatIntentTransaction({
|
||||
intent: {
|
||||
agentText: '我想帮我做一个页面,页面上每天自动更新天气',
|
||||
msgId: 'm-page',
|
||||
},
|
||||
user: { userId: 'user-1' },
|
||||
intentDraftService: createDraftStore(),
|
||||
scheduleService: createScheduleService(),
|
||||
env: enabledEnv,
|
||||
});
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('intent transaction commits pending draft after user confirms', async () => {
|
||||
const drafts = createDraftStore({
|
||||
id: 'draft-1',
|
||||
userId: 'user-1',
|
||||
layer: 'L1',
|
||||
draftType: 'timed_reminder',
|
||||
title: '项目计划例会',
|
||||
status: 'draft',
|
||||
payload: {
|
||||
title: '项目计划例会',
|
||||
remindLocal: '2026-08-28 14:30',
|
||||
hour: 14,
|
||||
minute: 30,
|
||||
recurrence: 'once',
|
||||
sourceChannel: 'agent',
|
||||
},
|
||||
});
|
||||
const scheduleService = createScheduleService();
|
||||
const calls = [];
|
||||
scheduleService.createItem = async (payload) => {
|
||||
@@ -90,13 +114,6 @@ test('intent transaction commits draft after user confirms', async () => {
|
||||
return { id: 'reminder-1', ...payload };
|
||||
};
|
||||
|
||||
await handleWechatIntentTransaction({
|
||||
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'm3' },
|
||||
user: { userId: 'user-1' },
|
||||
intentDraftService: drafts,
|
||||
scheduleService,
|
||||
env: enabledEnv,
|
||||
});
|
||||
const reply = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '确认', msgId: 'm4' },
|
||||
user: { userId: 'user-1' },
|
||||
@@ -108,28 +125,26 @@ test('intent transaction commits draft after user confirms', async () => {
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
|
||||
test('intent transaction asks for slot fill without creating draft', async () => {
|
||||
const drafts = createDraftStore();
|
||||
const reply = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '设置提醒', msgId: 'm6' },
|
||||
user: { userId: 'user-2' },
|
||||
intentDraftService: drafts,
|
||||
scheduleService: createScheduleService(),
|
||||
env: enabledEnv,
|
||||
test('intent transaction cancels pending draft', async () => {
|
||||
const drafts = createDraftStore({
|
||||
id: 'draft-1',
|
||||
userId: 'user-1',
|
||||
layer: 'L2',
|
||||
draftType: 'agent_tool',
|
||||
title: '定时任务',
|
||||
status: 'draft',
|
||||
payload: {
|
||||
toolName: 'scheduled_task_create',
|
||||
toolArgs: { taskSpec: '做新闻页面', hour: 6 },
|
||||
},
|
||||
});
|
||||
assert.match(reply, /补充/);
|
||||
assert.equal(await drafts.getPendingDraft('user-2'), null);
|
||||
});
|
||||
|
||||
test('intent transaction answers query guard without creating draft', async () => {
|
||||
const drafts = createDraftStore();
|
||||
const reply = await handleWechatIntentTransaction({
|
||||
intent: { agentText: '看看我的待办', msgId: 'm5' },
|
||||
intent: { agentText: '取消', msgId: 'm5' },
|
||||
user: { userId: 'user-1' },
|
||||
intentDraftService: drafts,
|
||||
scheduleService: createScheduleService(),
|
||||
env: enabledEnv,
|
||||
});
|
||||
assert.match(reply, /待办/);
|
||||
assert.match(reply, /已取消/);
|
||||
assert.equal(await drafts.getPendingDraft('user-1'), null);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import {
|
||||
buildScheduledTaskCreatePayload,
|
||||
formatScheduledTaskClarification,
|
||||
formatScheduledTaskCreateReply,
|
||||
formatScheduledTaskListReply,
|
||||
isScheduledTaskIntent,
|
||||
parseScheduledTaskIntent,
|
||||
} from '../../scheduled-task-intent.mjs';
|
||||
import {
|
||||
isScheduledTaskWorkerEnabled,
|
||||
scheduledTaskWorkerDisabledMessage,
|
||||
} from '../../scheduled-task-worker-config.mjs';
|
||||
|
||||
const WEEKDAY_NAMES = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
|
||||
@@ -53,28 +46,7 @@ export async function handleWechatScheduledTaskIntent({
|
||||
}
|
||||
|
||||
if (taskIntent.action === 'create_scheduled_task') {
|
||||
if (taskIntent.needsClarification?.length) {
|
||||
return formatScheduledTaskClarification(taskIntent);
|
||||
}
|
||||
const payload = buildScheduledTaskCreatePayload(taskIntent, {
|
||||
userId: user.userId,
|
||||
sourceChannel: 'wechat',
|
||||
sourceMessageId: intent.msgId || null,
|
||||
sourceText: intent.agentText,
|
||||
timezone,
|
||||
});
|
||||
const task = await scheduledTaskService.createTask(payload);
|
||||
const workerWarning = isScheduledTaskWorkerEnabled(env)
|
||||
? null
|
||||
: scheduledTaskWorkerDisabledMessage(env);
|
||||
let reply = formatScheduledTaskCreateReply(task, { workerWarning });
|
||||
if (task.recurrence === 'weekly' && task.weekday != null) {
|
||||
reply = reply.replace(
|
||||
/执行时间:/,
|
||||
`执行时间:${WEEKDAY_NAMES[Number(task.weekday)] ?? ''} `,
|
||||
);
|
||||
}
|
||||
return reply;
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn?.(
|
||||
|
||||
@@ -2,8 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { handleWechatScheduledTaskIntent } from './scheduled-task.mjs';
|
||||
|
||||
test('handleWechatScheduledTaskIntent creates daily automation task', async () => {
|
||||
const created = [];
|
||||
test('handleWechatScheduledTaskIntent passes create requests to goose', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
msgType: 'text',
|
||||
@@ -12,45 +11,15 @@ test('handleWechatScheduledTaskIntent creates daily automation task', async () =
|
||||
},
|
||||
user: { userId: 'user-1' },
|
||||
scheduledTaskService: {
|
||||
async createTask(input) {
|
||||
created.push(input);
|
||||
return {
|
||||
...input,
|
||||
id: 'task-1',
|
||||
nextRunAt: Date.now() + 3600_000,
|
||||
};
|
||||
async createTask() {
|
||||
throw new Error('should not create from inbound regex');
|
||||
},
|
||||
},
|
||||
env: { H5_SCHEDULED_TASK_WORKER_ENABLED: '1' },
|
||||
});
|
||||
|
||||
assert.equal(created.length, 1);
|
||||
assert.equal(created[0].hour, 6);
|
||||
assert.match(created[0].taskSpec, /今日新闻页面/u);
|
||||
assert.match(reply, /已设置每天定时任务/u);
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent warns when worker disabled', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
msgType: 'text',
|
||||
agentText: '每天6点帮我做今日新闻页面',
|
||||
msgId: 'msg-1',
|
||||
},
|
||||
user: { userId: 'user-1' },
|
||||
scheduledTaskService: {
|
||||
async createTask(input) {
|
||||
return { ...input, id: 'task-1', nextRunAt: Date.now() + 3600_000 };
|
||||
},
|
||||
},
|
||||
env: { H5_SCHEDULED_TASK_WORKER_ENABLED: '0', H5_REMINDER_WORKER_ENABLED: '0' },
|
||||
});
|
||||
|
||||
assert.match(reply, /⚠️/u);
|
||||
assert.match(reply, /未开启定时自动执行 Worker/u);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent returns clarification for incomplete request', async () => {
|
||||
test('handleWechatScheduledTaskIntent passes incomplete create requests to goose', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
msgType: 'text',
|
||||
@@ -60,12 +29,11 @@ test('handleWechatScheduledTaskIntent returns clarification for incomplete reque
|
||||
user: { userId: 'user-1' },
|
||||
scheduledTaskService: {
|
||||
async createTask() {
|
||||
throw new Error('should not create');
|
||||
throw new Error('should not create from inbound regex');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.match(reply, /执行时间和任务内容/u);
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent ignores non automation text', async () => {
|
||||
@@ -84,3 +52,20 @@ test('handleWechatScheduledTaskIntent ignores non automation text', async () =>
|
||||
});
|
||||
assert.equal(reply, null);
|
||||
});
|
||||
|
||||
test('handleWechatScheduledTaskIntent still lists tasks', async () => {
|
||||
const reply = await handleWechatScheduledTaskIntent({
|
||||
intent: {
|
||||
msgType: 'text',
|
||||
agentText: '看看我的定时自动任务',
|
||||
msgId: 'msg-1',
|
||||
},
|
||||
user: { userId: 'user-1' },
|
||||
scheduledTaskService: {
|
||||
async listTasks() {
|
||||
return [{ title: '新闻页面', recurrence: 'daily', hour: 6, minute: 0, timezone: 'Asia/Shanghai' }];
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.match(reply, /新闻页面/u);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
isWechatPageEditText,
|
||||
isWechatPageRetryText,
|
||||
} from './page-continuation.mjs';
|
||||
import { shouldUseScheduledTaskAutomation } from '../../scheduled-task-intent.mjs';
|
||||
|
||||
export const PAGE_GENERATE_PATTERN =
|
||||
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu;
|
||||
@@ -39,7 +38,6 @@ export const CONNECTIVITY_TEST_PATTERN = /^(测试\s*\d*|test\s*\d*)[!!。.\s]
|
||||
export function isPageGenerateText(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
if (shouldUseScheduledTaskAutomation(normalized)) return false;
|
||||
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
|
||||
return (
|
||||
PAGE_GENERATE_PATTERN.test(normalized)
|
||||
|
||||
Reference in New Issue
Block a user