Files
memind/scripts/verify-session-repair-local.mjs
T
john 614837e199 fix: repair chat history from DB when Goose session has empty placeholders
Portal direct chat → goosed escalation can leave hollow Goose messages while
h5_conversation_messages retains the full dialogue. Backfill session detail
from DB, extend snapshot list fallback, and keep stored session id on transient
boot failures so refresh no longer looks like a blank new chat.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 11:04:25 +08:00

129 lines
5.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
/**
* 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);
});