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 { 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
@@ -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': {
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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:',
|
||||||
|
|||||||
@@ -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,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?.(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user