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

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:
john
2026-08-28 22:16:23 +08:00
parent fef2df119d
commit e13df82021
11 changed files with 586 additions and 398 deletions
+146
View File
@@ -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}`);
}
+114
View File
@@ -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 原样发给用户,并请用户回复「确认」后再写入;在用户确认前不要声称已成功设置。',
},
};
}
+96
View File
@@ -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);
});
+19
View File
@@ -1,3 +1,4 @@
import { commitAgentSideEffectTool } from './agent-side-effect-tools.mjs';
import { resolveScheduleTimestamp } from './schedule-time.mjs'; import { resolveScheduleTimestamp } from './schedule-time.mjs';
import { buildScheduledTaskCreatePayload } from './scheduled-task-intent.mjs'; import { buildScheduledTaskCreatePayload } from './scheduled-task-intent.mjs';
import { isUnifiedTasksEnabled } from './task-unified-config.mjs'; import { isUnifiedTasksEnabled } from './task-unified-config.mjs';
@@ -136,5 +137,23 @@ export async function commitIntentDraft({
return committed; 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}`); throw new Error(`不支持的草稿类型:${kind}`);
} }
+69 -99
View File
@@ -18,11 +18,12 @@ import mysql from 'mysql2/promise';
import { createScheduleService } from './schedule-service.mjs'; import { createScheduleService } from './schedule-service.mjs';
import { createScheduledTaskService } from './scheduled-task-service.mjs'; import { createScheduledTaskService } from './scheduled-task-service.mjs';
import { import {
isScheduledTaskWorkerEnabled, executeScheduleCreateItemTool,
scheduledTaskWorkerDisabledMessage, executeScheduleCreateReminderTool,
} from './scheduled-task-worker-config.mjs'; executeScheduledTaskCreateTool,
import { resolveScheduleTimestamp } from './schedule-time.mjs'; } from './agent-side-effect-tools.mjs';
import { shouldAutoCreateReminderAtStart } from './schedule-service.mjs'; import { createIntentDraftService } from './intent-draft-service.mjs';
import { requireUserConfirmForSideEffectTool } from './intent-tool-confirm-gate.mjs';
import { renderLongImage } from './mindspace-long-image.mjs'; import { renderLongImage } from './mindspace-long-image.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs'; import { createUserDataSpaceService } from './user-data-space-service.mjs';
import { writePageAccessPolicy, readPageAccessPolicy } from './page-data-policy-store.mjs'; import { writePageAccessPolicy, readPageAccessPolicy } from './page-data-policy-store.mjs';
@@ -490,6 +491,7 @@ const ALL_TOOLS = [
let quotaPool = null; let quotaPool = null;
let scheduleService = null; let scheduleService = null;
let scheduledTaskService = null; let scheduledTaskService = null;
let intentDraftService = null;
let userDataSpaceService = null; let userDataSpaceService = null;
function isQuotaSyncConfigured() { function isQuotaSyncConfigured() {
@@ -549,6 +551,39 @@ function getScheduledTaskService() {
return scheduledTaskService; 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()) { if (isScheduleConfigured()) {
ALL_TOOLS.push( ALL_TOOLS.push(
{ {
@@ -1127,85 +1162,31 @@ async function callTool(name, args) {
]; ];
} }
case 'schedule_create_item': { case 'schedule_create_item': {
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai'; const gated = await gateSideEffectTool('schedule_create_item', args, {
const startAt = resolveScheduleTimestamp({ layer: 'L1',
epochMs: args.startAt, actionLevel: 1,
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,
title: args.title, 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({ if (gated) return gated;
epochMs: args.remindAt, const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
localString: args.remindLocal, const result = await executeScheduleCreateItemTool(args, {
userId: PRIVATE_DATA_USER_ID,
scheduleService: getScheduleService(),
timezone, timezone,
fieldName: '提醒时间',
}); });
if ( return [{ type: 'text', text: JSON.stringify(result, null, 2) }];
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) }];
} }
case 'schedule_create_reminder': { case 'schedule_create_reminder': {
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai'; const gated = await gateSideEffectTool('schedule_create_reminder', args, {
const remindAt = resolveScheduleTimestamp({ layer: 'L1',
epochMs: args.remindAt, actionLevel: 1,
localString: args.remindLocal,
timezone,
fieldName: '提醒时间',
}); });
if (remindAt == null) throw new Error('缺少提醒时间 remindLocal 或 remindAt'); if (gated) return gated;
const reminder = await getScheduleService().createReminder({ const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
const reminder = await executeScheduleCreateReminderTool(args, {
userId: PRIVATE_DATA_USER_ID, userId: PRIVATE_DATA_USER_ID,
itemId: args.itemId, scheduleService: getScheduleService(),
remindAt, timezone,
offsetMinutes: args.offsetMinutes ?? null,
channel: args.channel ?? 'wechat',
}); });
return [{ type: 'text', text: JSON.stringify(reminder, null, 2) }]; 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) }]; return [{ type: 'text', text: JSON.stringify(items, null, 2) }];
} }
case 'scheduled_task_create': { case 'scheduled_task_create': {
const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai'; const gated = await gateSideEffectTool('scheduled_task_create', args, {
const task = await getScheduledTaskService().createTask({ layer: 'L2',
userId: PRIVATE_DATA_USER_ID, actionLevel: 2,
title: args.title ?? null, title: args.title ?? args.taskSpec,
taskSpec: args.taskSpec, });
recurrence: args.recurrence ?? 'daily', if (gated) return gated;
runAtLocal: args.runAtLocal ?? null, const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? 'Asia/Shanghai';
hour: args.hour ?? null, const task = await executeScheduledTaskCreateTool(args, {
minute: args.minute ?? 0, userId: PRIVATE_DATA_USER_ID,
weekday: args.weekday ?? null, scheduledTaskService: getScheduledTaskService(),
timezone, timezone,
notifyChannel: args.notifyChannel ?? 'both',
sourceChannel: 'agent',
sourceMessageId: args.sourceMessageId ?? null,
sourceText: args.sourceText ?? null,
}); });
const workerWarning = isScheduledTaskWorkerEnabled()
? null
: scheduledTaskWorkerDisabledMessage();
return [{ return [{
type: 'text', type: 'text',
text: JSON.stringify( text: JSON.stringify(task, null, 2),
workerWarning ? { ...task, workerWarning } : task,
null,
2,
),
}]; }];
} }
case 'scheduled_task_list': { case 'scheduled_task_list': {
+33 -13
View File
@@ -94,22 +94,35 @@ async function main() {
const drafts = createDraftStore(); const drafts = createDraftStore();
const scheduleService = createScheduleService(); const scheduleService = createScheduleService();
const card = await handleWechatIntentTransaction({ const passthrough = await handleWechatIntentTransaction({
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'verify-1', msgType: 'text' }, intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'verify-1', msgType: 'text' },
user: { userId: 'verify-user' }, user: { userId: 'verify-user' },
intentDraftService: drafts, intentDraftService: drafts,
scheduleService, scheduleService,
env, env,
}); });
if (card?.includes('我准备执行') && card.includes('确认')) { if (passthrough === null) pass('new messages pass through to goose');
pass('creates action card for timed reminder'); else fail('new messages pass through to goose', passthrough);
} else {
fail('creates action card for timed reminder', card);
}
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'); const pending = await drafts.getPendingDraft('verify-user');
if (pending?.draftType === 'timed_reminder') pass('persists pending draft'); if (pending?.draftType === 'timed_reminder') pass('pending draft can be committed after tool gate');
else fail('persists pending draft', JSON.stringify(pending)); else fail('pending draft can be committed after tool gate', JSON.stringify(pending));
const committed = await handleWechatIntentTransaction({ const committed = await handleWechatIntentTransaction({
intent: { agentText: '确认', msgId: 'verify-2', msgType: 'text' }, intent: { agentText: '确认', msgId: 'verify-2', msgType: 'text' },
@@ -131,11 +144,18 @@ async function main() {
scheduleService, scheduleService,
env, env,
}); });
if (slotFill?.includes('补充') && !slotFill.includes('我准备执行')) { if (slotFill === null) pass('incomplete requests pass through to goose');
pass('slot fill does not create confirmable draft'); else fail('incomplete requests pass through to goose', slotFill);
} else {
fail('slot fill does not create confirmable draft', 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({ const disabled = await handleWechatIntentTransaction({
intent: { agentText: '下午2点提醒我', msgId: 'verify-4', msgType: 'text' }, intent: { agentText: '下午2点提醒我', msgId: 'verify-4', msgType: 'text' },
+34 -181
View File
@@ -1,102 +1,13 @@
import { classifyUserIntent, inferActionLevel } from '../../intent-classifier.mjs';
import { commitIntentDraft } from '../../intent-transaction-commit.mjs'; import { commitIntentDraft } from '../../intent-transaction-commit.mjs';
import { import {
formatDraftCancelledReply, formatDraftCancelledReply,
formatDraftCommittedReply, formatDraftCommittedReply,
formatIntentActionCard,
parseDraftUserReply, parseDraftUserReply,
} from '../../intent-action-card.mjs'; } 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 { formatScheduledTaskCreateReply } from '../../scheduled-task-intent.mjs';
import { isIntentTransactionEnabled } from '../../intent-transaction-config.mjs';
import { isScheduledTaskWorkerEnabled, scheduledTaskWorkerDisabledMessage } from '../../scheduled-task-worker-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({ export async function handleWechatIntentTransaction({
intent, intent,
user, user,
@@ -114,108 +25,50 @@ export async function handleWechatIntentTransaction({
try { try {
const pending = await intentDraftService.getPendingDraft(user.userId); const pending = await intentDraftService.getPendingDraft(user.userId);
const replyKind = parseDraftUserReply(text); const replyKind = parseDraftUserReply(text);
if (pending && replyKind) { if (!pending || !replyKind) {
if (replyKind === 'cancel') { return null;
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 });
}
} }
const classification = classifyUserIntent(text, { timezone }); if (replyKind === 'cancel') {
const { layer, kind, action } = classification; await intentDraftService.cancelDraft(pending.id, user.userId);
return formatDraftCancelledReply();
if (layer === 'L0') { }
return formatQueryGuardReply(text, { 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, scheduleService,
scheduledTaskService, scheduledTaskService,
taskUnifiedService, taskUnifiedService,
userId: user.userId,
timezone, timezone,
env, env,
}); });
} await intentDraftService.markDraftCommitted(pending.id, user.userId, committed);
if (committed.kind === 'scheduled_task') {
if (layer === 'L3' || layer === null || kind === 'agent_schedule' || kind === 'agent_automation') { const workerWarning = isScheduledTaskWorkerEnabled(env)
return null; ? null
} : scheduledTaskWorkerDisabledMessage(env);
let reply = formatScheduledTaskCreateReply(committed.task, { workerWarning });
if (kind === 'manage') { reply = `${reply}\n\n(已通过确认卡片写入)`;
return null; return reply;
}
if (layer === 'ambiguous' || kind === 'clarify') {
if (classification.clarify?.length && kind !== 'clarify') {
return buildCard(classification, {}, inferActionLevel(classification, text), text);
} }
await intentDraftService.createDraft({ if (committed.kind === 'schedule_item' || committed.kind === 'schedule_reminder') {
userId: user.userId, return formatDraftCommittedReply({
layer: 'ambiguous', title: pending.title,
draftType: 'clarify', kind: committed.kind === 'schedule_reminder' ? 'timed_reminder' : 'create_todo',
actionLevel: 2, });
title: '待确认意图', }
payload: { clarify: ['notify_vs_act'], sourceText: text }, return formatDraftCommittedReply({ title: pending.title, kind: pending.draftType });
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);
} }
const draftableKinds = new Set([ return null;
'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;
} catch (err) { } catch (err) {
logger.warn?.( logger.warn?.(
'[wechat-intent-transaction] failed:', '[wechat-intent-transaction] failed:',
+50 -35
View File
@@ -2,8 +2,8 @@ import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { handleWechatIntentTransaction } from './intent-transaction.mjs'; import { handleWechatIntentTransaction } from './intent-transaction.mjs';
function createDraftStore() { function createDraftStore(initial = null) {
let pending = null; let pending = initial;
return { return {
async getPendingDraft(userId) { async getPendingDraft(userId) {
return pending?.userId === userId ? pending : null; return pending?.userId === userId ? pending : null;
@@ -43,9 +43,6 @@ function createScheduleService() {
async createReminder(payload) { async createReminder(payload) {
return { id: 'reminder-1', ...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); 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 drafts = createDraftStore();
const reply = await handleWechatIntentTransaction({ const reply = await handleWechatIntentTransaction({
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'm2' }, intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'm2' },
@@ -71,14 +68,41 @@ test('intent transaction creates action card for simple timed reminder', async (
scheduleService: createScheduleService(), scheduleService: createScheduleService(),
env: enabledEnv, env: enabledEnv,
}); });
assert.match(reply, /我准备执行/); assert.equal(reply, null);
assert.match(reply, /确认/); assert.equal(await drafts.getPendingDraft('user-1'), null);
const pending = await drafts.getPendingDraft('user-1');
assert.equal(pending.draftType, 'timed_reminder');
}); });
test('intent transaction commits draft after user confirms', async () => { test('intent transaction passes through page generation with auto refresh wording', async () => {
const drafts = createDraftStore(); 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 scheduleService = createScheduleService();
const calls = []; const calls = [];
scheduleService.createItem = async (payload) => { scheduleService.createItem = async (payload) => {
@@ -90,13 +114,6 @@ test('intent transaction commits draft after user confirms', async () => {
return { id: 'reminder-1', ...payload }; 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({ const reply = await handleWechatIntentTransaction({
intent: { agentText: '确认', msgId: 'm4' }, intent: { agentText: '确认', msgId: 'm4' },
user: { userId: 'user-1' }, user: { userId: 'user-1' },
@@ -108,28 +125,26 @@ test('intent transaction commits draft after user confirms', async () => {
assert.equal(calls.length, 2); assert.equal(calls.length, 2);
}); });
test('intent transaction asks for slot fill without creating draft', async () => { test('intent transaction cancels pending draft', async () => {
const drafts = createDraftStore(); const drafts = createDraftStore({
const reply = await handleWechatIntentTransaction({ id: 'draft-1',
intent: { agentText: '设置提醒', msgId: 'm6' }, userId: 'user-1',
user: { userId: 'user-2' }, layer: 'L2',
intentDraftService: drafts, draftType: 'agent_tool',
scheduleService: createScheduleService(), title: '定时任务',
env: enabledEnv, 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({ const reply = await handleWechatIntentTransaction({
intent: { agentText: '看看我的待办', msgId: 'm5' }, intent: { agentText: '取消', msgId: 'm5' },
user: { userId: 'user-1' }, user: { userId: 'user-1' },
intentDraftService: drafts, intentDraftService: drafts,
scheduleService: createScheduleService(), scheduleService: createScheduleService(),
env: enabledEnv, env: enabledEnv,
}); });
assert.match(reply, /待办/); assert.match(reply, /已取消/);
assert.equal(await drafts.getPendingDraft('user-1'), null); assert.equal(await drafts.getPendingDraft('user-1'), null);
}); });
+1 -29
View File
@@ -1,15 +1,8 @@
import { import {
buildScheduledTaskCreatePayload,
formatScheduledTaskClarification,
formatScheduledTaskCreateReply,
formatScheduledTaskListReply, formatScheduledTaskListReply,
isScheduledTaskIntent, isScheduledTaskIntent,
parseScheduledTaskIntent, parseScheduledTaskIntent,
} from '../../scheduled-task-intent.mjs'; } from '../../scheduled-task-intent.mjs';
import {
isScheduledTaskWorkerEnabled,
scheduledTaskWorkerDisabledMessage,
} from '../../scheduled-task-worker-config.mjs';
const WEEKDAY_NAMES = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; const WEEKDAY_NAMES = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
@@ -53,28 +46,7 @@ export async function handleWechatScheduledTaskIntent({
} }
if (taskIntent.action === 'create_scheduled_task') { if (taskIntent.action === 'create_scheduled_task') {
if (taskIntent.needsClarification?.length) { return null;
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;
} }
} catch (err) { } catch (err) {
logger.warn?.( logger.warn?.(
+24 -39
View File
@@ -2,8 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { handleWechatScheduledTaskIntent } from './scheduled-task.mjs'; import { handleWechatScheduledTaskIntent } from './scheduled-task.mjs';
test('handleWechatScheduledTaskIntent creates daily automation task', async () => { test('handleWechatScheduledTaskIntent passes create requests to goose', async () => {
const created = [];
const reply = await handleWechatScheduledTaskIntent({ const reply = await handleWechatScheduledTaskIntent({
intent: { intent: {
msgType: 'text', msgType: 'text',
@@ -12,45 +11,15 @@ test('handleWechatScheduledTaskIntent creates daily automation task', async () =
}, },
user: { userId: 'user-1' }, user: { userId: 'user-1' },
scheduledTaskService: { scheduledTaskService: {
async createTask(input) { async createTask() {
created.push(input); throw new Error('should not create from inbound regex');
return {
...input,
id: 'task-1',
nextRunAt: Date.now() + 3600_000,
};
}, },
}, },
env: { H5_SCHEDULED_TASK_WORKER_ENABLED: '1' },
}); });
assert.equal(reply, null);
assert.equal(created.length, 1);
assert.equal(created[0].hour, 6);
assert.match(created[0].taskSpec, /今日新闻页面/u);
assert.match(reply, /已设置每天定时任务/u);
}); });
test('handleWechatScheduledTaskIntent warns when worker disabled', async () => { test('handleWechatScheduledTaskIntent passes incomplete create requests to goose', 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 () => {
const reply = await handleWechatScheduledTaskIntent({ const reply = await handleWechatScheduledTaskIntent({
intent: { intent: {
msgType: 'text', msgType: 'text',
@@ -60,12 +29,11 @@ test('handleWechatScheduledTaskIntent returns clarification for incomplete reque
user: { userId: 'user-1' }, user: { userId: 'user-1' },
scheduledTaskService: { scheduledTaskService: {
async createTask() { async createTask() {
throw new Error('should not create'); throw new Error('should not create from inbound regex');
}, },
}, },
}); });
assert.equal(reply, null);
assert.match(reply, /执行时间和任务内容/u);
}); });
test('handleWechatScheduledTaskIntent ignores non automation text', async () => { test('handleWechatScheduledTaskIntent ignores non automation text', async () => {
@@ -84,3 +52,20 @@ test('handleWechatScheduledTaskIntent ignores non automation text', async () =>
}); });
assert.equal(reply, null); 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);
});
-2
View File
@@ -4,7 +4,6 @@ import {
isWechatPageEditText, isWechatPageEditText,
isWechatPageRetryText, isWechatPageRetryText,
} from './page-continuation.mjs'; } from './page-continuation.mjs';
import { shouldUseScheduledTaskAutomation } from '../../scheduled-task-intent.mjs';
export const PAGE_GENERATE_PATTERN = export const PAGE_GENERATE_PATTERN =
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu; /(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu;
@@ -39,7 +38,6 @@ export const CONNECTIVITY_TEST_PATTERN = /^(测试\s*\d*|test\s*\d*)[!!。.\s]
export function isPageGenerateText(text) { export function isPageGenerateText(text) {
const normalized = String(text ?? '').trim(); const normalized = String(text ?? '').trim();
if (!normalized) return false; if (!normalized) return false;
if (shouldUseScheduledTaskAutomation(normalized)) return false;
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false; if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
return ( return (
PAGE_GENERATE_PATTERN.test(normalized) PAGE_GENERATE_PATTERN.test(normalized)