#!/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); });