mirror of
https://github.com/7836246/cursor2api.git
synced 2026-05-07 22:27:15 +08:00
- 新增 src/logger-db.ts:SQLite 封装层(WAL 模式,支持写入/分页/状态计数/按需 payload 查询) - logger.ts:双写 SQLite+JSONL,启动时 db_enabled 模式跳过 JSONL 读取避免 OOM,新增游标分页和后端过滤函数 - config.ts/config-api.ts:新增 db_enabled/db_path 配置字段及 LOG_DB_ENABLED/LOG_DB_PATH 环境变量 - log-viewer.ts/index.ts:新增 /api/requests/more 支持 status/keyword/since 后端过滤 - Vue UI:搜索框 400ms 防抖,状态/时间筛选立即触发后端查询,statusCounts 不受状态筛选影响,SSE 实时推送时增量更新计数 - 新增迁移工具 test/migrate-jsonl-to-sqlite.mjs 和单元测试 test/unit-logger-db.mjs - 完善 README.md、config.yaml.example、docker-compose.yml、vue-ui/README.md 文档
164 lines
5.4 KiB
JavaScript
164 lines
5.4 KiB
JavaScript
#!/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('完成。');
|