File size: 5,556 Bytes
097fb32 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | #!/usr/bin/env node
/**
* test/migrate-jsonl-to-sqlite.mjs
*
* 将现有 JSONL 日志文件迁移到 SQLite 数据库。
* 运行方式:node test/migrate-jsonl-to-sqlite.mjs [--db ./logs/cursor2api.db] [--dir ./logs] [--dry-run]
*
* 选项:
* --db <path> SQLite 文件路径(默认 ./logs/cursor2api.db)
* --dir <path> JSONL 日志目录(默认 ./logs)
* --dry-run 只统计不写入
* --clear 写入前清空数据库已有数据
*/
import Database from 'better-sqlite3';
import { readFileSync, readdirSync, existsSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
// ==================== 参数解析 ====================
const args = process.argv.slice(2);
function getArg(name) {
const idx = args.indexOf(name);
return idx >= 0 ? args[idx + 1] : null;
}
const DB_PATH = getArg('--db') || './logs/cursor2api.db';
const LOG_DIR = getArg('--dir') || './logs';
const DRY_RUN = args.includes('--dry-run');
const CLEAR = args.includes('--clear');
console.log('=== JSONL → SQLite 迁移工具 ===');
console.log(`日志目录: ${LOG_DIR}`);
console.log(`SQLite: ${DB_PATH}`);
console.log(`模式: ${DRY_RUN ? 'dry-run(只统计)' : '写入'}`);
if (CLEAR && !DRY_RUN) console.log('清空模式: 是');
console.log();
// ==================== 检查日志目录 ====================
if (!existsSync(LOG_DIR)) {
console.error(`日志目录不存在: ${LOG_DIR}`);
process.exit(1);
}
const jsonlFiles = readdirSync(LOG_DIR)
.filter(f => f.startsWith('cursor2api-') && f.endsWith('.jsonl'))
.sort();
if (jsonlFiles.length === 0) {
console.log('未找到 JSONL 日志文件,退出。');
process.exit(0);
}
console.log(`找到 ${jsonlFiles.length} 个 JSONL 文件:`);
for (const f of jsonlFiles) {
const content = readFileSync(join(LOG_DIR, f), 'utf-8');
const lines = content.split('\n').filter(Boolean);
console.log(` ${f} (${lines.length} 行)`);
}
console.log();
if (DRY_RUN) {
let total = 0;
for (const f of jsonlFiles) {
const lines = readFileSync(join(LOG_DIR, f), 'utf-8').split('\n').filter(Boolean);
total += lines.length;
}
console.log(`[dry-run] 共 ${total} 条记录,无写入操作。`);
process.exit(0);
}
// ==================== 初始化 SQLite ====================
const dbDir = dirname(DB_PATH);
if (dbDir && !existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
const db = new Database(DB_PATH);
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
db.exec(`
CREATE TABLE IF NOT EXISTS requests (
request_id TEXT PRIMARY KEY,
timestamp INTEGER NOT NULL,
summary_json TEXT NOT NULL,
payload_json TEXT
);
CREATE INDEX IF NOT EXISTS idx_timestamp ON requests(timestamp);
`);
if (CLEAR) {
const { changes } = db.prepare('DELETE FROM requests').run();
console.log(`已清空数据库(删除 ${changes} 条)`);
}
const existingCount = db.prepare('SELECT COUNT(*) as cnt FROM requests').get().cnt;
console.log(`数据库现有记录: ${existingCount} 条`);
console.log();
// ==================== 迁移 ====================
const insert = db.prepare(
'INSERT OR IGNORE INTO requests (request_id, timestamp, summary_json, payload_json) VALUES (?, ?, ?, ?)'
);
const migrate = db.transaction((lines) => {
let inserted = 0, skipped = 0, malformed = 0;
for (const line of lines) {
try {
const record = JSON.parse(line);
const summary = record.summary;
if (!summary?.requestId) { malformed++; continue; }
const result = insert.run(
summary.requestId,
summary.startTime || record.timestamp || Date.now(),
JSON.stringify(summary),
record.payload ? JSON.stringify(record.payload) : null
);
if (result.changes > 0) inserted++;
else skipped++;
} catch {
malformed++;
}
}
return { inserted, skipped, malformed };
});
let totalInserted = 0, totalSkipped = 0, totalMalformed = 0;
for (const f of jsonlFiles) {
const content = readFileSync(join(LOG_DIR, f), 'utf-8');
const lines = content.split('\n').filter(Boolean);
process.stdout.write(`迁移 ${f} (${lines.length} 行)... `);
const { inserted, skipped, malformed } = migrate(lines);
console.log(`插入 ${inserted},跳过(重复) ${skipped},格式错误 ${malformed}`);
totalInserted += inserted;
totalSkipped += skipped;
totalMalformed += malformed;
}
// ==================== 结果统计 ====================
const finalCount = db.prepare('SELECT COUNT(*) as cnt FROM requests').get().cnt;
console.log();
console.log('=== 迁移完成 ===');
console.log(`插入新记录: ${totalInserted}`);
console.log(`跳过(重复): ${totalSkipped}`);
console.log(`格式错误: ${totalMalformed}`);
console.log(`数据库总计: ${finalCount} 条`);
// 验证:读取最新 5 条
console.log();
console.log('=== 验证:最新 5 条记录 ===');
const rows = db.prepare('SELECT request_id, timestamp, summary_json FROM requests ORDER BY timestamp DESC LIMIT 5').all();
for (const row of rows) {
const s = JSON.parse(row.summary_json);
const date = new Date(row.timestamp).toISOString();
console.log(` [${date}] ${row.request_id} | ${s.model || '?'} | ${s.status || '?'} | ${s.title ? s.title.slice(0, 40) : '(无标题)'}`);
}
db.close();
console.log();
console.log('完成。');
|