Files
memind/scripts/verify-intent-transaction-layer.mjs
T
john d58dc2a251 feat(wechat): add Intent Transaction Layer with unified task schema
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>
2026-08-24 15:59:17 +08:00

247 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Phase A ITL 离线验证:分类 → Draft → Confirm → Commitmock 服务,无需 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);
});