aicodeswitch 5.2.11 → 5.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1367 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
12
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
13
+ var m = o[Symbol.asyncIterator], i;
14
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
15
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
16
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
+ };
18
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
19
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
20
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
21
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
22
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
23
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
24
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
25
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
26
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
27
+ function fulfill(value) { resume("next", value); }
28
+ function reject(value) { resume("throw", value); }
29
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
30
+ };
31
+ var __importDefault = (this && this.__importDefault) || function (mod) {
32
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.LogStore = void 0;
36
+ /**
37
+ * LogStore —— 追加写 NDJSON 日志存储
38
+ *
39
+ * 设计要点:
40
+ * - 每条日志一行(NDJSON),按 namespace 分目录,文件按日期 + 大小滚动,**只追加不重写**。
41
+ * - append 在 per-file 锁内「先 stat 取 size 作 offset,再 append」,保证字节偏移严格对应。
42
+ * - 会话索引 sessionRefs 存 {file, offset, length, timestamp, logId};追加写下偏移永远稳定,
43
+ * 按会话取日志 = 按字节范围随机读,零邻居解析。
44
+ * - 查询 getRecent/search 流式逐行扫,内存只持轻量描述符 + 当前页;切页后按字节范围回填正文。
45
+ * - 会话级删除用 tombstone(按 id),追加写无需重写文件。
46
+ *
47
+ * 仅依赖 node:fs / node:path / node:crypto,零原生依赖。
48
+ */
49
+ const fs_1 = require("fs");
50
+ const path_1 = __importDefault(require("path"));
51
+ const crypto_1 = __importDefault(require("crypto"));
52
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB 滚动新文件
53
+ const FLUSH_DELAY = 3000;
54
+ const FLUSH_THRESHOLD = 50;
55
+ const READ_CHUNK = 64 * 1024;
56
+ /** 从日志推导 sessionId(与 fs-database.extractSessionIdFromLog 同源逻辑) */
57
+ function extractSessionId(log) {
58
+ var _a;
59
+ const headers = log.headers || {};
60
+ const headerSessionId = headers['session-id'] || headers['session_id'];
61
+ if (typeof headerSessionId === 'string')
62
+ return headerSessionId;
63
+ if (log.body) {
64
+ try {
65
+ const body = typeof log.body === 'string' ? JSON.parse(log.body) : log.body;
66
+ if ((_a = body === null || body === void 0 ? void 0 : body.metadata) === null || _a === void 0 ? void 0 : _a.user_id) {
67
+ const userId = body.metadata.user_id;
68
+ try {
69
+ const parsed = JSON.parse(userId);
70
+ if (parsed && typeof parsed === 'object' && parsed.session_id) {
71
+ return parsed.session_id;
72
+ }
73
+ }
74
+ catch (_b) {
75
+ return userId;
76
+ }
77
+ }
78
+ }
79
+ catch (_c) {
80
+ // 忽略
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ function utcDate(ts) {
86
+ return new Date(ts).toISOString().split('T')[0];
87
+ }
88
+ /** 判断一条记录(描述符或已解析日志)是否命中字段筛选条件(AND 组合) */
89
+ function matchEntry(rec, filters) {
90
+ if (filters.targetType && rec.targetType !== filters.targetType)
91
+ return false;
92
+ if (filters.vendorId && rec.vendorId !== filters.vendorId)
93
+ return false;
94
+ if (filters.targetServiceId && rec.targetServiceId !== filters.targetServiceId)
95
+ return false;
96
+ if (filters.targetModel && rec.targetModel !== filters.targetModel)
97
+ return false;
98
+ if (filters.routeId && rec.routeId !== filters.routeId)
99
+ return false;
100
+ return true;
101
+ }
102
+ /** 筛选条件是否非空 */
103
+ function hasFilters(filters) {
104
+ return !!(filters && (filters.targetType || filters.vendorId || filters.targetServiceId || filters.targetModel || filters.routeId));
105
+ }
106
+ function nsDirName(ns) {
107
+ // 'global' -> 'global';'key:{keyId}' -> 直接用 keyId 作目录名(keyId 本身已带 key_ 前缀,避免 key-key_ 重复)
108
+ return ns === 'global' ? 'global' : ns.slice('key:'.length);
109
+ }
110
+ class LogStore {
111
+ constructor(rootDir) {
112
+ Object.defineProperty(this, "rootDir", {
113
+ enumerable: true,
114
+ configurable: true,
115
+ writable: true,
116
+ value: void 0
117
+ });
118
+ Object.defineProperty(this, "namespaces", {
119
+ enumerable: true,
120
+ configurable: true,
121
+ writable: true,
122
+ value: new Map()
123
+ });
124
+ Object.defineProperty(this, "initialized", {
125
+ enumerable: true,
126
+ configurable: true,
127
+ writable: true,
128
+ value: false
129
+ });
130
+ this.rootDir = rootDir;
131
+ }
132
+ /** 启动时加载所有已存在 namespace 的索引 */
133
+ init() {
134
+ return __awaiter(this, void 0, void 0, function* () {
135
+ if (this.initialized)
136
+ return;
137
+ this.initialized = true;
138
+ yield fs_1.promises.mkdir(this.rootDir, { recursive: true });
139
+ let entries = [];
140
+ try {
141
+ entries = yield fs_1.promises.readdir(this.rootDir);
142
+ }
143
+ catch (_a) {
144
+ entries = [];
145
+ }
146
+ for (const name of entries) {
147
+ if (name === 'legacy-backup')
148
+ continue;
149
+ const full = path_1.default.join(this.rootDir, name);
150
+ let stat;
151
+ try {
152
+ stat = yield fs_1.promises.stat(full);
153
+ }
154
+ catch (_b) {
155
+ continue;
156
+ }
157
+ if (!stat.isDirectory())
158
+ continue;
159
+ // 'global' 目录 → global;其余目录名即 keyId,重建 namespace = key:{keyId}
160
+ if (name === 'global') {
161
+ yield this.ensureNs('global');
162
+ }
163
+ else {
164
+ yield this.ensureNs(`key:${name}`);
165
+ }
166
+ }
167
+ });
168
+ }
169
+ ensureNs(ns) {
170
+ return __awaiter(this, void 0, void 0, function* () {
171
+ let st = this.namespaces.get(ns);
172
+ if (st && st.loaded)
173
+ return st;
174
+ const dir = path_1.default.join(this.rootDir, nsDirName(ns));
175
+ yield fs_1.promises.mkdir(dir, { recursive: true });
176
+ if (!st) {
177
+ st = {
178
+ name: ns,
179
+ dir,
180
+ shards: [],
181
+ sessionRefs: new Map(),
182
+ tombstones: new Set(),
183
+ timeline: [],
184
+ timelineReady: false,
185
+ appendLocks: new Map(),
186
+ flushTimer: null,
187
+ dirtyCount: 0,
188
+ loaded: false,
189
+ };
190
+ this.namespaces.set(ns, st);
191
+ }
192
+ yield this.loadNsState(st);
193
+ return st;
194
+ });
195
+ }
196
+ loadNsState(st) {
197
+ return __awaiter(this, void 0, void 0, function* () {
198
+ try {
199
+ const shardsRaw = yield fs_1.promises.readFile(path_1.default.join(st.dir, 'shards-index.json'), 'utf-8');
200
+ st.shards = JSON.parse(shardsRaw);
201
+ if (!Array.isArray(st.shards))
202
+ st.shards = [];
203
+ }
204
+ catch (_a) {
205
+ st.shards = [];
206
+ }
207
+ try {
208
+ const refsRaw = yield fs_1.promises.readFile(path_1.default.join(st.dir, 'session-index.json'), 'utf-8');
209
+ const parsed = JSON.parse(refsRaw);
210
+ st.sessionRefs = new Map(Array.isArray(parsed) ? parsed : Object.entries(parsed));
211
+ }
212
+ catch (_b) {
213
+ st.sessionRefs = new Map();
214
+ }
215
+ try {
216
+ const tombRaw = yield fs_1.promises.readFile(path_1.default.join(st.dir, 'tombstones.json'), 'utf-8');
217
+ const parsed = JSON.parse(tombRaw);
218
+ st.tombstones = new Set(Array.isArray(parsed) ? parsed : []);
219
+ }
220
+ catch (_c) {
221
+ st.tombstones = new Set();
222
+ }
223
+ // 时间线索引:加载 sidecar 并做 count 一致性校验
224
+ const expectedCount = st.shards.reduce((sum, s) => sum + s.count, 0);
225
+ let loaded = null;
226
+ try {
227
+ const tlRaw = yield fs_1.promises.readFile(path_1.default.join(st.dir, 'timeline-index.json'), 'utf-8');
228
+ const parsed = JSON.parse(tlRaw);
229
+ if (Array.isArray(parsed))
230
+ loaded = parsed;
231
+ }
232
+ catch (_d) {
233
+ loaded = null;
234
+ }
235
+ if (loaded && loaded.length === expectedCount) {
236
+ st.timeline = loaded;
237
+ st.timelineReady = true;
238
+ }
239
+ else {
240
+ // sidecar 缺失/过期:置空,后台重建;期间查询回退扫描
241
+ st.timeline = [];
242
+ st.timelineReady = false;
243
+ if (expectedCount > 0) {
244
+ void this.rebuildTimeline(st);
245
+ }
246
+ else {
247
+ st.timelineReady = true; // 空库视为就绪
248
+ }
249
+ }
250
+ st.loaded = true;
251
+ });
252
+ }
253
+ /**
254
+ * 后台一次性重建时间线索引:按 startTime 升序遍历分片,逐行取 id/ts/筛选字段。
255
+ * 仅在 sidecar 缺失/过期时触发;重建完成后 flush 落盘。
256
+ */
257
+ rebuildTimeline(st) {
258
+ return __awaiter(this, void 0, void 0, function* () {
259
+ var _a, e_1, _b, _c;
260
+ try {
261
+ const sorted = [...st.shards].sort((a, b) => a.startTime - b.startTime);
262
+ const timeline = [];
263
+ for (const shard of sorted) {
264
+ try {
265
+ for (var _d = true, _e = (e_1 = void 0, __asyncValues(this.readLines(st, shard.filename))), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
266
+ _c = _f.value;
267
+ _d = false;
268
+ const ln = _c;
269
+ let log;
270
+ try {
271
+ log = JSON.parse(ln.text);
272
+ }
273
+ catch (_g) {
274
+ continue;
275
+ }
276
+ timeline.push({
277
+ file: shard.filename,
278
+ offset: ln.offset,
279
+ length: ln.length,
280
+ ts: log.timestamp || 0,
281
+ id: log.id,
282
+ targetType: log.targetType,
283
+ vendorId: log.vendorId,
284
+ targetServiceId: log.targetServiceId,
285
+ targetModel: log.targetModel,
286
+ routeId: log.routeId,
287
+ });
288
+ }
289
+ }
290
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
291
+ finally {
292
+ try {
293
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
294
+ }
295
+ finally { if (e_1) throw e_1.error; }
296
+ }
297
+ }
298
+ st.timeline = timeline;
299
+ st.timelineReady = true;
300
+ this.scheduleFlush(st);
301
+ }
302
+ catch (_h) {
303
+ // 重建失败:保持 timelineReady=false,查询继续走扫描回退
304
+ }
305
+ });
306
+ }
307
+ // ============ 写入 ============
308
+ append(ns, logIn) {
309
+ return __awaiter(this, void 0, void 0, function* () {
310
+ const st = yield this.ensureNs(ns);
311
+ const id = logIn.id || crypto_1.default.randomUUID();
312
+ const log = Object.assign(Object.assign({}, logIn), { id });
313
+ const dateStr = utcDate(log.timestamp);
314
+ const filename = this.pickActiveFile(st, dateStr);
315
+ const lockKey = filename;
316
+ const prev = st.appendLocks.get(lockKey) || Promise.resolve();
317
+ const work = prev.then(() => __awaiter(this, void 0, void 0, function* () {
318
+ const filePath = path_1.default.join(st.dir, filename);
319
+ const line = Buffer.from(JSON.stringify(log) + '\n', 'utf8');
320
+ let offset = 0;
321
+ try {
322
+ const stat = yield fs_1.promises.stat(filePath);
323
+ offset = stat.size;
324
+ }
325
+ catch (_a) {
326
+ offset = 0;
327
+ }
328
+ yield fs_1.promises.appendFile(filePath, line);
329
+ let meta = st.shards.find(s => s.filename === filename);
330
+ if (!meta) {
331
+ meta = {
332
+ filename,
333
+ date: dateStr,
334
+ startTime: log.timestamp,
335
+ endTime: log.timestamp,
336
+ count: 0,
337
+ size: 0,
338
+ };
339
+ st.shards.push(meta);
340
+ }
341
+ meta.count += 1;
342
+ if (log.timestamp < meta.startTime)
343
+ meta.startTime = log.timestamp;
344
+ if (log.timestamp > meta.endTime)
345
+ meta.endTime = log.timestamp;
346
+ meta.size = offset + line.length;
347
+ const sid = extractSessionId(log);
348
+ if (sid) {
349
+ const ref = {
350
+ file: filename,
351
+ offset,
352
+ length: line.length - 1,
353
+ timestamp: log.timestamp,
354
+ logId: id,
355
+ };
356
+ let arr = st.sessionRefs.get(sid);
357
+ if (!arr) {
358
+ arr = [];
359
+ st.sessionRefs.set(sid, arr);
360
+ }
361
+ arr.push(ref);
362
+ }
363
+ // 时间线索引:append 顺序 push(最旧在前),查询时反向遍历
364
+ st.timeline.push({
365
+ file: filename,
366
+ offset,
367
+ length: line.length - 1,
368
+ ts: log.timestamp,
369
+ id,
370
+ targetType: log.targetType,
371
+ vendorId: log.vendorId,
372
+ targetServiceId: log.targetServiceId,
373
+ targetModel: log.targetModel,
374
+ routeId: log.routeId,
375
+ });
376
+ st.timelineReady = true;
377
+ this.scheduleFlush(st);
378
+ const res = { id, namespace: ns, file: filename, offset, length: line.length - 1 };
379
+ return res;
380
+ }));
381
+ st.appendLocks.set(lockKey, work);
382
+ try {
383
+ return yield work;
384
+ }
385
+ finally {
386
+ if (st.appendLocks.get(lockKey) === work)
387
+ st.appendLocks.delete(lockKey);
388
+ }
389
+ });
390
+ }
391
+ pickActiveFile(st, dateStr) {
392
+ const sameDay = st.shards.filter(s => s.date === dateStr);
393
+ for (let i = sameDay.length - 1; i >= 0; i--) {
394
+ if (sameDay[i].size < MAX_FILE_SIZE)
395
+ return sameDay[i].filename;
396
+ }
397
+ const seq = sameDay.length;
398
+ return seq === 0 ? `${dateStr}.ndjson` : `${dateStr}.${seq}.ndjson`;
399
+ }
400
+ // ============ 查询 ============
401
+ count(ns) {
402
+ return __awaiter(this, void 0, void 0, function* () {
403
+ const st = yield this.ensureNs(ns);
404
+ return st.shards.reduce((sum, s) => sum + s.count, 0);
405
+ });
406
+ }
407
+ /** 最近日志(newest-first,分页)。优先走时间线索引(零扫描),未就绪时回退扫描。 */
408
+ getRecent(ns_1) {
409
+ return __awaiter(this, arguments, void 0, function* (ns, opts = {}) {
410
+ var _a, _b;
411
+ const st = yield this.ensureNs(ns);
412
+ if (!st.timelineReady)
413
+ return this.getRecentScan(st, opts);
414
+ const limit = (_a = opts.limit) !== null && _a !== void 0 ? _a : 100;
415
+ const offset = (_b = opts.offset) !== null && _b !== void 0 ? _b : 0;
416
+ const since = opts.since;
417
+ const page = [];
418
+ let skipped = 0;
419
+ // timeline 为 append 顺序(最旧在前),反向遍历得到 newest-first
420
+ for (let i = st.timeline.length - 1; i >= 0 && page.length < limit; i--) {
421
+ const r = st.timeline[i];
422
+ if (since != null && r.ts < since)
423
+ break; // newest-first:一旦早于 since 即终止
424
+ if (st.tombstones.has(r.id))
425
+ continue;
426
+ if (skipped < offset) {
427
+ skipped++;
428
+ continue;
429
+ }
430
+ page.push(r);
431
+ }
432
+ return this.hydrate(st, page);
433
+ });
434
+ }
435
+ /** getRecent 的扫描回退实现(冷启动索引未就绪时使用) */
436
+ getRecentScan(st, opts) {
437
+ return __awaiter(this, void 0, void 0, function* () {
438
+ var _a, e_2, _b, _c;
439
+ var _d, _e;
440
+ const limit = (_d = opts.limit) !== null && _d !== void 0 ? _d : 100;
441
+ const offset = (_e = opts.offset) !== null && _e !== void 0 ? _e : 0;
442
+ const since = opts.since;
443
+ const need = offset + limit;
444
+ const collected = [];
445
+ const sorted = [...st.shards].sort((a, b) => b.endTime - a.endTime);
446
+ for (const shard of sorted) {
447
+ if (since != null && shard.endTime < since)
448
+ break; // 整片早于 since,跳过
449
+ if (collected.length >= need)
450
+ break;
451
+ try {
452
+ for (var _f = true, _g = (e_2 = void 0, __asyncValues(this.readLines(st, shard.filename))), _h; _h = yield _g.next(), _a = _h.done, !_a; _f = true) {
453
+ _c = _h.value;
454
+ _f = false;
455
+ const ln = _c;
456
+ let log;
457
+ try {
458
+ log = JSON.parse(ln.text);
459
+ }
460
+ catch (_j) {
461
+ continue; // 坏行跳过(NDJSON 逐行容错)
462
+ }
463
+ if (since != null && (log.timestamp || 0) < since)
464
+ continue;
465
+ if (st.tombstones.has(log.id))
466
+ continue;
467
+ collected.push({ file: shard.filename, offset: ln.offset, length: ln.length, ts: log.timestamp || 0, id: log.id });
468
+ }
469
+ }
470
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
471
+ finally {
472
+ try {
473
+ if (!_f && !_a && (_b = _g.return)) yield _b.call(_g);
474
+ }
475
+ finally { if (e_2) throw e_2.error; }
476
+ }
477
+ }
478
+ collected.sort((a, b) => b.ts - a.ts);
479
+ const page = collected.slice(offset, offset + limit);
480
+ return this.hydrate(st, page);
481
+ });
482
+ }
483
+ /**
484
+ * 统一查询:字段筛选 + 关键词 + 时间窗 + 分页 + 全量命中总数。
485
+ * - 无关键词且索引就绪:走快路径,在描述符上直接筛选(零全量扫描、零 JSON.parse)。
486
+ * - 有关键词或索引未就绪:回退扫描,match 谓词同时检查字段筛选与正文子串。
487
+ */
488
+ query(ns, opts) {
489
+ return __awaiter(this, void 0, void 0, function* () {
490
+ const st = yield this.ensureNs(ns);
491
+ const limit = opts.limit;
492
+ const offset = opts.offset;
493
+ const filters = opts.filters;
494
+ const keyword = (opts.keyword || '').toLowerCase().trim();
495
+ // 快路径:无关键词且索引就绪
496
+ if (!keyword && st.timelineReady) {
497
+ const need = offset + limit;
498
+ const page = [];
499
+ let total = 0;
500
+ for (let i = st.timeline.length - 1; i >= 0; i--) {
501
+ const r = st.timeline[i];
502
+ if (opts.since != null && r.ts < opts.since)
503
+ break;
504
+ if (opts.until != null && r.ts > opts.until)
505
+ continue;
506
+ if (st.tombstones.has(r.id))
507
+ continue;
508
+ if (hasFilters(filters) && !matchEntry(r, filters))
509
+ continue;
510
+ total++;
511
+ if (page.length < need)
512
+ page.push(r);
513
+ }
514
+ const slice = page.slice(offset, offset + limit);
515
+ const data = yield this.hydrate(st, slice);
516
+ return { data, total };
517
+ }
518
+ // 慢路径:扫描(关键词必须查正文,或索引未就绪)
519
+ return this.queryScan(st, opts, keyword);
520
+ });
521
+ }
522
+ /** query 的扫描回退实现 */
523
+ queryScan(st, opts, keyword) {
524
+ return __awaiter(this, void 0, void 0, function* () {
525
+ var _a, e_3, _b, _c;
526
+ const limit = opts.limit;
527
+ const offset = opts.offset;
528
+ const filters = opts.filters;
529
+ const need = offset + limit;
530
+ const page = [];
531
+ let total = 0;
532
+ const sorted = [...st.shards].sort((a, b) => b.endTime - a.endTime);
533
+ for (const shard of sorted) {
534
+ if (opts.since != null && shard.endTime < opts.since)
535
+ break;
536
+ if (opts.until != null && shard.startTime > opts.until)
537
+ continue;
538
+ try {
539
+ for (var _d = true, _e = (e_3 = void 0, __asyncValues(this.readLines(st, shard.filename))), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
540
+ _c = _f.value;
541
+ _d = false;
542
+ const ln = _c;
543
+ // 关键词先在原始文本上做廉价 substring 命中
544
+ if (keyword && !ln.text.toLowerCase().includes(keyword))
545
+ continue;
546
+ let log;
547
+ try {
548
+ log = JSON.parse(ln.text);
549
+ }
550
+ catch (_g) {
551
+ continue;
552
+ }
553
+ const ts = log.timestamp || 0;
554
+ if (opts.since != null && ts < opts.since)
555
+ continue;
556
+ if (opts.until != null && ts > opts.until)
557
+ continue;
558
+ if (st.tombstones.has(log.id))
559
+ continue;
560
+ if (hasFilters(filters) && !matchEntry(log, filters))
561
+ continue;
562
+ total++;
563
+ if (page.length < need) {
564
+ page.push({ file: shard.filename, offset: ln.offset, length: ln.length, ts, id: log.id });
565
+ }
566
+ }
567
+ }
568
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
569
+ finally {
570
+ try {
571
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
572
+ }
573
+ finally { if (e_3) throw e_3.error; }
574
+ }
575
+ }
576
+ page.sort((a, b) => b.ts - a.ts);
577
+ const slice = page.slice(offset, offset + limit);
578
+ const data = yield this.hydrate(st, slice);
579
+ return { data, total };
580
+ });
581
+ }
582
+ getBySession(ns_1, sessionId_1) {
583
+ return __awaiter(this, arguments, void 0, function* (ns, sessionId, opts = {}) {
584
+ var _a, _b;
585
+ const st = yield this.ensureNs(ns);
586
+ const limit = (_a = opts.limit) !== null && _a !== void 0 ? _a : 100;
587
+ const offset = (_b = opts.offset) !== null && _b !== void 0 ? _b : 0;
588
+ const since = opts.since;
589
+ let refs = st.sessionRefs.get(sessionId) || [];
590
+ if (since != null)
591
+ refs = refs.filter(r => r.timestamp >= since);
592
+ const filtered = [...refs].filter(r => !st.tombstones.has(r.logId));
593
+ filtered.sort((a, b) => b.timestamp - a.timestamp);
594
+ const page = filtered.slice(offset, offset + limit);
595
+ return this.hydrate(st, page);
596
+ });
597
+ }
598
+ /** 批量按会话取近期日志(跨会话合并文件读取),用于 Agent Map 启动重建。 */
599
+ getBySessionsBatch(ns_1, sessionIds_1) {
600
+ return __awaiter(this, arguments, void 0, function* (ns, sessionIds, opts = {}) {
601
+ var _a;
602
+ const st = yield this.ensureNs(ns);
603
+ const result = new Map();
604
+ if (sessionIds.length === 0)
605
+ return result;
606
+ const since = opts.since;
607
+ const perLimit = (_a = opts.perSessionLimit) !== null && _a !== void 0 ? _a : 100;
608
+ const picked = [];
609
+ for (const sid of sessionIds) {
610
+ let refs = st.sessionRefs.get(sid) || [];
611
+ if (since != null)
612
+ refs = refs.filter(r => r.timestamp >= since);
613
+ refs = refs.filter(r => !st.tombstones.has(r.logId));
614
+ refs.sort((a, b) => a.timestamp - b.timestamp);
615
+ const tail = refs.slice(-perLimit);
616
+ for (const r of tail)
617
+ picked.push(Object.assign(Object.assign({}, r), { sessionId: sid }));
618
+ }
619
+ if (picked.length === 0)
620
+ return result;
621
+ // 2) 按文件分组,每文件打开一次
622
+ const byFile = new Map();
623
+ for (const p of picked) {
624
+ let arr = byFile.get(p.file);
625
+ if (!arr) {
626
+ arr = [];
627
+ byFile.set(p.file, arr);
628
+ }
629
+ arr.push(p);
630
+ }
631
+ const buckets = new Map();
632
+ for (const [file, group] of byFile) {
633
+ const fd = yield fs_1.promises.open(path_1.default.join(st.dir, file), 'r');
634
+ try {
635
+ for (const p of group) {
636
+ const buf = Buffer.alloc(p.length);
637
+ yield fd.read(buf, 0, p.length, p.offset);
638
+ let log;
639
+ try {
640
+ log = JSON.parse(buf.toString('utf8'));
641
+ }
642
+ catch (_b) {
643
+ continue;
644
+ }
645
+ let bucket = buckets.get(p.sessionId);
646
+ if (!bucket) {
647
+ bucket = [];
648
+ buckets.set(p.sessionId, bucket);
649
+ }
650
+ bucket.push(log);
651
+ }
652
+ }
653
+ finally {
654
+ yield fd.close();
655
+ }
656
+ }
657
+ for (const [sid, logs] of buckets) {
658
+ logs.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
659
+ result.set(sid, logs);
660
+ }
661
+ return result;
662
+ });
663
+ }
664
+ /** 两阶段搜索:流式收集匹配描述符 → 切页 → 字节范围回填正文。 */
665
+ search(ns_1, query_1) {
666
+ return __awaiter(this, arguments, void 0, function* (ns, query, opts = {}) {
667
+ var _a, e_4, _b, _c;
668
+ var _d, _e;
669
+ const st = yield this.ensureNs(ns);
670
+ const q = (query || '').toLowerCase().trim();
671
+ if (!q)
672
+ return this.getRecent(ns, opts);
673
+ const limit = (_d = opts.limit) !== null && _d !== void 0 ? _d : 100;
674
+ const offset = (_e = opts.offset) !== null && _e !== void 0 ? _e : 0;
675
+ const need = offset + limit;
676
+ const matches = [];
677
+ const sorted = [...st.shards].sort((a, b) => b.endTime - a.endTime);
678
+ for (const shard of sorted) {
679
+ if (matches.length >= need)
680
+ break;
681
+ try {
682
+ for (var _f = true, _g = (e_4 = void 0, __asyncValues(this.readLines(st, shard.filename))), _h; _h = yield _g.next(), _a = _h.done, !_a; _f = true) {
683
+ _c = _h.value;
684
+ _f = false;
685
+ const ln = _c;
686
+ // 先在原始文本上做廉价 substring 命中,命中再 parse
687
+ if (!ln.text.toLowerCase().includes(q))
688
+ continue;
689
+ let log;
690
+ try {
691
+ log = JSON.parse(ln.text);
692
+ }
693
+ catch (_j) {
694
+ continue;
695
+ }
696
+ if (st.tombstones.has(log.id))
697
+ continue;
698
+ matches.push({ file: shard.filename, offset: ln.offset, length: ln.length, ts: log.timestamp || 0, id: log.id });
699
+ }
700
+ }
701
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
702
+ finally {
703
+ try {
704
+ if (!_f && !_a && (_b = _g.return)) yield _b.call(_g);
705
+ }
706
+ finally { if (e_4) throw e_4.error; }
707
+ }
708
+ }
709
+ matches.sort((a, b) => b.ts - a.ts);
710
+ const page = matches.slice(offset, offset + limit);
711
+ return this.hydrate(st, page);
712
+ });
713
+ }
714
+ /** search 的匹配计数(流式,不持正文) */
715
+ searchCount(ns, query) {
716
+ return __awaiter(this, void 0, void 0, function* () {
717
+ var _a, e_5, _b, _c;
718
+ const st = yield this.ensureNs(ns);
719
+ const q = (query || '').toLowerCase().trim();
720
+ if (!q)
721
+ return this.count(ns);
722
+ let n = 0;
723
+ const sorted = [...st.shards].sort((a, b) => b.endTime - a.endTime);
724
+ for (const shard of sorted) {
725
+ try {
726
+ for (var _d = true, _e = (e_5 = void 0, __asyncValues(this.readLines(st, shard.filename))), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
727
+ _c = _f.value;
728
+ _d = false;
729
+ const ln = _c;
730
+ if (!ln.text.toLowerCase().includes(q))
731
+ continue;
732
+ n++;
733
+ }
734
+ }
735
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
736
+ finally {
737
+ try {
738
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
739
+ }
740
+ finally { if (e_5) throw e_5.error; }
741
+ }
742
+ }
743
+ return n;
744
+ });
745
+ }
746
+ /** 按字节范围回填一组 ref 的正文,按文件分组、每文件打开一次 */
747
+ hydrate(st, refs) {
748
+ return __awaiter(this, void 0, void 0, function* () {
749
+ if (refs.length === 0)
750
+ return [];
751
+ const byFile = new Map();
752
+ for (let i = 0; i < refs.length; i++) {
753
+ const ref = refs[i];
754
+ let arr = byFile.get(ref.file);
755
+ if (!arr) {
756
+ arr = [];
757
+ byFile.set(ref.file, arr);
758
+ }
759
+ arr.push({ idx: i, ref });
760
+ }
761
+ const out = new Array(refs.length);
762
+ for (const [file, group] of byFile) {
763
+ const fd = yield fs_1.promises.open(path_1.default.join(st.dir, file), 'r');
764
+ try {
765
+ for (const g of group) {
766
+ const buf = Buffer.alloc(g.ref.length);
767
+ yield fd.read(buf, 0, g.ref.length, g.ref.offset);
768
+ try {
769
+ out[g.idx] = JSON.parse(buf.toString('utf8'));
770
+ }
771
+ catch (_a) {
772
+ // 坏行跳过
773
+ }
774
+ }
775
+ }
776
+ finally {
777
+ yield fd.close();
778
+ }
779
+ }
780
+ return out.filter(Boolean);
781
+ });
782
+ }
783
+ // ============ 流式逐行读(带字节偏移) ============
784
+ readLines(st, filename) {
785
+ return __asyncGenerator(this, arguments, function* readLines_1() {
786
+ const filePath = path_1.default.join(st.dir, filename);
787
+ let fd;
788
+ try {
789
+ fd = yield __await(fs_1.promises.open(filePath, 'r'));
790
+ }
791
+ catch (_a) {
792
+ return yield __await(void 0);
793
+ }
794
+ try {
795
+ let carry = Buffer.alloc(0);
796
+ let carryOffset = 0;
797
+ let filePos = 0;
798
+ while (true) {
799
+ const buf = Buffer.alloc(READ_CHUNK);
800
+ const { bytesRead } = yield __await(fd.read(buf, 0, READ_CHUNK, filePos));
801
+ if (bytesRead === 0) {
802
+ if (carry.length > 0) {
803
+ yield yield __await({ text: carry.toString('utf8'), offset: carryOffset, length: carry.length });
804
+ carry = Buffer.alloc(0);
805
+ }
806
+ break;
807
+ }
808
+ const data = buf.subarray(0, bytesRead);
809
+ const block = carry.length > 0 ? Buffer.concat([carry, data]) : data;
810
+ const blockStart = carryOffset;
811
+ let searchFrom = 0;
812
+ let nl = block.indexOf(0x0a, searchFrom);
813
+ while (nl !== -1) {
814
+ const lineBuf = block.subarray(searchFrom, nl);
815
+ yield yield __await({
816
+ text: lineBuf.toString('utf8'),
817
+ offset: blockStart + searchFrom,
818
+ length: lineBuf.length,
819
+ });
820
+ searchFrom = nl + 1;
821
+ nl = block.indexOf(0x0a, searchFrom);
822
+ }
823
+ carry = block.subarray(searchFrom);
824
+ carryOffset = blockStart + searchFrom;
825
+ filePos += bytesRead;
826
+ }
827
+ }
828
+ finally {
829
+ yield __await(fd.close());
830
+ }
831
+ });
832
+ }
833
+ // ============ 删除 / 清理 ============
834
+ /** 按会话删除日志(tombstone + 清理 sessionRefs) */
835
+ deleteLogsBySession(ns, sessionId) {
836
+ return __awaiter(this, void 0, void 0, function* () {
837
+ const st = yield this.ensureNs(ns);
838
+ const refs = st.sessionRefs.get(sessionId) || [];
839
+ let n = 0;
840
+ for (const r of refs) {
841
+ if (!st.tombstones.has(r.logId)) {
842
+ st.tombstones.add(r.logId);
843
+ n++;
844
+ }
845
+ }
846
+ st.sessionRefs.delete(sessionId);
847
+ if (n > 0)
848
+ this.scheduleFlush(st);
849
+ return n;
850
+ });
851
+ }
852
+ /** 按 id 批量删除(tombstone) */
853
+ deleteLogsByIds(ns, ids) {
854
+ return __awaiter(this, void 0, void 0, function* () {
855
+ const st = yield this.ensureNs(ns);
856
+ let n = 0;
857
+ for (const id of ids) {
858
+ if (id && !st.tombstones.has(id)) {
859
+ st.tombstones.add(id);
860
+ n++;
861
+ }
862
+ }
863
+ if (n > 0)
864
+ this.scheduleFlush(st);
865
+ return n;
866
+ });
867
+ }
868
+ /** 清空某 namespace 全部日志 */
869
+ clear(ns) {
870
+ return __awaiter(this, void 0, void 0, function* () {
871
+ const st = yield this.ensureNs(ns);
872
+ for (const s of st.shards) {
873
+ try {
874
+ yield fs_1.promises.unlink(path_1.default.join(st.dir, s.filename));
875
+ }
876
+ catch (_a) {
877
+ // 忽略
878
+ }
879
+ }
880
+ st.shards = [];
881
+ st.sessionRefs.clear();
882
+ st.tombstones.clear();
883
+ st.timeline = [];
884
+ st.timelineReady = true;
885
+ yield this.flushNow(st);
886
+ });
887
+ }
888
+ /** 保留最近 N 天,删除更早的分片文件(整文件删,ref 自带 file,零逐条修正) */
889
+ retain(ns, days) {
890
+ return __awaiter(this, void 0, void 0, function* () {
891
+ const st = yield this.ensureNs(ns);
892
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
893
+ const toDelete = st.shards.filter(s => s.endTime < cutoff);
894
+ if (toDelete.length === 0)
895
+ return 0;
896
+ const deleted = new Set(toDelete.map(s => s.filename));
897
+ for (const s of toDelete) {
898
+ try {
899
+ yield fs_1.promises.unlink(path_1.default.join(st.dir, s.filename));
900
+ }
901
+ catch (_a) {
902
+ // 忽略
903
+ }
904
+ }
905
+ st.shards = st.shards.filter(s => !deleted.has(s.filename));
906
+ // 清理指向已删文件的 sessionRefs
907
+ for (const [sid, refs] of st.sessionRefs) {
908
+ const remaining = refs.filter(r => !deleted.has(r.file));
909
+ if (remaining.length === 0)
910
+ st.sessionRefs.delete(sid);
911
+ else if (remaining.length < refs.length)
912
+ st.sessionRefs.set(sid, remaining);
913
+ }
914
+ // 清理时间线索引中指向已删文件的描述符
915
+ if (st.timeline.length > 0) {
916
+ st.timeline = st.timeline.filter(r => !deleted.has(r.file));
917
+ }
918
+ yield this.flushNow(st);
919
+ return toDelete.length;
920
+ });
921
+ }
922
+ /**
923
+ * 流式遍历某 namespace 的全部日志(逐行 parse,跳过坏行与 tombstone)。
924
+ * 供统计 contentType 分布重建、迁移校验等「需要扫全量」的场景;查询走 getRecent/search。
925
+ */
926
+ streamAll(ns_1) {
927
+ return __asyncGenerator(this, arguments, function* streamAll_1(ns, opts = {}) {
928
+ var _a, e_6, _b, _c;
929
+ const st = yield __await(this.ensureNs(ns));
930
+ const sorted = [...st.shards].sort((a, b) => a.startTime - b.startTime);
931
+ for (const shard of sorted) {
932
+ try {
933
+ for (var _d = true, _e = (e_6 = void 0, __asyncValues(this.readLines(st, shard.filename))), _f; _f = yield __await(_e.next()), _a = _f.done, !_a; _d = true) {
934
+ _c = _f.value;
935
+ _d = false;
936
+ const ln = _c;
937
+ let log;
938
+ try {
939
+ log = JSON.parse(ln.text);
940
+ }
941
+ catch (_g) {
942
+ continue;
943
+ }
944
+ if (opts.skipTombstone && log.id && st.tombstones.has(log.id))
945
+ continue;
946
+ yield yield __await(log);
947
+ }
948
+ }
949
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
950
+ finally {
951
+ try {
952
+ if (!_d && !_a && (_b = _e.return)) yield __await(_b.call(_e));
953
+ }
954
+ finally { if (e_6) throw e_6.error; }
955
+ }
956
+ }
957
+ });
958
+ }
959
+ /**
960
+ * 带过滤的分页查询:流式扫描全量,对每条日志应用 match 谓词(+ 可选 since/until 时间窗),
961
+ * 收集轻量描述符直到够一页,同时继续计数得到 total;最后按字节范围回填当前页。
962
+ * 返回 { data, total }。内存仅持描述符 + 当前页。
963
+ */
964
+ getFiltered(ns, opts) {
965
+ return __awaiter(this, void 0, void 0, function* () {
966
+ var _a, e_7, _b, _c;
967
+ const st = yield this.ensureNs(ns);
968
+ const limit = opts.limit;
969
+ const offset = opts.offset;
970
+ const need = offset + limit;
971
+ const page = [];
972
+ let total = 0;
973
+ const sorted = [...st.shards].sort((a, b) => b.endTime - a.endTime);
974
+ outer: for (const shard of sorted) {
975
+ if (opts.since != null && shard.endTime < opts.since)
976
+ break;
977
+ if (opts.until != null && shard.startTime > opts.until)
978
+ continue;
979
+ try {
980
+ for (var _d = true, _e = (e_7 = void 0, __asyncValues(this.readLines(st, shard.filename))), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
981
+ _c = _f.value;
982
+ _d = false;
983
+ const ln = _c;
984
+ let log;
985
+ try {
986
+ log = JSON.parse(ln.text);
987
+ }
988
+ catch (_g) {
989
+ continue;
990
+ }
991
+ const ts = log.timestamp || 0;
992
+ if (opts.since != null && ts < opts.since)
993
+ continue;
994
+ if (opts.until != null && ts > opts.until)
995
+ continue;
996
+ if (st.tombstones.has(log.id))
997
+ continue;
998
+ if (opts.match && !opts.match(log))
999
+ continue;
1000
+ total++;
1001
+ if (page.length < need) {
1002
+ page.push({ file: shard.filename, offset: ln.offset, length: ln.length, ts, id: log.id });
1003
+ }
1004
+ if (page.length >= need && opts.since == null && opts.until == null && !opts.match) {
1005
+ // 无过滤时可提前结束(total == page.length 满足分页即可)
1006
+ break outer;
1007
+ }
1008
+ }
1009
+ }
1010
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
1011
+ finally {
1012
+ try {
1013
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
1014
+ }
1015
+ finally { if (e_7) throw e_7.error; }
1016
+ }
1017
+ }
1018
+ page.sort((a, b) => b.ts - a.ts);
1019
+ const slice = page.slice(offset, offset + limit);
1020
+ const data = yield this.hydrate(st, slice);
1021
+ return { data, total };
1022
+ });
1023
+ }
1024
+ /** 列出所有已加载的 namespace(供定时清理遍历 AccessKey) */
1025
+ listNamespaces() {
1026
+ return Array.from(this.namespaces.keys());
1027
+ }
1028
+ // ============ 索引持久化(防抖) ============
1029
+ scheduleFlush(st) {
1030
+ st.dirtyCount += 1;
1031
+ if (st.dirtyCount >= FLUSH_THRESHOLD) {
1032
+ if (st.flushTimer) {
1033
+ clearTimeout(st.flushTimer);
1034
+ st.flushTimer = null;
1035
+ }
1036
+ this.flushNow(st).catch(() => { });
1037
+ return;
1038
+ }
1039
+ if (!st.flushTimer) {
1040
+ st.flushTimer = setTimeout(() => {
1041
+ st.flushTimer = null;
1042
+ this.flushNow(st).catch(() => { });
1043
+ }, FLUSH_DELAY);
1044
+ if (typeof st.flushTimer.unref === 'function')
1045
+ st.flushTimer.unref();
1046
+ }
1047
+ }
1048
+ flushNow(st) {
1049
+ return __awaiter(this, void 0, void 0, function* () {
1050
+ st.dirtyCount = 0;
1051
+ if (st.flushTimer) {
1052
+ clearTimeout(st.flushTimer);
1053
+ st.flushTimer = null;
1054
+ }
1055
+ const tmp1 = path_1.default.join(st.dir, '.tmp-shards-index.json');
1056
+ const tmp2 = path_1.default.join(st.dir, '.tmp-session-index.json');
1057
+ const tmp3 = path_1.default.join(st.dir, '.tmp-tombstones.json');
1058
+ const tmp4 = path_1.default.join(st.dir, '.tmp-timeline-index.json');
1059
+ yield fs_1.promises.writeFile(tmp1, JSON.stringify(st.shards));
1060
+ yield fs_1.promises.rename(tmp1, path_1.default.join(st.dir, 'shards-index.json'));
1061
+ yield fs_1.promises.writeFile(tmp2, JSON.stringify(Array.from(st.sessionRefs.entries())));
1062
+ yield fs_1.promises.rename(tmp2, path_1.default.join(st.dir, 'session-index.json'));
1063
+ yield fs_1.promises.writeFile(tmp3, JSON.stringify(Array.from(st.tombstones)));
1064
+ yield fs_1.promises.rename(tmp3, path_1.default.join(st.dir, 'tombstones.json'));
1065
+ yield fs_1.promises.writeFile(tmp4, JSON.stringify(st.timeline));
1066
+ yield fs_1.promises.rename(tmp4, path_1.default.join(st.dir, 'timeline-index.json'));
1067
+ });
1068
+ }
1069
+ /**
1070
+ * store 是否真的有数据文件(看实际 *.ndjson,不信 shards-index.json 元数据,
1071
+ * 防「索引还在、数据文件被删」的误判)。
1072
+ */
1073
+ storeHasData() {
1074
+ return __awaiter(this, void 0, void 0, function* () {
1075
+ let ents = [];
1076
+ try {
1077
+ ents = yield fs_1.promises.readdir(this.rootDir);
1078
+ }
1079
+ catch (_a) {
1080
+ return false;
1081
+ }
1082
+ for (const name of ents) {
1083
+ if (name === 'legacy-backup')
1084
+ continue;
1085
+ if (name.startsWith('.'))
1086
+ continue;
1087
+ const full = path_1.default.join(this.rootDir, name);
1088
+ try {
1089
+ const stat = yield fs_1.promises.stat(full);
1090
+ if (!stat.isDirectory())
1091
+ continue;
1092
+ }
1093
+ catch (_b) {
1094
+ continue;
1095
+ }
1096
+ try {
1097
+ const files = yield fs_1.promises.readdir(full);
1098
+ if (files.some(f => f.endsWith('.ndjson')))
1099
+ return true;
1100
+ }
1101
+ catch (_c) {
1102
+ // 忽略
1103
+ }
1104
+ }
1105
+ return false;
1106
+ });
1107
+ }
1108
+ /**
1109
+ * 流式收集某 namespace 已存在的全部 logId,供迁移去重。
1110
+ * 仅在迁移启动时为「有数据的 namespace」调用一次;store 空则返回空 Set。
1111
+ */
1112
+ collectIds(ns) {
1113
+ return __awaiter(this, void 0, void 0, function* () {
1114
+ var _a, e_8, _b, _c;
1115
+ const ids = new Set();
1116
+ try {
1117
+ try {
1118
+ for (var _d = true, _e = __asyncValues(this.streamAll(ns)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
1119
+ _c = _f.value;
1120
+ _d = false;
1121
+ const log = _c;
1122
+ if (log.id)
1123
+ ids.add(log.id);
1124
+ }
1125
+ }
1126
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
1127
+ finally {
1128
+ try {
1129
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
1130
+ }
1131
+ finally { if (e_8) throw e_8.error; }
1132
+ }
1133
+ }
1134
+ catch (_g) {
1135
+ // 忽略
1136
+ }
1137
+ return ids;
1138
+ });
1139
+ }
1140
+ /**
1141
+ * 稳健迁移旧 JSON 日志到 NDJSON(main.ts 在 listen 之前调用)。
1142
+ * - 主库:{dataPath}/logs.json(旧单文件)+ {dataPath}/logs/logs-*.json(旧分片)→ 'global'
1143
+ * - AccessKey:{dataPath}/key-logs/<keyId>/*.json(跳过 logs-index.json)→ 'key:{keyId}'
1144
+ *
1145
+ * 设计(幂等 + 非破坏性 + 内容感知):
1146
+ * - gate:仅当 `标记缺失 OR store 实际无数据文件` 才进入迁移。正常重启(标记在 + 数据在)直接跳过,不扫源。
1147
+ * - 去重:进入迁移时,先为每个有数据的 namespace 用 collectIds 建已存在 idSet,
1148
+ * 迁移时 idSet.has(id) 则跳过。于是「部分残留/中断续跑/标记过期重跑」都不产生重复。
1149
+ * - 非破坏性:不 wipe 任何目录;源头 logs/、key-logs/ 始终只读、永不移动/删除(见 UPGRADE.md)。
1150
+ * - 标记 `.log-store-migration` 仅作「已完成」提示,不单独决定跳过——必须同时 storeHasData()。
1151
+ */
1152
+ migrateLegacy(dataPath) {
1153
+ return __awaiter(this, void 0, void 0, function* () {
1154
+ yield fs_1.promises.mkdir(this.rootDir, { recursive: true });
1155
+ // 兼容:清理旧版本标记名
1156
+ try {
1157
+ yield fs_1.promises.unlink(path_1.default.join(this.rootDir, '.legacy-migrated'));
1158
+ }
1159
+ catch (_a) {
1160
+ // 无旧标记,忽略
1161
+ }
1162
+ const marker = path_1.default.join(this.rootDir, '.log-store-migration');
1163
+ let hasMarker = false;
1164
+ try {
1165
+ yield fs_1.promises.access(marker);
1166
+ hasMarker = true;
1167
+ }
1168
+ catch (_b) {
1169
+ hasMarker = false;
1170
+ }
1171
+ const hasData = yield this.storeHasData();
1172
+ // 快速路径:标记在 + store 真有数据 → 已完成,跳过(不扫源)
1173
+ if (hasMarker && hasData) {
1174
+ return { global: 0, keys: {} };
1175
+ }
1176
+ console.log(`[LogStore] migrate: start (marker=${hasMarker}, hasData=${hasData})`);
1177
+ // store 无实际数据文件时,可能残留指向已删文件的 stale 索引(shards-index.json 等)。
1178
+ // 清掉这些索引 + 内存态,避免「索引说有、文件没有」的幽灵;.ndjson 本就不存在,无数据丢失风险。
1179
+ if (!hasData) {
1180
+ for (const ns of this.listNamespaces()) {
1181
+ const dir = path_1.default.join(this.rootDir, nsDirName(ns));
1182
+ for (const idxFile of ['shards-index.json', 'session-index.json', 'tombstones.json', 'timeline-index.json']) {
1183
+ try {
1184
+ yield fs_1.promises.unlink(path_1.default.join(dir, idxFile));
1185
+ }
1186
+ catch (_c) {
1187
+ // 忽略
1188
+ }
1189
+ }
1190
+ }
1191
+ this.namespaces.clear();
1192
+ }
1193
+ // 为每个有数据的 namespace 建去重 idSet(防部分残留导致重复追加)
1194
+ const idSets = new Map();
1195
+ for (const ns of this.listNamespaces()) {
1196
+ try {
1197
+ const dir = path_1.default.join(this.rootDir, nsDirName(ns));
1198
+ const files = yield fs_1.promises.readdir(dir);
1199
+ if (files.some(f => f.endsWith('.ndjson'))) {
1200
+ idSets.set(ns, yield this.collectIds(ns));
1201
+ }
1202
+ }
1203
+ catch (_d) {
1204
+ // 忽略
1205
+ }
1206
+ }
1207
+ const ensureIdSet = (ns) => {
1208
+ let s = idSets.get(ns);
1209
+ if (!s) {
1210
+ s = new Set();
1211
+ idSets.set(ns, s);
1212
+ }
1213
+ return s;
1214
+ };
1215
+ const result = { global: 0, keys: {} };
1216
+ let sourceFiles = 0;
1217
+ const migrateFile = (ns, file) => __awaiter(this, void 0, void 0, function* () {
1218
+ // ★ 不变量:源头文件(logs/、key-logs/、logs.json)只读——本函数只 readFile,
1219
+ // 绝不 unlink / rm / rename 任何源文件,也不把源文件移动到别处。
1220
+ // 所有写入只落到 log-store/{namespace}/ 内。违反此约束会破坏用户数据可恢复性。
1221
+ if (file.startsWith(this.rootDir)) {
1222
+ console.error(`[LogStore] migrate: refuse to touch file inside log-store (${file}); source must be logs/ or key-logs/`);
1223
+ return 0;
1224
+ }
1225
+ let content;
1226
+ try {
1227
+ content = yield fs_1.promises.readFile(file, 'utf-8');
1228
+ }
1229
+ catch (_a) {
1230
+ return 0;
1231
+ }
1232
+ sourceFiles++;
1233
+ // 旧分片可能含 \x00 损坏:截断到首个 NUL
1234
+ const nul = content.indexOf('\x00');
1235
+ if (nul !== -1)
1236
+ content = content.substring(0, nul);
1237
+ let arr;
1238
+ try {
1239
+ arr = JSON.parse(content);
1240
+ if (!Array.isArray(arr))
1241
+ arr = [];
1242
+ }
1243
+ catch (_b) {
1244
+ console.warn(`[LogStore] migrate: skip unparseable file ${file}`);
1245
+ return 0;
1246
+ }
1247
+ const idSet = ensureIdSet(ns);
1248
+ let written = 0;
1249
+ let skippedDup = 0;
1250
+ for (const log of arr) {
1251
+ if (!log || typeof log !== 'object')
1252
+ continue;
1253
+ if (!log.id)
1254
+ log.id = crypto_1.default.randomUUID();
1255
+ if (typeof log.timestamp !== 'number')
1256
+ log.timestamp = Date.now();
1257
+ if (idSet.has(log.id)) {
1258
+ skippedDup++;
1259
+ continue; // 去重:已存在则跳过
1260
+ }
1261
+ try {
1262
+ yield this.append(ns, log);
1263
+ idSet.add(log.id);
1264
+ written++;
1265
+ }
1266
+ catch (err) {
1267
+ console.error(`[LogStore] migrate: append failed in ${file}`, err);
1268
+ }
1269
+ }
1270
+ if (skippedDup > 0) {
1271
+ console.log(`[LogStore] migrate: ${file} dedup-skipped=${skippedDup} written=${written}`);
1272
+ }
1273
+ return written;
1274
+ });
1275
+ // 主库:logs.json + logs/logs-*.json
1276
+ const oldSingle = path_1.default.join(dataPath, 'logs.json');
1277
+ try {
1278
+ yield fs_1.promises.access(oldSingle);
1279
+ result.global += yield migrateFile('global', oldSingle);
1280
+ }
1281
+ catch (_e) {
1282
+ // 无单文件
1283
+ }
1284
+ const oldLogsDir = path_1.default.join(dataPath, 'logs');
1285
+ try {
1286
+ const files = yield fs_1.promises.readdir(oldLogsDir);
1287
+ for (const f of files) {
1288
+ if (!f.endsWith('.json'))
1289
+ continue;
1290
+ if (!/^logs-.*\.json$/.test(f))
1291
+ continue;
1292
+ result.global += yield migrateFile('global', path_1.default.join(oldLogsDir, f));
1293
+ }
1294
+ }
1295
+ catch (_f) {
1296
+ // 无 logs 目录
1297
+ }
1298
+ // AccessKey:key-logs/<keyId>/*.json
1299
+ const keyRoot = path_1.default.join(dataPath, 'key-logs');
1300
+ try {
1301
+ const keyDirs = yield fs_1.promises.readdir(keyRoot);
1302
+ for (const kd of keyDirs) {
1303
+ const kdFull = path_1.default.join(keyRoot, kd);
1304
+ let st;
1305
+ try {
1306
+ st = yield fs_1.promises.stat(kdFull);
1307
+ }
1308
+ catch (_g) {
1309
+ continue;
1310
+ }
1311
+ if (!st.isDirectory())
1312
+ continue;
1313
+ const ns = `key:${kd}`;
1314
+ let files;
1315
+ try {
1316
+ files = yield fs_1.promises.readdir(kdFull);
1317
+ }
1318
+ catch (_h) {
1319
+ continue;
1320
+ }
1321
+ let n = 0;
1322
+ for (const f of files) {
1323
+ if (!f.endsWith('.json'))
1324
+ continue;
1325
+ if (f === 'logs-index.json')
1326
+ continue; // 跳过旧索引
1327
+ n += yield migrateFile(ns, path_1.default.join(kdFull, f));
1328
+ }
1329
+ if (n > 0)
1330
+ result.keys[kd] = (result.keys[kd] || 0) + n;
1331
+ }
1332
+ }
1333
+ catch (_j) {
1334
+ // 无 key-logs
1335
+ }
1336
+ // 落盘所有 namespace
1337
+ for (const nss of this.namespaces.values()) {
1338
+ try {
1339
+ yield this.flushNow(nss);
1340
+ }
1341
+ catch (_k) {
1342
+ // 忽略
1343
+ }
1344
+ }
1345
+ // 写标记(记录版本/时间/源文件数,便于排查;仍需 storeHasData 才会在下次跳过)
1346
+ yield fs_1.promises.writeFile(marker, JSON.stringify({ version: 1, finishedAt: Date.now(), sources: sourceFiles }));
1347
+ if (result.global > 0 || Object.keys(result.keys).length > 0 || !hasData) {
1348
+ console.log(`[LogStore] migration done: global=${result.global} keys=${JSON.stringify(result.keys)} sources=${sourceFiles}`);
1349
+ }
1350
+ return result;
1351
+ });
1352
+ }
1353
+ /** 关闭时强制落盘所有 namespace */
1354
+ close() {
1355
+ return __awaiter(this, void 0, void 0, function* () {
1356
+ for (const st of this.namespaces.values()) {
1357
+ try {
1358
+ yield this.flushNow(st);
1359
+ }
1360
+ catch (_a) {
1361
+ // 忽略
1362
+ }
1363
+ }
1364
+ });
1365
+ }
1366
+ }
1367
+ exports.LogStore = LogStore;