d58dc2a251
Introduce Draft → Confirm → Commit flow for WeChat schedule intents behind feature flags, plus h5_tasks dual-write/read aggregation and rollout scripts so reminders and automations get explicit user confirmation before persisting. Co-authored-by: Cursor <cursoragent@cursor.com>
247 lines
8.4 KiB
JavaScript
247 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Phase A ITL 离线验证:分类 → Draft → Confirm → Commit(mock 服务,无需 DB)
|
||
*/
|
||
import assert from 'node:assert/strict';
|
||
import { classifyUserIntent } from '../intent-classifier.mjs';
|
||
import { handleWechatIntentTransaction } from '../wechat/handlers/intent-transaction.mjs';
|
||
import { isIntentTransactionEnabled } from '../intent-transaction-config.mjs';
|
||
import { attachUnifiedTaskSync } from '../task-unified-sync.mjs';
|
||
import { createTaskUnifiedService } from '../task-unified-service.mjs';
|
||
import { formatQueryGuardReply } from '../intent-query-guard.mjs';
|
||
|
||
const env = {
|
||
H5_INTENT_TRANSACTION_ENABLED: '1',
|
||
H5_UNIFIED_TASKS_ENABLED: '1',
|
||
H5_DEFAULT_TIMEZONE: 'Asia/Shanghai',
|
||
};
|
||
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
function pass(label, detail = '') {
|
||
passed += 1;
|
||
console.log(`✔ ${label}${detail ? `: ${detail}` : ''}`);
|
||
}
|
||
|
||
function fail(label, detail = '') {
|
||
failed += 1;
|
||
console.error(`✘ ${label}${detail ? `: ${detail}` : ''}`);
|
||
}
|
||
|
||
function createDraftStore() {
|
||
let pending = null;
|
||
return {
|
||
async getPendingDraft(userId) {
|
||
return pending?.userId === userId ? pending : null;
|
||
},
|
||
async createDraft(payload) {
|
||
pending = {
|
||
id: 'draft-verify-1',
|
||
status: 'draft',
|
||
...payload,
|
||
payload: payload.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;
|
||
},
|
||
};
|
||
}
|
||
|
||
function createScheduleService() {
|
||
const calls = [];
|
||
return {
|
||
calls,
|
||
async createItem(payload) {
|
||
calls.push(['createItem', payload]);
|
||
return { id: 'item-verify-1', ...payload };
|
||
},
|
||
async createReminder(payload) {
|
||
calls.push(['createReminder', payload]);
|
||
return { id: 'reminder-verify-1', ...payload };
|
||
},
|
||
buildTodoDigestText() {
|
||
return '今天有 0 条待办。';
|
||
},
|
||
};
|
||
}
|
||
|
||
async function main() {
|
||
if (!isIntentTransactionEnabled(env)) {
|
||
fail('feature flag', 'H5_INTENT_TRANSACTION_ENABLED 应为 1');
|
||
process.exit(1);
|
||
}
|
||
pass('feature flag enabled');
|
||
|
||
const query = classifyUserIntent('有没有我的新闻定时任务');
|
||
if (query.layer === 'L0') pass('query guard routes inventory to L0');
|
||
else fail('query guard routes inventory to L0', JSON.stringify(query));
|
||
|
||
const bare = classifyUserIntent('设置提醒');
|
||
if (bare.layer === 'L1' && bare.clarify?.length) pass('bare reminder asks for slot fill');
|
||
else fail('bare reminder asks for slot fill', JSON.stringify(bare));
|
||
|
||
const cancel = classifyUserIntent('取消每日新闻任务');
|
||
if (cancel.action === 'cancel_scheduled_task') pass('cancel routes to manage action');
|
||
else fail('cancel routes to manage action', JSON.stringify(cancel));
|
||
|
||
const drafts = createDraftStore();
|
||
const scheduleService = createScheduleService();
|
||
const card = await handleWechatIntentTransaction({
|
||
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'verify-1', msgType: 'text' },
|
||
user: { userId: 'verify-user' },
|
||
intentDraftService: drafts,
|
||
scheduleService,
|
||
env,
|
||
});
|
||
if (card?.includes('我准备执行') && card.includes('确认')) {
|
||
pass('creates action card for timed reminder');
|
||
} else {
|
||
fail('creates action card for timed reminder', card);
|
||
}
|
||
|
||
const pending = await drafts.getPendingDraft('verify-user');
|
||
if (pending?.draftType === 'timed_reminder') pass('persists pending draft');
|
||
else fail('persists pending draft', JSON.stringify(pending));
|
||
|
||
const committed = await handleWechatIntentTransaction({
|
||
intent: { agentText: '确认', msgId: 'verify-2', msgType: 'text' },
|
||
user: { userId: 'verify-user' },
|
||
intentDraftService: drafts,
|
||
scheduleService,
|
||
env,
|
||
});
|
||
if (committed?.includes('已设置提醒') && scheduleService.calls.length === 2) {
|
||
pass('confirm commits reminder to schedule service');
|
||
} else {
|
||
fail('confirm commits reminder to schedule service', `${committed} calls=${scheduleService.calls.length}`);
|
||
}
|
||
|
||
const slotFill = await handleWechatIntentTransaction({
|
||
intent: { agentText: '设置提醒', msgId: 'verify-3', msgType: 'text' },
|
||
user: { userId: 'verify-user-2' },
|
||
intentDraftService: createDraftStore(),
|
||
scheduleService,
|
||
env,
|
||
});
|
||
if (slotFill?.includes('补充') && !slotFill.includes('我准备执行')) {
|
||
pass('slot fill does not create confirmable draft');
|
||
} else {
|
||
fail('slot fill does not create confirmable draft', slotFill);
|
||
}
|
||
|
||
const disabled = await handleWechatIntentTransaction({
|
||
intent: { agentText: '下午2点提醒我', msgId: 'verify-4', msgType: 'text' },
|
||
user: { userId: 'verify-user-3' },
|
||
intentDraftService: createDraftStore(),
|
||
scheduleService,
|
||
env: { H5_INTENT_TRANSACTION_ENABLED: '0' },
|
||
});
|
||
if (disabled === null) pass('returns null when feature disabled');
|
||
else fail('returns null when feature disabled', disabled);
|
||
|
||
const syncCalls = [];
|
||
const memoryPool = {
|
||
tasks: [],
|
||
async query(sql, params = []) {
|
||
if (sql.includes('JOIN h5_schedule_reminders')) {
|
||
return [[{
|
||
id: 'item-verify-2',
|
||
title: '项目计划例会',
|
||
start_at: 9999,
|
||
timezone: 'Asia/Shanghai',
|
||
source_channel: 'wechat',
|
||
source_message_id: null,
|
||
source_text: null,
|
||
reminder_id: 'reminder-verify-2',
|
||
remind_at: 9999,
|
||
channel: 'wechat',
|
||
reminder_status: 'pending',
|
||
}]];
|
||
}
|
||
if (sql.includes('INSERT INTO h5_tasks')) {
|
||
this.tasks.push({ legacy_ref_json: params[12], user_id: params[1] });
|
||
return [{ affectedRows: 1 }];
|
||
}
|
||
if (sql.includes('SELECT * FROM h5_tasks') && sql.includes('legacy_ref_json')) return [[]];
|
||
if (sql.includes('SELECT * FROM h5_tasks WHERE id = ?')) {
|
||
return [[{
|
||
id: params[0],
|
||
user_id: 'verify-user',
|
||
type: 'reminder',
|
||
title: '项目计划例会',
|
||
spec_json: '{}',
|
||
trigger_json: '{}',
|
||
action_json: '{}',
|
||
action_level: 1,
|
||
notify_channel: 'wechat',
|
||
status: 'active',
|
||
next_run_at: 1000,
|
||
last_run_at: null,
|
||
legacy_ref_json: '{"table":"h5_schedule_reminders","id":"reminder-verify-1"}',
|
||
source_channel: 'wechat',
|
||
source_message_id: null,
|
||
source_text: null,
|
||
created_at: 1,
|
||
updated_at: 1,
|
||
}]];
|
||
}
|
||
return [[]];
|
||
},
|
||
};
|
||
const unifiedService = createTaskUnifiedService(memoryPool, { clock: { now: () => 1234 } });
|
||
const wrappedSchedule = {
|
||
async createItem(payload) {
|
||
return { id: 'item-verify-2', userId: payload.userId, kind: 'event', title: payload.title, timezone: 'Asia/Shanghai', status: 'active' };
|
||
},
|
||
async createReminder(payload) {
|
||
return { id: 'reminder-verify-2', itemId: payload.itemId, remindAt: payload.remindAt, channel: 'wechat' };
|
||
},
|
||
};
|
||
attachUnifiedTaskSync({
|
||
scheduleService: wrappedSchedule,
|
||
scheduledTaskService: {},
|
||
taskUnifiedService: {
|
||
...unifiedService,
|
||
async syncFromCommit(payload) {
|
||
syncCalls.push(payload.kind);
|
||
return unifiedService.syncFromCommit(payload);
|
||
},
|
||
},
|
||
pool: memoryPool,
|
||
env,
|
||
});
|
||
await wrappedSchedule.createReminder({ userId: 'verify-user', itemId: 'item-verify-2', remindAt: 9999 });
|
||
if (syncCalls.includes('timed_reminder')) pass('schedule reminder path dual-writes via attachUnifiedTaskSync');
|
||
else fail('schedule reminder path dual-writes via attachUnifiedTaskSync', syncCalls.join(','));
|
||
|
||
const queryReply = await formatQueryGuardReply('有没有我的新闻定时任务', {
|
||
taskUnifiedService: {
|
||
async listUserTasks() {
|
||
return [{ type: 'automation', title: '每日新闻页', trigger: { repeat: 'daily', hour: 5, minute: 30 }, nextRunAt: 1000 }];
|
||
},
|
||
},
|
||
userId: 'verify-user',
|
||
});
|
||
if (queryReply.includes('任务一览') && queryReply.includes('每日新闻页')) {
|
||
pass('query guard uses unified task list');
|
||
} else {
|
||
fail('query guard uses unified task list', queryReply);
|
||
}
|
||
|
||
console.log(`\n${passed} passed, ${failed} failed`);
|
||
process.exit(failed > 0 ? 1 : 0);
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error(err);
|
||
process.exit(1);
|
||
});
|