| #!/usr/bin/env node |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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); |
| } |
|
|
| |
|
|
| 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} 条`); |
|
|
| |
| 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('完成。'); |
|
|