#!/usr/bin/env node /** * 唐用户智趣通道 103 只读巡检 + 推荐配置输出 * * Usage: * node scripts/verify-tang-cursor-channel-103.mjs * node scripts/verify-tang-cursor-channel-103.mjs --print-recommended-patch */ import process from 'node:process'; import { execSync } from 'node:child_process'; import mysql from 'mysql2/promise'; import { CURSOR_CHANNEL_FEATURES, defaultCursorChannelFeatures, normalizeCursorChannelFeatures, } from '../cursor-channel-features.mjs'; const TANG = 'a70ff537-8908-486e-9b6c-042e07cc25db'; const TANG_USERNAME = 'wx_ul610et8'; const RECOMMENDED_FEATURES = { ...defaultCursorChannelFeatures(), [CURSOR_CHANNEL_FEATURES.PAGE_GENERATE]: { enabled: true }, [CURSOR_CHANNEL_FEATURES.PAGE_DATA]: { enabled: true }, [CURSOR_CHANNEL_FEATURES.EXCEL_ANALYSIS]: { enabled: false }, [CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE]: { enabled: false }, [CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS]: { enabled: true }, }; const args = process.argv.slice(2); const printPatch = args.includes('--print-recommended-patch'); function pass(label, detail = '') { console.log(`✔ ${label}${detail ? `: ${detail}` : ''}`); } function warn(label, detail = '') { console.warn(`⚠ ${label}${detail ? `: ${detail}` : ''}`); } function fail(label, detail = '') { console.error(`✘ ${label}${detail ? `: ${detail}` : ''}`); } async function loadProdDatabaseUrl() { return execSync( "ssh -o BatchMode=yes -o ConnectTimeout=8 john@58.38.22.103 \"grep '^DATABASE_URL=' /Users/john/Project/Memind/.env | cut -d= -f2-\"", { encoding: 'utf8' }, ).trim(); } async function read103Env(pattern) { try { const out = execSync( `ssh -o BatchMode=yes -o ConnectTimeout=8 john@58.38.22.103 'grep -E "${pattern}" /Users/john/Project/Memind/.env || true'`, { encoding: 'utf8' }, ).trim(); return out.split('\n').filter(Boolean); } catch { return []; } } async function main() { let failed = 0; const databaseUrl = await loadProdDatabaseUrl(); const pool = mysql.createPool({ uri: databaseUrl, connectionLimit: 2 }); const [users] = await pool.query( 'SELECT id, username, display_name FROM h5_users WHERE id = ? LIMIT 1', [TANG], ); const user = users[0]; if (user?.username === TANG_USERNAME) pass('唐用户身份', `${user.display_name} / ${user.username}`); else { fail('唐用户身份'); failed += 1; } const envLines = await read103Env('^(MEMIND_CURSOR|MEMIND_CURSOR_CHAT_BRIDGE|H5_SCHEDULED_TASK)'); const envMap = Object.fromEntries( envLines.map((line) => { const idx = line.indexOf('='); return [line.slice(0, idx), line.slice(idx + 1)]; }), ); if (envMap.MEMIND_CURSOR_EXECUTOR_ENABLED === '1') pass('Cursor 基础设施', 'MEMIND_CURSOR_EXECUTOR_ENABLED=1'); else { fail('Cursor 基础设施', '未开启 MEMIND_CURSOR_EXECUTOR_ENABLED'); failed += 1; } if (envMap.MEMIND_CURSOR_CHAT_BRIDGE_ENABLED === '1') { pass('Chat Bridge 基础设施', '已配置'); } else { warn('Chat Bridge 基础设施', '未开 MEMIND_CURSOR_CHAT_BRIDGE_ENABLED;后台 chatBridge 开关暂不会生效'); } if (envMap.H5_SCHEDULED_TASK_WORKER_ENABLED === '1') pass('定时任务 Worker', '已开启'); else { warn('定时任务 Worker', 'H5_SCHEDULED_TASK_WORKER_ENABLED 未开'); } const [cursorRows] = await pool.query( 'SELECT config_json FROM h5_wechat_cursor_executor_config WHERE config_scope = ? LIMIT 1', ['global'], ); const stored = cursorRows[0]?.config_json ?? null; const parsed = typeof stored === 'string' ? JSON.parse(stored) : (stored ?? {}); const features = normalizeCursorChannelFeatures(parsed.features, { intentAllowlist: parsed.intentAllowlist, }); if (parsed.enabled) pass('智趣总开关', 'enabled'); else { warn('智趣总开关', '当前关闭'); } const allowlist = Array.isArray(parsed.userAllowlist) ? parsed.userAllowlist : []; if (allowlist.map((v) => v.toLowerCase()).includes(TANG.toLowerCase())) { pass('唐在白名单', `${allowlist.length} 人`); } else { fail('唐在白名单', `当前: ${allowlist.join(', ') || '(空)'}`); failed += 1; } for (const [key, label] of [ [CURSOR_CHANNEL_FEATURES.PAGE_GENERATE, '页面生成'], [CURSOR_CHANNEL_FEATURES.PAGE_DATA, '问卷 Page Data'], [CURSOR_CHANNEL_FEATURES.SCHEDULED_TASKS, '定时任务 Cursor'], [CURSOR_CHANNEL_FEATURES.CHAT_BRIDGE, '普通聊天 Bridge'], ]) { const on = features[key]?.enabled === true; const want = RECOMMENDED_FEATURES[key]?.enabled === true; if (on === want) pass(`能力 ${label}`, on ? '已开' : '已关(符合推荐)'); else warn(`能力 ${label}`, `当前=${on ? '开' : '关'},推荐=${want ? '开' : '关'}`); } const [activeTasks] = await pool.query( `SELECT COUNT(*) AS c FROM h5_scheduled_tasks WHERE user_id = ? AND status IN ('active','locked')`, [TANG], ); const activeCount = Number(activeTasks[0]?.c ?? 0); if (activeCount > 0) pass('active 定时任务', String(activeCount)); else warn('active 定时任务', '0 — 需重建新闻/天气任务后才能验证 scheduledTasks'); const [failedTasks] = await pool.query( `SELECT title, last_error FROM h5_scheduled_tasks WHERE user_id = ? AND status = 'failed' ORDER BY updated_at DESC LIMIT 5`, [TANG], ); if (failedTasks.length) { console.log('\n--- 最近 failed 定时任务 ---'); for (const row of failedTasks) { console.log(`- ${row.title}: ${String(row.last_error ?? '').slice(0, 120)}`); } } const recommendedPatch = { enabled: true, userAllowlist: [TANG], channelAllowlist: ['h5', 'wechat_mp'], features: RECOMMENDED_FEATURES, fallbackToDeepseek: true, }; if (printPatch) { console.log('\n--- 推荐 memind_adm 保存 payload ---'); console.log(JSON.stringify(recommendedPatch, null, 2)); } else { console.log('\n提示: node scripts/verify-tang-cursor-channel-103.mjs --print-recommended-patch'); } await pool.end(); process.exit(failed > 0 ? 1 : 0); } main().catch((err) => { console.error(err); process.exit(1); });