#!/usr/bin/env node /** * Simulate GET /api/sessions/:id repair for a session using live 103 data. * Usage: node scripts/verify-session-repair-local.mjs [sessionId] [userId] */ import { createConnection } from 'mysql2/promise'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { countNonEmptyConversationMessages, loadSessionConversationRows, repairConversationFromDbRows, shouldRepairConversationFromDb, } from '../conversation-repair.mjs'; import { extractConversationMessageText } from '../conversation-memory.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, '..'); const sessionId = process.argv[2] ?? '20260705_2'; const userId = process.argv[3] ?? 'a6fb1e97-2b0f-447b-b138-4561d8e5c53e'; function previewMessage(message, index) { const text = extractConversationMessageText(message).replace(/\s+/g, ' ').slice(0, 90); return `${index}. [${message.role}] ${text || '(empty)'}`; } async function load103Env() { const { execSync } = await import('node:child_process'); const envText = execSync('ssh -o BatchMode=yes john@58.38.22.103 cat /Users/john/Project/Memind/.env', { encoding: 'utf8', }); const dbLine = envText.match(/^DATABASE_URL=(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, ''); const secret = envText.match(/^TKMIND_SERVER__SECRET_KEY=(.+)$/m)?.[1]?.trim().replace(/^["']|["']$/g, ''); if (!dbLine || !secret) throw new Error('missing DATABASE_URL or TKMIND_SERVER__SECRET_KEY on 103'); const url = new URL(dbLine); return { pool: await createConnection({ host: url.hostname, port: Number(url.port || 3306), user: decodeURIComponent(url.username), password: decodeURIComponent(url.password), database: url.pathname.replace(/^\//, ''), }), secret, }; } async function fetchGooseSession(secret, sid) { const { execSync } = await import('node:child_process'); const json = execSync( `ssh -o BatchMode=yes john@58.38.22.103 'SECRET=$(grep ^TKMIND_SERVER__SECRET_KEY= /Users/john/Project/Memind/.env | cut -d= -f2- | tr -d "\\""); curl -k -sf "https://127.0.0.1:18006/sessions/${sid}" -H "X-Secret-Key: $SECRET"'`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }, ); return JSON.parse(json); } async function fetchProductionApi(sessionId) { const { execSync } = await import('node:child_process'); const login = execSync( `curl -sk -c /tmp/verify-session-repair-cj.txt -X POST https://m.tkmind.cn/auth/login -H 'Content-Type: application/json' -d '{"username":"john","password":"981122tj"}'`, { encoding: 'utf8' }, ); const auth = JSON.parse(login); if (!auth.authenticated) throw new Error('login failed'); const detail = execSync( `curl -sk -b /tmp/verify-session-repair-cj.txt 'https://m.tkmind.cn/api/sessions/${encodeURIComponent(sessionId)}'`, { encoding: 'utf8' }, ); return JSON.parse(detail); } async function main() { console.log(`\n=== 模拟验证 session ${sessionId} (user ${userId}) ===\n`); const { pool, secret } = await load103Env(); const gooseSession = await fetchGooseSession(secret, sessionId); const dbRows = await loadSessionConversationRows(pool, sessionId, userId); await pool.end(); const gooseConversation = (gooseSession.conversation ?? []).filter( (m) => m?.metadata?.userVisible !== false, ); const repairedConversation = repairConversationFromDbRows(gooseConversation, dbRows); console.log('--- Goose 原始(API 发版前会返回这类数据)---'); console.log(`消息数: ${gooseConversation.length}, 非空: ${countNonEmptyConversationMessages(gooseConversation)}`); gooseConversation.forEach((m, i) => console.log(previewMessage(m, i))); console.log('\n--- DB h5_conversation_messages ---'); console.log(`行数: ${dbRows.length}`); dbRows.forEach((row, i) => { const text = String(row.text ?? '').replace(/\s+/g, ' ').slice(0, 90); console.log(`${i}. [${row.role}] seq=${row.sequence_no} ${text || '(empty)'}`); }); console.log(`\nshouldRepair: ${shouldRepairConversationFromDb(gooseConversation, dbRows)}`); console.log('\n--- 本地 repair 后(发版后 API 应接近此结果)---'); console.log(`消息数: ${repairedConversation.length}, 非空: ${countNonEmptyConversationMessages(repairedConversation)}`); repairedConversation.forEach((m, i) => console.log(previewMessage(m, i))); console.log('\n--- 当前生产 API(未发版,无 repair)---'); try { const prod = await fetchProductionApi(sessionId); const prodConv = (prod.conversation ?? []).filter((m) => m?.metadata?.userVisible !== false); console.log(`name: ${prod.name}`); console.log(`消息数: ${prodConv.length}, 非空: ${countNonEmptyConversationMessages(prodConv)}`); prodConv.forEach((m, i) => console.log(previewMessage(m, i))); } catch (err) { console.log(`生产 API 拉取失败: ${err instanceof Error ? err.message : err}`); } console.log('\n=== 结论 ==='); const repairedNonEmpty = countNonEmptyConversationMessages(repairedConversation); const gooseNonEmpty = countNonEmptyConversationMessages(gooseConversation); if (repairedNonEmpty > gooseNonEmpty) { console.log(`✔ repair 后非空消息 ${gooseNonEmpty} → ${repairedNonEmpty},发版后 john 应能看到完整历史`); } else { console.log(`✖ repair 未改善非空消息数 (${gooseNonEmpty} → ${repairedNonEmpty})`); process.exitCode = 1; } } main().catch((err) => { console.error(err); process.exit(1); });