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>
129 lines
4.6 KiB
JavaScript
129 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 唐用户 ITL / Unified Tasks 103 就绪检查(只读)
|
|
*
|
|
* 用法:
|
|
* node scripts/verify-tang-itl-readiness-103.mjs
|
|
*/
|
|
import process from 'node:process';
|
|
import mysql from 'mysql2/promise';
|
|
import { classifyUserIntent } from '../intent-classifier.mjs';
|
|
import { createTaskUnifiedService, formatUnifiedTaskListReply } from '../task-unified-service.mjs';
|
|
import { handleWechatIntentTransaction } from '../wechat/handlers/intent-transaction.mjs';
|
|
import { loadH5Environment } from './load-env.mjs';
|
|
|
|
loadH5Environment(import.meta.dirname);
|
|
|
|
const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db';
|
|
const DUPLICATE_WEATHER_TASK_ID = '66f0ee11-7777-415f-affd-34422c772d0f';
|
|
|
|
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}` : ''}`);
|
|
}
|
|
|
|
async function main() {
|
|
if (!process.env.DATABASE_URL) {
|
|
fail('DATABASE_URL', '未配置');
|
|
process.exit(1);
|
|
}
|
|
|
|
const pool = mysql.createPool({ uri: process.env.DATABASE_URL, connectionLimit: 2 });
|
|
const service = createTaskUnifiedService(pool);
|
|
|
|
const [dupRows] = await pool.query(
|
|
`SELECT id, status FROM h5_scheduled_tasks WHERE id = ? AND user_id = ? LIMIT 1`,
|
|
[DUPLICATE_WEATHER_TASK_ID, TANG],
|
|
);
|
|
const dup = dupRows?.[0];
|
|
if (!dup || dup.status === 'cancelled') pass('duplicate weather task cleaned up');
|
|
else fail('duplicate weather task cleaned up', `status=${dup.status}`);
|
|
|
|
const [activeTasks] = await pool.query(
|
|
`SELECT id, title, hour, minute, status FROM h5_scheduled_tasks
|
|
WHERE user_id = ? AND status IN ('active', 'locked')
|
|
ORDER BY hour, minute`,
|
|
[TANG],
|
|
);
|
|
if (activeTasks.length >= 1 && activeTasks.length <= 4) {
|
|
pass('active scheduled task count sane', String(activeTasks.length));
|
|
} else {
|
|
fail('active scheduled task count sane', String(activeTasks.length));
|
|
}
|
|
|
|
const unified = await service.listUserTasks({ userId: TANG, limit: 20 });
|
|
if (unified.length >= activeTasks.length) pass('unified list covers legacy tasks', `${unified.length} unified`);
|
|
else fail('unified list covers legacy tasks', `${unified.length} unified vs ${activeTasks.length} legacy`);
|
|
|
|
const queryText = '有没有我的新闻定时任务';
|
|
const classification = classifyUserIntent(queryText);
|
|
if (classification.layer === 'L0') pass('inventory query classified as L0');
|
|
else fail('inventory query classified as L0', JSON.stringify(classification));
|
|
|
|
const listReply = formatUnifiedTaskListReply(unified);
|
|
if (listReply.includes('任务一览') || listReply.includes('没有')) pass('unified list reply formatted');
|
|
else fail('unified list reply formatted', listReply);
|
|
|
|
const drafts = {
|
|
pending: null,
|
|
async getPendingDraft() { return this.pending; },
|
|
async createDraft(payload) { this.pending = { id: 'draft-tang', status: 'draft', ...payload }; return this.pending; },
|
|
async cancelDraft() { this.pending = null; },
|
|
async markDraftCommitted(id, userId, ref) {
|
|
this.pending = { ...this.pending, status: 'committed', committedRef: ref };
|
|
return this.pending;
|
|
},
|
|
};
|
|
const scheduleCalls = [];
|
|
const card = await handleWechatIntentTransaction({
|
|
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'tang-itl-1', msgType: 'text' },
|
|
user: { userId: TANG },
|
|
intentDraftService: drafts,
|
|
taskUnifiedService: service,
|
|
scheduleService: {
|
|
async createItem(payload) {
|
|
scheduleCalls.push(['createItem', payload.title]);
|
|
return { id: 'item-sim', ...payload, userId: TANG };
|
|
},
|
|
async createReminder(payload) {
|
|
scheduleCalls.push(['createReminder']);
|
|
return { id: 'rem-sim', ...payload, remindAt: payload.remindAt, channel: 'wechat' };
|
|
},
|
|
buildTodoDigestText: async () => '今天有 0 条待办。',
|
|
},
|
|
env,
|
|
});
|
|
if (card?.includes('确认')) pass('ITL action card dry path');
|
|
else fail('ITL action card dry path', card);
|
|
|
|
console.log('\n--- active scheduled tasks ---');
|
|
for (const row of activeTasks) {
|
|
console.log(`- ${row.title} (${row.hour}:${String(row.minute).padStart(2, '0')}) [${row.status}]`);
|
|
}
|
|
console.log('\n--- unified preview ---');
|
|
console.log(listReply);
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
await pool.end();
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|