@lakphy/local-router 0.4.2 → 0.5.0

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.
package/dist/cli.js CHANGED
@@ -9285,13 +9285,13 @@ var require_src = __commonJS((exports) => {
9285
9285
  });
9286
9286
 
9287
9287
  // src/cli.ts
9288
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
9288
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
9289
9289
  import { setTimeout as sleep2 } from "timers/promises";
9290
9290
  import { parseArgs as parseArgs3 } from "util";
9291
9291
 
9292
9292
  // src/cli/config-command.ts
9293
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9294
- import { dirname as dirname3, join as join11 } from "path";
9293
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9294
+ import { dirname as dirname3, join as join12 } from "path";
9295
9295
  import { createInterface as createInterface4 } from "readline/promises";
9296
9296
  import { parseArgs as parseArgs2 } from "util";
9297
9297
 
@@ -10569,7 +10569,7 @@ function validateConfigOrThrow(config) {
10569
10569
  }
10570
10570
 
10571
10571
  // src/cli/process.ts
10572
- import { closeSync, openSync, readFileSync as readFileSync6, statSync } from "fs";
10572
+ import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
10573
10573
  import { setTimeout as sleep } from "timers/promises";
10574
10574
  import { parseArgs } from "util";
10575
10575
 
@@ -51809,10 +51809,23 @@ async function getLogMetrics(options) {
51809
51809
  }
51810
51810
 
51811
51811
  // src/log-query.ts
51812
- import { createReadStream as createReadStream2, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
51813
- import { join as join4, resolve as resolve4 } from "path";
51812
+ import { createReadStream as createReadStream3, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
51813
+ import { join as join5, resolve as resolve4 } from "path";
51814
51814
  import { createInterface as createInterface2 } from "readline";
51815
51815
 
51816
+ // src/log-index.ts
51817
+ import { Database } from "bun:sqlite";
51818
+ import {
51819
+ closeSync,
51820
+ createReadStream as createReadStream2,
51821
+ existsSync as existsSync3,
51822
+ mkdirSync as mkdirSync2,
51823
+ openSync,
51824
+ readSync,
51825
+ statSync
51826
+ } from "fs";
51827
+ import { join as join4 } from "path";
51828
+
51816
51829
  // src/log-session-identity.ts
51817
51830
  var USER_SESSION_DELIMITER = "_account__session_";
51818
51831
  function toRecord(value) {
@@ -51889,6 +51902,824 @@ function resolveLogSessionIdentity(requestBody) {
51889
51902
  };
51890
51903
  }
51891
51904
 
51905
+ // src/log-index.ts
51906
+ var SCHEMA_VERSION = 1;
51907
+ var MAX_INDEX_QUEUE = 20000;
51908
+ var INDEX_BATCH_SIZE = 250;
51909
+ var INDEX_FLUSH_DELAY_MS = 50;
51910
+ var LIKE_SEARCH_THRESHOLD = 2;
51911
+ var FTS_TOKEN_PATTERN = /[\p{L}\p{N}_-]+/gu;
51912
+ var singleton = null;
51913
+ function encodeBase64Url(value) {
51914
+ return Buffer.from(value, "utf-8").toString("base64url");
51915
+ }
51916
+ function decodeBase64Url(value) {
51917
+ return Buffer.from(value, "base64url").toString("utf-8");
51918
+ }
51919
+ function encodeOffsetLogEventId(date5, offset) {
51920
+ return encodeBase64Url(JSON.stringify({ v: 2, d: date5, o: offset }));
51921
+ }
51922
+ function decodeOffsetLogEventId(id) {
51923
+ try {
51924
+ const parsed = JSON.parse(decodeBase64Url(id));
51925
+ if (parsed.v !== 2 || typeof parsed.d !== "string" || !Number.isInteger(parsed.o)) {
51926
+ return null;
51927
+ }
51928
+ const offset = Number(parsed.o);
51929
+ if (offset < 0)
51930
+ return null;
51931
+ return { v: 2, date: parsed.d, offset };
51932
+ } catch {
51933
+ return null;
51934
+ }
51935
+ }
51936
+ function encodeCursor(data) {
51937
+ return encodeBase64Url(JSON.stringify(data));
51938
+ }
51939
+ function decodeCursor(raw2) {
51940
+ const parsed = JSON.parse(decodeBase64Url(raw2));
51941
+ if (parsed.v !== 2 || parsed.sort !== "time_desc" && parsed.sort !== "time_asc" || typeof parsed.id !== "string" || typeof parsed.queryHash !== "string" || !Number.isFinite(parsed.tsMs)) {
51942
+ throw new Error("cursor \u975E\u6CD5");
51943
+ }
51944
+ return parsed;
51945
+ }
51946
+ function toDayStart2(ms) {
51947
+ const date5 = new Date(ms);
51948
+ return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
51949
+ }
51950
+ function listDateStrings2(fromMs, toMs) {
51951
+ const result = [];
51952
+ for (let day = toDayStart2(fromMs);day <= toDayStart2(toMs); day += 24 * 60 * 60 * 1000) {
51953
+ result.push(new Date(day).toISOString().slice(0, 10));
51954
+ }
51955
+ return result;
51956
+ }
51957
+ function getStatusClass2(event) {
51958
+ if (event.error_type)
51959
+ return "network_error";
51960
+ const status = event.upstream_status ?? 0;
51961
+ if (status >= 200 && status < 300)
51962
+ return "2xx";
51963
+ if (status >= 400 && status < 500)
51964
+ return "4xx";
51965
+ if (status >= 500)
51966
+ return "5xx";
51967
+ return "network_error";
51968
+ }
51969
+ function isErrorEvent2(event) {
51970
+ if (event.error_type)
51971
+ return true;
51972
+ const status = event.upstream_status ?? 0;
51973
+ return status < 200 || status >= 400;
51974
+ }
51975
+ function getLevel(event) {
51976
+ return isErrorEvent2(event) ? "error" : "info";
51977
+ }
51978
+ function buildMessage(event) {
51979
+ if (event.error_message)
51980
+ return event.error_message;
51981
+ if (event.error_type)
51982
+ return event.error_type;
51983
+ const status = event.upstream_status ?? 0;
51984
+ return `${event.method} ${event.path} -> ${status}`;
51985
+ }
51986
+ function toPercent2(numerator, denominator) {
51987
+ if (denominator <= 0)
51988
+ return 0;
51989
+ return Number((numerator / denominator * 100).toFixed(2));
51990
+ }
51991
+ function hashQuery(query) {
51992
+ const stable = {
51993
+ fromMs: query.fromMs,
51994
+ toMs: query.toMs,
51995
+ levels: [...query.levels].sort(),
51996
+ providers: [...query.providers].sort(),
51997
+ routeTypes: [...query.routeTypes].sort(),
51998
+ models: [...query.models].sort(),
51999
+ modelIns: [...query.modelIns].sort(),
52000
+ modelOuts: [...query.modelOuts].sort(),
52001
+ users: [...query.users].sort(),
52002
+ sessions: [...query.sessions].sort(),
52003
+ statusClasses: [...query.statusClasses].sort(),
52004
+ hasError: query.hasError,
52005
+ q: query.q
52006
+ };
52007
+ return Bun.hash(JSON.stringify(stable)).toString(36);
52008
+ }
52009
+ function buildSearchText(event) {
52010
+ const identity = resolveLogSessionIdentity(event.request_body);
52011
+ return [
52012
+ event.request_id,
52013
+ event.path,
52014
+ event.provider,
52015
+ event.model_in,
52016
+ event.model_out,
52017
+ event.route_type,
52018
+ identity.userIdRaw ?? "",
52019
+ identity.userKey ?? "",
52020
+ identity.sessionId ?? "",
52021
+ event.error_type ?? "",
52022
+ event.error_message ?? "",
52023
+ buildMessage(event)
52024
+ ].join(" ").toLowerCase();
52025
+ }
52026
+ function buildFtsQuery(q) {
52027
+ const tokens = q.match(FTS_TOKEN_PATTERN)?.map((token2) => token2.trim()).filter(Boolean) ?? [];
52028
+ if (tokens.length === 0)
52029
+ return null;
52030
+ return tokens.map((token2) => `"${token2.replaceAll('"', '""')}"`).join(" AND ");
52031
+ }
52032
+ function shouldUseFts(q) {
52033
+ return q.trim().length >= LIKE_SEARCH_THRESHOLD && buildFtsQuery(q) !== null;
52034
+ }
52035
+ function escapeLikePattern(value) {
52036
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
52037
+ }
52038
+ function eventToRow(input) {
52039
+ const { event } = input;
52040
+ if (!event.ts_start)
52041
+ return null;
52042
+ const tsMs = Date.parse(event.ts_start);
52043
+ if (!Number.isFinite(tsMs))
52044
+ return null;
52045
+ const identity = resolveLogSessionIdentity(event.request_body);
52046
+ const level = getLevel(event);
52047
+ const statusClass = getStatusClass2(event);
52048
+ const latencyMs = Math.max(0, event.latency_ms ?? 0);
52049
+ const model = event.model_out || event.model_in;
52050
+ return {
52051
+ id: input.id,
52052
+ ts_ms: tsMs,
52053
+ ts_start: event.ts_start,
52054
+ level,
52055
+ provider: event.provider,
52056
+ route_type: event.route_type,
52057
+ model,
52058
+ model_in: event.model_in,
52059
+ model_out: event.model_out,
52060
+ path: event.path,
52061
+ request_id: event.request_id,
52062
+ latency_ms: latencyMs,
52063
+ upstream_status: event.upstream_status ?? 0,
52064
+ status_class: statusClass,
52065
+ has_error: level === "error" ? 1 : 0,
52066
+ message: buildMessage(event),
52067
+ error_type: event.error_type,
52068
+ has_metadata: identity.hasMetadata ? 1 : 0,
52069
+ user_id_raw: identity.userIdRaw,
52070
+ user_key: identity.userKey,
52071
+ session_id: identity.sessionId,
52072
+ source_date: input.date,
52073
+ source_file: input.filePath,
52074
+ line_number: input.lineNumber,
52075
+ byte_offset: input.offset,
52076
+ byte_length: input.byteLength,
52077
+ search_text: buildSearchText(event)
52078
+ };
52079
+ }
52080
+ function rowToSummary(row) {
52081
+ return {
52082
+ id: row.id,
52083
+ ts: row.ts_start,
52084
+ level: row.level,
52085
+ provider: row.provider,
52086
+ routeType: row.route_type,
52087
+ model: row.model,
52088
+ modelIn: row.model_in,
52089
+ modelOut: row.model_out,
52090
+ path: row.path,
52091
+ requestId: row.request_id,
52092
+ latencyMs: row.latency_ms,
52093
+ upstreamStatus: row.upstream_status,
52094
+ statusClass: row.status_class,
52095
+ hasError: row.has_error === 1,
52096
+ message: row.message,
52097
+ errorType: row.error_type,
52098
+ hasMetadata: row.has_metadata === 1,
52099
+ userIdRaw: row.user_id_raw,
52100
+ userKey: row.user_key,
52101
+ sessionId: row.session_id
52102
+ };
52103
+ }
52104
+ async function* readJsonlLinesWithOffsets(filePath) {
52105
+ const stream = createReadStream2(filePath);
52106
+ let buffer2 = Buffer.alloc(0);
52107
+ let bufferOffset = 0;
52108
+ let lineNumber = 0;
52109
+ for await (const chunk of stream) {
52110
+ const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
52111
+ buffer2 = buffer2.length === 0 ? chunkBuffer : Buffer.concat([buffer2, chunkBuffer]);
52112
+ let newlineIndex = buffer2.indexOf(10);
52113
+ while (newlineIndex !== -1) {
52114
+ const lineBuffer = buffer2.subarray(0, newlineIndex);
52115
+ const byteLength = newlineIndex + 1;
52116
+ const lineOffset = bufferOffset;
52117
+ lineNumber += 1;
52118
+ yield {
52119
+ line: lineBuffer.toString("utf-8").replace(/\r$/, ""),
52120
+ offset: lineOffset,
52121
+ lineNumber,
52122
+ byteLength
52123
+ };
52124
+ buffer2 = buffer2.subarray(byteLength);
52125
+ bufferOffset += byteLength;
52126
+ newlineIndex = buffer2.indexOf(10);
52127
+ }
52128
+ }
52129
+ if (buffer2.length > 0) {
52130
+ lineNumber += 1;
52131
+ yield {
52132
+ line: buffer2.toString("utf-8").replace(/\r$/, ""),
52133
+ offset: bufferOffset,
52134
+ lineNumber,
52135
+ byteLength: buffer2.length
52136
+ };
52137
+ }
52138
+ }
52139
+ function readLineAtOffset(filePath, offset) {
52140
+ const fd = openSync(filePath, "r");
52141
+ try {
52142
+ const chunks = [];
52143
+ const buffer2 = Buffer.allocUnsafe(64 * 1024);
52144
+ let position = offset;
52145
+ while (true) {
52146
+ const bytesRead = readSync(fd, buffer2, 0, buffer2.length, position);
52147
+ if (bytesRead <= 0)
52148
+ break;
52149
+ const readable = buffer2.subarray(0, bytesRead);
52150
+ const newline = readable.indexOf(10);
52151
+ if (newline >= 0 && newline < bytesRead) {
52152
+ chunks.push(Buffer.from(readable.subarray(0, newline)));
52153
+ break;
52154
+ }
52155
+ chunks.push(Buffer.from(readable));
52156
+ position += bytesRead;
52157
+ }
52158
+ if (chunks.length === 0)
52159
+ return null;
52160
+ return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
52161
+ } finally {
52162
+ closeSync(fd);
52163
+ }
52164
+ }
52165
+ function createEmptyStats() {
52166
+ return {
52167
+ total: 0,
52168
+ errorCount: 0,
52169
+ errorRate: 0,
52170
+ avgLatencyMs: 0,
52171
+ p95LatencyMs: 0
52172
+ };
52173
+ }
52174
+ function createEmptyQueryResult(query, meta3 = {}) {
52175
+ return {
52176
+ items: [],
52177
+ nextCursor: null,
52178
+ hasMore: false,
52179
+ stats: createEmptyStats(),
52180
+ meta: {
52181
+ scannedFiles: 0,
52182
+ scannedLines: 0,
52183
+ parseErrors: 0,
52184
+ truncated: false,
52185
+ indexUsed: true,
52186
+ indexFresh: true,
52187
+ usesFts: shouldUseFts(query.q),
52188
+ queryMs: 0,
52189
+ rowsReturned: 0,
52190
+ statsMode: "exact",
52191
+ ...meta3
52192
+ }
52193
+ };
52194
+ }
52195
+ function appendInClause(clauses, params, column2, values) {
52196
+ if (values.length === 0)
52197
+ return;
52198
+ clauses.push(`${column2} IN (${values.map(() => "?").join(", ")})`);
52199
+ params.push(...values);
52200
+ }
52201
+ function buildWhereClause(query, options = {}) {
52202
+ const clauses = ["e.ts_ms >= ?", "e.ts_ms <= ?"];
52203
+ const params = [query.fromMs, query.toMs];
52204
+ const usesFts = !options.forceLikeSearch && shouldUseFts(query.q);
52205
+ appendInClause(clauses, params, "e.level", query.levels);
52206
+ appendInClause(clauses, params, "e.provider", query.providers);
52207
+ appendInClause(clauses, params, "e.route_type", query.routeTypes);
52208
+ appendInClause(clauses, params, "e.model", query.models);
52209
+ appendInClause(clauses, params, "e.model_in", query.modelIns);
52210
+ appendInClause(clauses, params, "e.model_out", query.modelOuts);
52211
+ appendInClause(clauses, params, "e.status_class", query.statusClasses);
52212
+ if (query.users.length > 0) {
52213
+ clauses.push(`(e.user_id_raw IN (${query.users.map(() => "?").join(", ")}) OR e.user_key IN (${query.users.map(() => "?").join(", ")}))`);
52214
+ params.push(...query.users, ...query.users);
52215
+ }
52216
+ appendInClause(clauses, params, "e.session_id", query.sessions);
52217
+ if (query.hasError !== null) {
52218
+ clauses.push("e.has_error = ?");
52219
+ params.push(query.hasError ? 1 : 0);
52220
+ }
52221
+ if (query.q) {
52222
+ if (usesFts) {
52223
+ const ftsQuery = buildFtsQuery(query.q);
52224
+ clauses.push("e.id IN (SELECT event_id FROM log_events_fts WHERE log_events_fts MATCH ?)");
52225
+ params.push(ftsQuery);
52226
+ } else {
52227
+ clauses.push("e.search_text LIKE ? ESCAPE '\\'");
52228
+ params.push(`%${escapeLikePattern(query.q.toLowerCase())}%`);
52229
+ }
52230
+ }
52231
+ return {
52232
+ whereSql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
52233
+ params,
52234
+ usesFts
52235
+ };
52236
+ }
52237
+
52238
+ class LogIndex {
52239
+ baseDir;
52240
+ config;
52241
+ db;
52242
+ queue = [];
52243
+ flushTimer = null;
52244
+ disposed = false;
52245
+ rebuildingFiles = new Set;
52246
+ dirtyFiles = new Set;
52247
+ insertEventStmt;
52248
+ insertFtsStmt;
52249
+ deleteFtsStmt;
52250
+ upsertFileStmt;
52251
+ constructor(baseDir, config2) {
52252
+ this.baseDir = baseDir;
52253
+ this.config = config2;
52254
+ mkdirSync2(baseDir, { recursive: true });
52255
+ const dbPath = join4(baseDir, "logs-index.sqlite");
52256
+ this.db = new Database(dbPath, { create: true, strict: true });
52257
+ this.configure();
52258
+ this.migrate();
52259
+ this.insertEventStmt = this.db.prepare(`
52260
+ INSERT OR REPLACE INTO log_events (
52261
+ id, ts_ms, ts_start, level, provider, route_type, model, model_in, model_out,
52262
+ path, request_id, latency_ms, upstream_status, status_class, has_error,
52263
+ message, error_type, has_metadata, user_id_raw, user_key, session_id,
52264
+ source_date, source_file, line_number, byte_offset, byte_length, search_text
52265
+ ) VALUES (
52266
+ $id, $ts_ms, $ts_start, $level, $provider, $route_type, $model, $model_in, $model_out,
52267
+ $path, $request_id, $latency_ms, $upstream_status, $status_class, $has_error,
52268
+ $message, $error_type, $has_metadata, $user_id_raw, $user_key, $session_id,
52269
+ $source_date, $source_file, $line_number, $byte_offset, $byte_length, $search_text
52270
+ )
52271
+ `);
52272
+ this.deleteFtsStmt = this.db.prepare("DELETE FROM log_events_fts WHERE event_id = ?");
52273
+ this.insertFtsStmt = this.db.prepare("INSERT INTO log_events_fts(event_id, search_text) VALUES (?, ?)");
52274
+ this.upsertFileStmt = this.db.prepare(`
52275
+ INSERT INTO log_index_files(file_path, source_date, size_bytes, mtime_ms, indexed_at)
52276
+ VALUES (?, ?, ?, ?, ?)
52277
+ ON CONFLICT(file_path) DO UPDATE SET
52278
+ source_date = excluded.source_date,
52279
+ size_bytes = excluded.size_bytes,
52280
+ mtime_ms = excluded.mtime_ms,
52281
+ indexed_at = excluded.indexed_at
52282
+ `);
52283
+ }
52284
+ dispose() {
52285
+ if (this.flushTimer) {
52286
+ clearTimeout(this.flushTimer);
52287
+ this.flushTimer = null;
52288
+ }
52289
+ this.queue = [];
52290
+ this.disposed = true;
52291
+ this.db.close();
52292
+ }
52293
+ enqueue(item) {
52294
+ if (this.disposed)
52295
+ return;
52296
+ if (this.queue.length >= MAX_INDEX_QUEUE) {
52297
+ const dropped = this.queue.shift();
52298
+ if (dropped) {
52299
+ this.dirtyFiles.add(dropped.filePath);
52300
+ }
52301
+ }
52302
+ this.queue.push(item);
52303
+ if (!this.flushTimer) {
52304
+ this.flushTimer = setTimeout(() => {
52305
+ this.flushTimer = null;
52306
+ this.flushQueue();
52307
+ }, INDEX_FLUSH_DELAY_MS);
52308
+ this.flushTimer.unref?.();
52309
+ }
52310
+ }
52311
+ async ensureRangeIndexed(fromMs, toMs) {
52312
+ let scannedFiles = 0;
52313
+ let scannedLines = 0;
52314
+ let parseErrors = 0;
52315
+ const eventsDir = join4(this.baseDir, "events");
52316
+ const dates = listDateStrings2(fromMs, toMs);
52317
+ for (const date5 of dates) {
52318
+ const filePath = join4(eventsDir, `${date5}.jsonl`);
52319
+ if (!existsSync3(filePath))
52320
+ continue;
52321
+ const stats = statSync(filePath);
52322
+ const fileRow = this.db.query("SELECT size_bytes, mtime_ms FROM log_index_files WHERE file_path = ?").get(filePath);
52323
+ const sizeBytes = stats.size;
52324
+ const mtimeMs = Math.trunc(stats.mtimeMs);
52325
+ if (!this.dirtyFiles.has(filePath) && fileRow && fileRow.size_bytes === sizeBytes && fileRow.mtime_ms === mtimeMs) {
52326
+ continue;
52327
+ }
52328
+ const result = await this.rebuildFile(filePath, date5, sizeBytes, mtimeMs);
52329
+ scannedFiles += 1;
52330
+ scannedLines += result.scannedLines;
52331
+ parseErrors += result.parseErrors;
52332
+ }
52333
+ return { scannedFiles, scannedLines, parseErrors };
52334
+ }
52335
+ queryEvents(query, options = {}) {
52336
+ const startedAt = performance.now();
52337
+ const queryHash = hashQuery(query);
52338
+ const decodedCursor = query.cursor ? decodeCursor(query.cursor) : null;
52339
+ if (decodedCursor) {
52340
+ if (decodedCursor.sort !== query.sort || decodedCursor.queryHash !== queryHash) {
52341
+ throw new Error("cursor \u4E0E\u5F53\u524D\u67E5\u8BE2\u6761\u4EF6\u4E0D\u5339\u914D");
52342
+ }
52343
+ }
52344
+ const { whereSql, params, usesFts } = buildWhereClause(query, options);
52345
+ const cursorClause = decodedCursor ? query.sort === "time_desc" ? "AND (e.ts_ms < ? OR (e.ts_ms = ? AND e.id < ?))" : "AND (e.ts_ms > ? OR (e.ts_ms = ? AND e.id > ?))" : "";
52346
+ const cursorParams = decodedCursor ? [decodedCursor.tsMs, decodedCursor.tsMs, decodedCursor.id] : [];
52347
+ const orderSql = query.sort === "time_desc" ? "ORDER BY e.ts_ms DESC, e.id DESC" : "ORDER BY e.ts_ms ASC, e.id ASC";
52348
+ const limit = Math.max(1, query.limit);
52349
+ let rows;
52350
+ try {
52351
+ rows = this.db.query(`
52352
+ SELECT
52353
+ e.id, e.ts_start, e.level, e.provider, e.route_type, e.model, e.model_in,
52354
+ e.model_out, e.path, e.request_id, e.latency_ms, e.upstream_status,
52355
+ e.status_class, e.has_error, e.message, e.error_type, e.has_metadata,
52356
+ e.user_id_raw, e.user_key, e.session_id, e.ts_ms
52357
+ FROM log_events e
52358
+ ${whereSql}
52359
+ ${cursorClause}
52360
+ ${orderSql}
52361
+ LIMIT ?
52362
+ `).all(...params, ...cursorParams, limit + 1);
52363
+ } catch (err) {
52364
+ if (!usesFts)
52365
+ throw err;
52366
+ const fallback = this.queryEvents(query, { forceLikeSearch: true });
52367
+ return {
52368
+ ...fallback,
52369
+ meta: {
52370
+ ...fallback.meta,
52371
+ usesFts: false,
52372
+ fallbackReason: `FTS \u67E5\u8BE2\u5931\u8D25\uFF0C\u5DF2\u9000\u56DE\u7D22\u5F15 LIKE/\u5185\u5B58\u8FC7\u6EE4: ${err instanceof Error ? err.message : String(err)}`
52373
+ }
52374
+ };
52375
+ }
52376
+ const pageRows = rows.slice(0, limit);
52377
+ const hasMore = rows.length > limit;
52378
+ const lastRow = pageRows[pageRows.length - 1];
52379
+ const stats = this.queryStats(whereSql, params);
52380
+ return {
52381
+ items: pageRows.map(rowToSummary),
52382
+ nextCursor: hasMore && lastRow ? encodeCursor({
52383
+ v: 2,
52384
+ sort: query.sort,
52385
+ tsMs: lastRow.ts_ms,
52386
+ id: lastRow.id,
52387
+ queryHash
52388
+ }) : null,
52389
+ hasMore,
52390
+ stats,
52391
+ meta: {
52392
+ scannedFiles: 0,
52393
+ scannedLines: 0,
52394
+ parseErrors: 0,
52395
+ truncated: false,
52396
+ indexUsed: true,
52397
+ indexFresh: true,
52398
+ usesFts,
52399
+ queryMs: Math.round((performance.now() - startedAt) * 100) / 100,
52400
+ rowsReturned: pageRows.length,
52401
+ statsMode: "exact"
52402
+ }
52403
+ };
52404
+ }
52405
+ getEventRecordByOffsetId(id) {
52406
+ const parsedId = decodeOffsetLogEventId(id);
52407
+ if (!parsedId)
52408
+ return null;
52409
+ const row = this.db.query("SELECT source_date, source_file, line_number, byte_offset FROM log_events WHERE id = ?").get(id);
52410
+ const filePath = row?.source_file ?? join4(this.baseDir, "events", `${parsedId.date}.jsonl`);
52411
+ if (!existsSync3(filePath))
52412
+ return null;
52413
+ const line2 = readLineAtOffset(filePath, row?.byte_offset ?? parsedId.offset);
52414
+ if (!line2?.trim())
52415
+ return null;
52416
+ const event = JSON.parse(line2);
52417
+ return {
52418
+ event,
52419
+ location: {
52420
+ id,
52421
+ date: row?.source_date ?? parsedId.date,
52422
+ file: filePath,
52423
+ line: row?.line_number ?? null,
52424
+ offset: row?.byte_offset ?? parsedId.offset
52425
+ }
52426
+ };
52427
+ }
52428
+ configure() {
52429
+ this.db.exec(`
52430
+ PRAGMA journal_mode = WAL;
52431
+ PRAGMA synchronous = NORMAL;
52432
+ PRAGMA temp_store = MEMORY;
52433
+ PRAGMA busy_timeout = 3000;
52434
+ PRAGMA foreign_keys = ON;
52435
+ `);
52436
+ }
52437
+ migrate() {
52438
+ this.db.exec(`
52439
+ CREATE TABLE IF NOT EXISTS log_index_meta (
52440
+ key TEXT PRIMARY KEY,
52441
+ value TEXT NOT NULL
52442
+ );
52443
+
52444
+ CREATE TABLE IF NOT EXISTS log_index_files (
52445
+ file_path TEXT PRIMARY KEY,
52446
+ source_date TEXT NOT NULL,
52447
+ size_bytes INTEGER NOT NULL,
52448
+ mtime_ms INTEGER NOT NULL,
52449
+ indexed_at INTEGER NOT NULL
52450
+ );
52451
+
52452
+ CREATE TABLE IF NOT EXISTS log_events (
52453
+ id TEXT PRIMARY KEY,
52454
+ ts_ms INTEGER NOT NULL,
52455
+ ts_start TEXT NOT NULL,
52456
+ level TEXT NOT NULL,
52457
+ provider TEXT NOT NULL,
52458
+ route_type TEXT NOT NULL,
52459
+ model TEXT NOT NULL,
52460
+ model_in TEXT NOT NULL,
52461
+ model_out TEXT NOT NULL,
52462
+ path TEXT NOT NULL,
52463
+ request_id TEXT NOT NULL,
52464
+ latency_ms INTEGER NOT NULL,
52465
+ upstream_status INTEGER NOT NULL,
52466
+ status_class TEXT NOT NULL,
52467
+ has_error INTEGER NOT NULL,
52468
+ message TEXT NOT NULL,
52469
+ error_type TEXT,
52470
+ has_metadata INTEGER NOT NULL,
52471
+ user_id_raw TEXT,
52472
+ user_key TEXT,
52473
+ session_id TEXT,
52474
+ source_date TEXT NOT NULL,
52475
+ source_file TEXT NOT NULL,
52476
+ line_number INTEGER,
52477
+ byte_offset INTEGER NOT NULL,
52478
+ byte_length INTEGER NOT NULL,
52479
+ search_text TEXT NOT NULL
52480
+ );
52481
+
52482
+ CREATE VIRTUAL TABLE IF NOT EXISTS log_events_fts
52483
+ USING fts5(event_id UNINDEXED, search_text);
52484
+
52485
+ CREATE INDEX IF NOT EXISTS idx_log_events_time_desc ON log_events(ts_ms DESC, id DESC);
52486
+ CREATE INDEX IF NOT EXISTS idx_log_events_time_asc ON log_events(ts_ms ASC, id ASC);
52487
+ CREATE INDEX IF NOT EXISTS idx_log_events_level_time ON log_events(level, ts_ms DESC);
52488
+ CREATE INDEX IF NOT EXISTS idx_log_events_provider_time ON log_events(provider, ts_ms DESC);
52489
+ CREATE INDEX IF NOT EXISTS idx_log_events_route_time ON log_events(route_type, ts_ms DESC);
52490
+ CREATE INDEX IF NOT EXISTS idx_log_events_model_time ON log_events(model, ts_ms DESC);
52491
+ CREATE INDEX IF NOT EXISTS idx_log_events_status_time ON log_events(status_class, ts_ms DESC);
52492
+ CREATE INDEX IF NOT EXISTS idx_log_events_error_time ON log_events(has_error, ts_ms DESC);
52493
+ CREATE INDEX IF NOT EXISTS idx_log_events_user_time ON log_events(user_key, ts_ms DESC);
52494
+ CREATE INDEX IF NOT EXISTS idx_log_events_session_time ON log_events(session_id, ts_ms DESC);
52495
+ CREATE INDEX IF NOT EXISTS idx_log_events_file ON log_events(source_file);
52496
+ `);
52497
+ this.db.prepare(`
52498
+ INSERT INTO log_index_meta(key, value)
52499
+ VALUES ('schema_version', ?)
52500
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
52501
+ `).run(String(SCHEMA_VERSION));
52502
+ }
52503
+ flushQueue() {
52504
+ if (this.queue.length === 0 || this.disposed)
52505
+ return;
52506
+ const batch = this.queue.splice(0, INDEX_BATCH_SIZE);
52507
+ const transaction = this.db.transaction((items) => {
52508
+ for (const item of items) {
52509
+ this.insertQueueItem(item);
52510
+ }
52511
+ });
52512
+ try {
52513
+ transaction(batch);
52514
+ } catch (err) {
52515
+ console.error("[log-index] \u589E\u91CF\u7D22\u5F15\u5199\u5165\u5931\u8D25:", err);
52516
+ }
52517
+ if (this.queue.length > 0 && !this.flushTimer) {
52518
+ this.flushTimer = setTimeout(() => {
52519
+ this.flushTimer = null;
52520
+ this.flushQueue();
52521
+ }, INDEX_FLUSH_DELAY_MS);
52522
+ this.flushTimer.unref?.();
52523
+ }
52524
+ }
52525
+ insertQueueItem(item) {
52526
+ if (this.rebuildingFiles.has(item.filePath))
52527
+ return;
52528
+ const id = encodeOffsetLogEventId(item.date, item.offset);
52529
+ const row = eventToRow({
52530
+ id,
52531
+ date: item.date,
52532
+ filePath: item.filePath,
52533
+ lineNumber: null,
52534
+ offset: item.offset,
52535
+ byteLength: item.byteLength,
52536
+ event: item.event
52537
+ });
52538
+ if (!row)
52539
+ return;
52540
+ this.insertEventStmt.run(row);
52541
+ this.deleteFtsStmt.run(id);
52542
+ this.insertFtsStmt.run(id, row.search_text);
52543
+ if (!this.dirtyFiles.has(item.filePath)) {
52544
+ try {
52545
+ const stats = statSync(item.filePath);
52546
+ const indexedThrough = item.offset + item.byteLength;
52547
+ this.upsertFileStmt.run(item.filePath, item.date, Math.min(indexedThrough, stats.size), Math.trunc(stats.mtimeMs), Date.now());
52548
+ } catch {}
52549
+ }
52550
+ }
52551
+ async rebuildFile(filePath, date5, sizeBytes, mtimeMs) {
52552
+ if (this.rebuildingFiles.has(filePath)) {
52553
+ return { scannedLines: 0, parseErrors: 0 };
52554
+ }
52555
+ this.rebuildingFiles.add(filePath);
52556
+ let scannedLines = 0;
52557
+ let parseErrors = 0;
52558
+ const rows = [];
52559
+ try {
52560
+ for await (const item of readJsonlLinesWithOffsets(filePath)) {
52561
+ scannedLines += 1;
52562
+ if (!item.line.trim())
52563
+ continue;
52564
+ let event;
52565
+ try {
52566
+ event = JSON.parse(item.line);
52567
+ } catch {
52568
+ parseErrors += 1;
52569
+ continue;
52570
+ }
52571
+ const row = eventToRow({
52572
+ id: encodeOffsetLogEventId(date5, item.offset),
52573
+ date: date5,
52574
+ filePath,
52575
+ lineNumber: item.lineNumber,
52576
+ offset: item.offset,
52577
+ byteLength: item.byteLength,
52578
+ event
52579
+ });
52580
+ if (row)
52581
+ rows.push(row);
52582
+ }
52583
+ const transaction = this.db.transaction((eventRows) => {
52584
+ this.db.prepare("DELETE FROM log_events_fts WHERE event_id IN (SELECT id FROM log_events WHERE source_file = ?)").run(filePath);
52585
+ this.db.prepare("DELETE FROM log_events WHERE source_file = ?").run(filePath);
52586
+ for (const row of eventRows) {
52587
+ this.insertEventStmt.run(row);
52588
+ this.insertFtsStmt.run(row.id, row.search_text);
52589
+ }
52590
+ this.upsertFileStmt.run(filePath, date5, sizeBytes, mtimeMs, Date.now());
52591
+ });
52592
+ transaction(rows);
52593
+ this.dirtyFiles.delete(filePath);
52594
+ return { scannedLines, parseErrors };
52595
+ } finally {
52596
+ this.rebuildingFiles.delete(filePath);
52597
+ }
52598
+ }
52599
+ queryStats(whereSql, params) {
52600
+ const aggregate = this.db.query(`
52601
+ SELECT
52602
+ COUNT(*) AS total,
52603
+ COALESCE(SUM(has_error), 0) AS errorCount,
52604
+ COALESCE(AVG(latency_ms), 0) AS avgLatencyMs
52605
+ FROM log_events e
52606
+ ${whereSql}
52607
+ `).get(...params);
52608
+ const total = Number(aggregate.total) || 0;
52609
+ if (total <= 0)
52610
+ return createEmptyStats();
52611
+ const p95Offset = Math.max(0, Math.ceil(total * 0.95) - 1);
52612
+ const p95Row = this.db.query(`
52613
+ SELECT latency_ms
52614
+ FROM log_events e
52615
+ ${whereSql}
52616
+ ORDER BY latency_ms ASC
52617
+ LIMIT 1 OFFSET ?
52618
+ `).get(...params, p95Offset);
52619
+ const errorCount = Number(aggregate.errorCount) || 0;
52620
+ return {
52621
+ total,
52622
+ errorCount,
52623
+ errorRate: toPercent2(errorCount, total),
52624
+ avgLatencyMs: Math.round(Number(aggregate.avgLatencyMs) || 0),
52625
+ p95LatencyMs: Math.round(p95Row?.latency_ms ?? 0)
52626
+ };
52627
+ }
52628
+ }
52629
+ function initLogIndex(baseDir, config2) {
52630
+ disposeLogIndex();
52631
+ if (config2?.enabled === false)
52632
+ return;
52633
+ try {
52634
+ singleton = new LogIndex(baseDir, config2);
52635
+ } catch (err) {
52636
+ singleton = null;
52637
+ console.error("[log-index] SQLite \u7D22\u5F15\u521D\u59CB\u5316\u5931\u8D25\uFF0C\u5C06\u9000\u56DE JSONL \u626B\u63CF:", err);
52638
+ }
52639
+ }
52640
+ function disposeLogIndex() {
52641
+ if (!singleton)
52642
+ return;
52643
+ try {
52644
+ singleton.dispose();
52645
+ } catch (err) {
52646
+ console.error("[log-index] SQLite \u7D22\u5F15\u5173\u95ED\u5931\u8D25:", err);
52647
+ } finally {
52648
+ singleton = null;
52649
+ }
52650
+ }
52651
+ function getLogIndex(baseDir) {
52652
+ if (!singleton)
52653
+ return null;
52654
+ if (baseDir && singleton.baseDir !== baseDir)
52655
+ return null;
52656
+ return singleton;
52657
+ }
52658
+ function enqueueLogEventForIndex(input) {
52659
+ const index = getLogIndex(input.baseDir);
52660
+ index?.enqueue(input);
52661
+ }
52662
+ async function queryIndexedLogEvents(logConfig, query) {
52663
+ if (!logConfig || logConfig.enabled === false)
52664
+ return createEmptyQueryResult(query);
52665
+ const baseDir = resolveLogBaseDir(logConfig);
52666
+ const index = getLogIndex(baseDir);
52667
+ if (!index)
52668
+ return null;
52669
+ try {
52670
+ const freshness = await index.ensureRangeIndexed(query.fromMs, query.toMs);
52671
+ const result = index.queryEvents(query);
52672
+ return {
52673
+ ...result,
52674
+ meta: {
52675
+ ...result.meta,
52676
+ scannedFiles: freshness.scannedFiles,
52677
+ scannedLines: freshness.scannedLines,
52678
+ parseErrors: freshness.parseErrors
52679
+ }
52680
+ };
52681
+ } catch (err) {
52682
+ if (err instanceof Error && err.message.includes("cursor")) {
52683
+ throw err;
52684
+ }
52685
+ return {
52686
+ ...createEmptyQueryResult(query, {
52687
+ indexUsed: false,
52688
+ indexFresh: false,
52689
+ fallbackReason: err instanceof Error ? err.message : String(err),
52690
+ statsMode: "none"
52691
+ })
52692
+ };
52693
+ }
52694
+ }
52695
+ function getIndexedLogEventDetail(logConfig, id) {
52696
+ if (!logConfig || logConfig.enabled === false)
52697
+ return null;
52698
+ const baseDir = resolveLogBaseDir(logConfig);
52699
+ const indexed = getLogIndex(baseDir)?.getEventRecordByOffsetId(id);
52700
+ if (indexed)
52701
+ return indexed;
52702
+ const parsedId = decodeOffsetLogEventId(id);
52703
+ if (!parsedId)
52704
+ return null;
52705
+ const filePath = join4(baseDir, "events", `${parsedId.date}.jsonl`);
52706
+ if (!existsSync3(filePath))
52707
+ return null;
52708
+ const line2 = readLineAtOffset(filePath, parsedId.offset);
52709
+ if (!line2?.trim())
52710
+ return null;
52711
+ return {
52712
+ event: JSON.parse(line2),
52713
+ location: {
52714
+ id,
52715
+ date: parsedId.date,
52716
+ file: filePath,
52717
+ line: null,
52718
+ offset: parsedId.offset
52719
+ }
52720
+ };
52721
+ }
52722
+
51892
52723
  // src/log-query.ts
51893
52724
  var WINDOW_MS2 = {
51894
52725
  "1h": 60 * 60 * 1000,
@@ -51900,49 +52731,57 @@ var MAX_QUERY_LIMIT = 200;
51900
52731
  var DEFAULT_QUERY_LIMIT = 50;
51901
52732
  var MAX_EXPORT_ROWS = 5000;
51902
52733
  var MAX_Q_LENGTH = 200;
51903
- function encodeBase64Url(value) {
52734
+ function encodeBase64Url2(value) {
51904
52735
  return Buffer.from(value, "utf-8").toString("base64url");
51905
52736
  }
51906
- function decodeBase64Url(value) {
52737
+ function decodeBase64Url2(value) {
51907
52738
  return Buffer.from(value, "base64url").toString("utf-8");
51908
52739
  }
51909
- function encodeCursor(data) {
51910
- return encodeBase64Url(JSON.stringify(data));
52740
+ function encodeCursor2(data) {
52741
+ return encodeBase64Url2(JSON.stringify(data));
51911
52742
  }
51912
- function decodeCursor(raw2) {
51913
- const parsed = JSON.parse(decodeBase64Url(raw2));
52743
+ function decodeCursor2(raw2) {
52744
+ const parsed = JSON.parse(decodeBase64Url2(raw2));
51914
52745
  if (!Number.isInteger(parsed.offset) || parsed.offset < 0) {
51915
52746
  throw new Error("cursor \u975E\u6CD5");
51916
52747
  }
51917
52748
  return parsed;
51918
52749
  }
52750
+ function isLegacyOffsetCursor(raw2) {
52751
+ try {
52752
+ const parsed = JSON.parse(decodeBase64Url2(raw2));
52753
+ return Number.isInteger(parsed.offset) && Number(parsed.offset) >= 0;
52754
+ } catch {
52755
+ return false;
52756
+ }
52757
+ }
51919
52758
  function encodeEventId(date5, line2) {
51920
- return encodeBase64Url(JSON.stringify({ d: date5, l: line2 }));
52759
+ return encodeBase64Url2(JSON.stringify({ d: date5, l: line2 }));
51921
52760
  }
51922
52761
  function decodeEventId(id) {
51923
- const parsed = JSON.parse(decodeBase64Url(id));
52762
+ const parsed = JSON.parse(decodeBase64Url2(id));
51924
52763
  if (typeof parsed.d !== "string" || !Number.isInteger(parsed.l) || Number(parsed.l) <= 0) {
51925
52764
  throw new Error("id \u975E\u6CD5");
51926
52765
  }
51927
52766
  return { date: parsed.d, line: Number(parsed.l) };
51928
52767
  }
51929
- function toPercent2(numerator, denominator) {
52768
+ function toPercent3(numerator, denominator) {
51930
52769
  if (denominator <= 0)
51931
52770
  return 0;
51932
52771
  return Number((numerator / denominator * 100).toFixed(2));
51933
52772
  }
51934
- function toDayStart2(ms) {
52773
+ function toDayStart3(ms) {
51935
52774
  const date5 = new Date(ms);
51936
52775
  return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
51937
52776
  }
51938
- function listDateStrings2(fromMs, toMs) {
52777
+ function listDateStrings3(fromMs, toMs) {
51939
52778
  const result = [];
51940
- for (let day = toDayStart2(fromMs);day <= toDayStart2(toMs); day += 24 * 60 * 60 * 1000) {
52779
+ for (let day = toDayStart3(fromMs);day <= toDayStart3(toMs); day += 24 * 60 * 60 * 1000) {
51941
52780
  result.push(new Date(day).toISOString().slice(0, 10));
51942
52781
  }
51943
52782
  return result;
51944
52783
  }
51945
- function getStatusClass2(event) {
52784
+ function getStatusClass3(event) {
51946
52785
  if (event.error_type)
51947
52786
  return "network_error";
51948
52787
  const status = event.upstream_status ?? 0;
@@ -51954,16 +52793,16 @@ function getStatusClass2(event) {
51954
52793
  return "5xx";
51955
52794
  return "network_error";
51956
52795
  }
51957
- function isErrorEvent2(event) {
52796
+ function isErrorEvent3(event) {
51958
52797
  if (event.error_type)
51959
52798
  return true;
51960
52799
  const status = event.upstream_status ?? 0;
51961
52800
  return status < 200 || status >= 400;
51962
52801
  }
51963
- function getLevel(event) {
51964
- return isErrorEvent2(event) ? "error" : "info";
52802
+ function getLevel2(event) {
52803
+ return isErrorEvent3(event) ? "error" : "info";
51965
52804
  }
51966
- function buildMessage(event) {
52805
+ function buildMessage2(event) {
51967
52806
  if (event.error_message)
51968
52807
  return event.error_message;
51969
52808
  if (event.error_type)
@@ -51988,7 +52827,7 @@ function containsKeyword(event, q) {
51988
52827
  identity.sessionId ?? "",
51989
52828
  event.error_type ?? "",
51990
52829
  event.error_message ?? "",
51991
- buildMessage(event)
52830
+ buildMessage2(event)
51992
52831
  ].join(" ").toLowerCase();
51993
52832
  return haystack.includes(keyword);
51994
52833
  }
@@ -52037,7 +52876,7 @@ function finalizeStats(stats) {
52037
52876
  return {
52038
52877
  total: stats.total,
52039
52878
  errorCount: stats.errorCount,
52040
- errorRate: toPercent2(stats.errorCount, stats.total),
52879
+ errorRate: toPercent3(stats.errorCount, stats.total),
52041
52880
  avgLatencyMs: Math.round(stats.latencySum / stats.total),
52042
52881
  p95LatencyMs: percentileFromCounts(stats.latencyCounts, stats.total, 0.95)
52043
52882
  };
@@ -52057,17 +52896,17 @@ function insertBoundedEvent(items, item, sort, maxKeep) {
52057
52896
  items.pop();
52058
52897
  }
52059
52898
  }
52060
- function clampLimit(limit) {
52899
+ function clampLimit(limit, maxLimit = MAX_QUERY_LIMIT) {
52061
52900
  if (!Number.isFinite(limit))
52062
52901
  return DEFAULT_QUERY_LIMIT;
52063
52902
  const integer2 = Math.floor(limit);
52064
52903
  if (integer2 <= 0)
52065
52904
  return DEFAULT_QUERY_LIMIT;
52066
- return Math.min(MAX_QUERY_LIMIT, integer2);
52905
+ return Math.min(maxLimit, integer2);
52067
52906
  }
52068
- function normalizeQuery(input) {
52907
+ function normalizeQuery(input, maxLimit = MAX_QUERY_LIMIT) {
52069
52908
  const sort = input.sort ?? "time_desc";
52070
- const limit = clampLimit(input.limit);
52909
+ const limit = clampLimit(input.limit, maxLimit);
52071
52910
  const qRaw = (input.q ?? "").trim();
52072
52911
  const q = qRaw.length > MAX_Q_LENGTH ? qRaw.slice(0, MAX_Q_LENGTH) : qRaw;
52073
52912
  return {
@@ -52107,7 +52946,7 @@ function eventToSummary(item) {
52107
52946
  upstreamStatus: event.upstream_status ?? 0,
52108
52947
  statusClass: item.statusClass,
52109
52948
  hasError: item.level === "error",
52110
- message: buildMessage(event),
52949
+ message: buildMessage2(event),
52111
52950
  errorType: event.error_type,
52112
52951
  hasMetadata: identity.hasMetadata,
52113
52952
  userIdRaw: identity.userIdRaw,
@@ -52115,6 +52954,57 @@ function eventToSummary(item) {
52115
52954
  sessionId: identity.sessionId
52116
52955
  };
52117
52956
  }
52957
+ function createLogEventSummaryFromEvent(event, location) {
52958
+ const ts = Date.parse(event.ts_start);
52959
+ return eventToSummary({
52960
+ id: location.id,
52961
+ date: location.date,
52962
+ line: location.line ?? 0,
52963
+ ts: Number.isFinite(ts) ? ts : 0,
52964
+ level: getLevel2(event),
52965
+ statusClass: getStatusClass3(event),
52966
+ event
52967
+ });
52968
+ }
52969
+ function logEventMatchesQuery(event, query) {
52970
+ if (!event.ts_start)
52971
+ return false;
52972
+ const ts = Date.parse(event.ts_start);
52973
+ if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
52974
+ return false;
52975
+ const level = getLevel2(event);
52976
+ const statusClass = getStatusClass3(event);
52977
+ if (query.levels.length > 0 && !query.levels.includes(level))
52978
+ return false;
52979
+ if (query.providers.length > 0 && !query.providers.includes(event.provider))
52980
+ return false;
52981
+ if (query.routeTypes.length > 0 && !query.routeTypes.includes(event.route_type))
52982
+ return false;
52983
+ const eventModel = event.model_out || event.model_in;
52984
+ if (query.models.length > 0 && !query.models.includes(eventModel))
52985
+ return false;
52986
+ if (query.modelIns.length > 0 && !query.modelIns.includes(event.model_in))
52987
+ return false;
52988
+ if (query.modelOuts.length > 0 && !query.modelOuts.includes(event.model_out))
52989
+ return false;
52990
+ const identity = resolveLogSessionIdentity(event.request_body);
52991
+ if (query.users.length > 0) {
52992
+ const matchedByRaw = identity.userIdRaw ? query.users.includes(identity.userIdRaw) : false;
52993
+ const matchedByUserKey = identity.userKey ? query.users.includes(identity.userKey) : false;
52994
+ if (!matchedByRaw && !matchedByUserKey)
52995
+ return false;
52996
+ }
52997
+ if (query.sessions.length > 0) {
52998
+ if (!identity.sessionId || !query.sessions.includes(identity.sessionId))
52999
+ return false;
53000
+ }
53001
+ if (query.statusClasses.length > 0 && !query.statusClasses.includes(statusClass))
53002
+ return false;
53003
+ const hasError = level === "error";
53004
+ if (query.hasError !== null && query.hasError !== hasError)
53005
+ return false;
53006
+ return containsKeyword(event, query.q);
53007
+ }
52118
53008
  function detectBodyPolicy(event) {
52119
53009
  const hasRequestBody = event.request_body !== undefined;
52120
53010
  const hasResponseBody = event.response_body !== undefined;
@@ -52168,14 +53058,14 @@ function readStreamContent(baseDir, streamFile) {
52168
53058
  if (!looksLikeStreamFile) {
52169
53059
  return { content: null, warning: "stream_file \u4E0D\u662F .sse.raw \u6587\u4EF6\uFF0C\u5DF2\u8DF3\u8FC7\u8BFB\u53D6\u3002" };
52170
53060
  }
52171
- if (existsSync3(resolvedFromFile)) {
53061
+ if (existsSync4(resolvedFromFile)) {
52172
53062
  return { content: readFileSync3(resolvedFromFile, "utf-8"), warning: null };
52173
53063
  }
52174
53064
  const fallbackPath = resolve4(resolvedBase, streamFile);
52175
53065
  if (!fallbackPath.startsWith(`${resolvedBase}/`) && fallbackPath !== resolvedBase) {
52176
53066
  return { content: null, warning: "stream_file \u8DEF\u5F84\u975E\u6CD5\uFF0C\u5DF2\u62D2\u7EDD\u8BFB\u53D6\u3002" };
52177
53067
  }
52178
- if (!existsSync3(fallbackPath)) {
53068
+ if (!existsSync4(fallbackPath)) {
52179
53069
  return { content: null, warning: "stream_file \u4E0D\u5B58\u5728\uFF0C\u53EF\u80FD\u5DF2\u88AB\u6E05\u7406\u3002" };
52180
53070
  }
52181
53071
  return { content: readFileSync3(fallbackPath, "utf-8"), warning: null };
@@ -52188,8 +53078,8 @@ function readStreamContent(baseDir, streamFile) {
52188
53078
  }
52189
53079
  async function buildLogEventDetail(id, parsed, location, context2) {
52190
53080
  const event = parsed;
52191
- const level = getLevel(event);
52192
- const statusClass = getStatusClass2(event);
53081
+ const level = getLevel2(event);
53082
+ const statusClass = getStatusClass3(event);
52193
53083
  const bodyPolicy = detectBodyPolicy(event);
52194
53084
  const requestBodyAvailable = event.request_body !== undefined;
52195
53085
  const responseBodyAvailable = event.response_body !== undefined;
@@ -52261,8 +53151,8 @@ async function buildLogEventDetail(id, parsed, location, context2) {
52261
53151
  };
52262
53152
  }
52263
53153
  async function scanEvents(baseDir, query) {
52264
- const eventsDir = join4(baseDir, "events");
52265
- if (!existsSync3(eventsDir)) {
53154
+ const eventsDir = join5(baseDir, "events");
53155
+ if (!existsSync4(eventsDir)) {
52266
53156
  return {
52267
53157
  items: [],
52268
53158
  stats: {
@@ -52280,8 +53170,8 @@ async function scanEvents(baseDir, query) {
52280
53170
  }
52281
53171
  };
52282
53172
  }
52283
- const dates = listDateStrings2(query.fromMs, query.toMs);
52284
- const offset = query.cursor ? decodeCursor(query.cursor).offset : 0;
53173
+ const dates = listDateStrings3(query.fromMs, query.toMs);
53174
+ const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
52285
53175
  const maxKeep = offset + query.limit;
52286
53176
  const items = [];
52287
53177
  const runningStats = createRunningStats();
@@ -52294,11 +53184,11 @@ async function scanEvents(baseDir, query) {
52294
53184
  truncated = true;
52295
53185
  break;
52296
53186
  }
52297
- const filePath = join4(eventsDir, `${date5}.jsonl`);
52298
- if (!existsSync3(filePath))
53187
+ const filePath = join5(eventsDir, `${date5}.jsonl`);
53188
+ if (!existsSync4(filePath))
52299
53189
  continue;
52300
53190
  scannedFiles += 1;
52301
- const stream = createReadStream2(filePath, { encoding: "utf-8" });
53191
+ const stream = createReadStream3(filePath, { encoding: "utf-8" });
52302
53192
  const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
52303
53193
  let lineNumber = 0;
52304
53194
  for await (const line2 of rl) {
@@ -52324,8 +53214,8 @@ async function scanEvents(baseDir, query) {
52324
53214
  const ts = Date.parse(event.ts_start);
52325
53215
  if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
52326
53216
  continue;
52327
- const level = getLevel(event);
52328
- const statusClass = getStatusClass2(event);
53217
+ const level = getLevel2(event);
53218
+ const statusClass = getStatusClass3(event);
52329
53219
  if (query.levels.length > 0 && !query.levels.includes(level))
52330
53220
  continue;
52331
53221
  if (query.providers.length > 0 && !query.providers.includes(event.provider))
@@ -52415,6 +53305,9 @@ function validateSort(value) {
52415
53305
  return value === "time_desc" || value === "time_asc";
52416
53306
  }
52417
53307
  async function queryLogEvents(context2, input) {
53308
+ return queryLogEventsInternal(context2, input, MAX_QUERY_LIMIT);
53309
+ }
53310
+ async function queryLogEventsInternal(context2, input, maxLimit) {
52418
53311
  const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
52419
53312
  if (!logEnabled) {
52420
53313
  return {
@@ -52437,30 +53330,51 @@ async function queryLogEvents(context2, input) {
52437
53330
  };
52438
53331
  }
52439
53332
  const baseDir = resolveLogBaseDir(context2.logConfig);
52440
- const query = normalizeQuery(input);
52441
- const offset = query.cursor ? decodeCursor(query.cursor).offset : 0;
53333
+ const query = normalizeQuery(input, maxLimit);
53334
+ const indexed = await queryIndexedLogEvents(context2.logConfig, query);
53335
+ if (indexed?.meta.indexUsed) {
53336
+ return indexed;
53337
+ }
53338
+ if (query.cursor && !isLegacyOffsetCursor(query.cursor)) {
53339
+ throw new Error(`\u7D22\u5F15\u67E5\u8BE2\u5931\u8D25\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7D22\u5F15 cursor \u56DE\u9000\u5230 JSONL\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u7B2C\u4E00\u9875${indexed?.meta.fallbackReason ? `: ${indexed.meta.fallbackReason}` : ""}`);
53340
+ }
53341
+ const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
52442
53342
  const scanned = await scanEvents(baseDir, query);
52443
53343
  const pageItems = scanned.items.slice(offset, offset + query.limit);
52444
53344
  const hasMore = scanned.stats.total > offset + query.limit;
52445
53345
  const nextOffset = offset + pageItems.length;
52446
53346
  return {
52447
53347
  items: pageItems.map(eventToSummary),
52448
- nextCursor: hasMore ? encodeCursor({ offset: nextOffset }) : null,
53348
+ nextCursor: hasMore ? encodeCursor2({ offset: nextOffset }) : null,
52449
53349
  hasMore,
52450
53350
  stats: scanned.stats,
52451
- meta: scanned.meta
53351
+ meta: {
53352
+ ...scanned.meta,
53353
+ indexUsed: false,
53354
+ indexFresh: false,
53355
+ fallbackReason: indexed?.meta.fallbackReason,
53356
+ statsMode: "exact"
53357
+ }
52452
53358
  };
52453
53359
  }
52454
53360
  async function getLogEventDetailById(context2, id) {
52455
53361
  const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
52456
53362
  if (!logEnabled)
52457
53363
  return null;
53364
+ const indexed = getIndexedLogEventDetail(context2.logConfig, id);
53365
+ if (indexed) {
53366
+ return buildLogEventDetail(id, indexed.event, {
53367
+ date: indexed.location.date,
53368
+ line: indexed.location.line ?? 0,
53369
+ file: indexed.location.file
53370
+ }, context2);
53371
+ }
52458
53372
  const { date: date5, line: line2 } = decodeEventId(id);
52459
53373
  const baseDir = resolveLogBaseDir(context2.logConfig);
52460
- const filePath = join4(baseDir, "events", `${date5}.jsonl`);
52461
- if (!existsSync3(filePath))
53374
+ const filePath = join5(baseDir, "events", `${date5}.jsonl`);
53375
+ if (!existsSync4(filePath))
52462
53376
  return null;
52463
- const stream = createReadStream2(filePath, { encoding: "utf-8" });
53377
+ const stream = createReadStream3(filePath, { encoding: "utf-8" });
52464
53378
  const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
52465
53379
  let lineNumber = 0;
52466
53380
  for await (const lineText of rl) {
@@ -52570,11 +53484,11 @@ function createJsonExportStream(data) {
52570
53484
  });
52571
53485
  }
52572
53486
  async function exportLogEvents(context2, input, format) {
52573
- const data = await queryLogEvents(context2, {
53487
+ const data = await queryLogEventsInternal(context2, {
52574
53488
  ...input,
52575
53489
  cursor: null,
52576
53490
  limit: MAX_EXPORT_ROWS
52577
- });
53491
+ }, MAX_EXPORT_ROWS);
52578
53492
  const now2 = new Date().toISOString().replace(/[:.]/g, "-");
52579
53493
  if (format === "csv") {
52580
53494
  return {
@@ -52613,18 +53527,18 @@ function parseBooleanFlag(value) {
52613
53527
  }
52614
53528
 
52615
53529
  // src/log-sessions.ts
52616
- import { createReadStream as createReadStream3, existsSync as existsSync4 } from "fs";
52617
- import { join as join5 } from "path";
53530
+ import { createReadStream as createReadStream4, existsSync as existsSync5 } from "fs";
53531
+ import { join as join6 } from "path";
52618
53532
  import { createInterface as createInterface3 } from "readline";
52619
53533
  var MAX_LINES_SCANNED3 = 250000;
52620
53534
  var MAX_Q_LENGTH2 = 200;
52621
- function toDayStart3(ms) {
53535
+ function toDayStart4(ms) {
52622
53536
  const date5 = new Date(ms);
52623
53537
  return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
52624
53538
  }
52625
- function listDateStrings3(fromMs, toMs) {
53539
+ function listDateStrings4(fromMs, toMs) {
52626
53540
  const result = [];
52627
- for (let day = toDayStart3(fromMs);day <= toDayStart3(toMs); day += 24 * 60 * 60 * 1000) {
53541
+ for (let day = toDayStart4(fromMs);day <= toDayStart4(toMs); day += 24 * 60 * 60 * 1000) {
52628
53542
  result.push(new Date(day).toISOString().slice(0, 10));
52629
53543
  }
52630
53544
  return result;
@@ -52735,8 +53649,8 @@ async function queryLogSessions(context2, input) {
52735
53649
  return createEmptyResult(normalized.fromMs, normalized.toMs);
52736
53650
  }
52737
53651
  const baseDir = resolveLogBaseDir(context2.logConfig);
52738
- const eventsDir = join5(baseDir, "events");
52739
- if (!existsSync4(eventsDir)) {
53652
+ const eventsDir = join6(baseDir, "events");
53653
+ if (!existsSync5(eventsDir)) {
52740
53654
  return createEmptyResult(normalized.fromMs, normalized.toMs);
52741
53655
  }
52742
53656
  const usersMap = new Map;
@@ -52748,17 +53662,17 @@ async function queryLogSessions(context2, input) {
52748
53662
  let scannedLines = 0;
52749
53663
  let parseErrors = 0;
52750
53664
  let truncated = false;
52751
- const dateStrings = listDateStrings3(normalized.fromMs, normalized.toMs);
53665
+ const dateStrings = listDateStrings4(normalized.fromMs, normalized.toMs);
52752
53666
  for (const date5 of dateStrings) {
52753
53667
  if (scannedLines >= MAX_LINES_SCANNED3) {
52754
53668
  truncated = true;
52755
53669
  break;
52756
53670
  }
52757
- const filePath = join5(eventsDir, `${date5}.jsonl`);
52758
- if (!existsSync4(filePath))
53671
+ const filePath = join6(eventsDir, `${date5}.jsonl`);
53672
+ if (!existsSync5(filePath))
52759
53673
  continue;
52760
53674
  scannedFiles += 1;
52761
- const stream = createReadStream3(filePath, { encoding: "utf-8" });
53675
+ const stream = createReadStream4(filePath, { encoding: "utf-8" });
52762
53676
  const rl = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
52763
53677
  for await (const line2 of rl) {
52764
53678
  if (scannedLines >= MAX_LINES_SCANNED3) {
@@ -52863,8 +53777,8 @@ async function queryLogSessions(context2, input) {
52863
53777
  }
52864
53778
 
52865
53779
  // src/log-storage.ts
52866
- import { existsSync as existsSync5, promises as fsPromises } from "fs";
52867
- import { join as join6 } from "path";
53780
+ import { existsSync as existsSync6, promises as fsPromises } from "fs";
53781
+ import { join as join7 } from "path";
52868
53782
  var cachedStorage = null;
52869
53783
  var calculationPromise = null;
52870
53784
  var lastCalculationTime = 0;
@@ -52872,7 +53786,7 @@ var CACHE_TTL_MS2 = 60 * 60 * 1000;
52872
53786
  var CALCULATION_INTERVAL_MS = 60 * 60 * 1000;
52873
53787
  var MIN_CALCULATION_INTERVAL_MS = 5 * 60 * 1000;
52874
53788
  async function calculateDirSize(dirPath) {
52875
- if (!existsSync5(dirPath)) {
53789
+ if (!existsSync6(dirPath)) {
52876
53790
  return { bytes: 0, fileCount: 0 };
52877
53791
  }
52878
53792
  let bytes = 0;
@@ -52881,7 +53795,7 @@ async function calculateDirSize(dirPath) {
52881
53795
  try {
52882
53796
  const entries = await fsPromises.readdir(currentPath, { withFileTypes: true });
52883
53797
  for (const entry of entries) {
52884
- const fullPath = join6(currentPath, entry.name);
53798
+ const fullPath = join7(currentPath, entry.name);
52885
53799
  if (entry.isDirectory()) {
52886
53800
  await walk(fullPath);
52887
53801
  } else if (entry.isFile()) {
@@ -52904,6 +53818,7 @@ async function doCalculateStorage(logConfig) {
52904
53818
  totalBytes: 0,
52905
53819
  eventsBytes: 0,
52906
53820
  streamsBytes: 0,
53821
+ indexBytes: 0,
52907
53822
  fileCount: 0,
52908
53823
  lastUpdatedAt: new Date().toISOString(),
52909
53824
  isCalculating: false
@@ -52911,18 +53826,40 @@ async function doCalculateStorage(logConfig) {
52911
53826
  }
52912
53827
  const baseDir = resolveLogBaseDir(logConfig);
52913
53828
  const [eventsResult, streamsResult] = await Promise.all([
52914
- calculateDirSize(join6(baseDir, "events")),
52915
- calculateDirSize(join6(baseDir, "streams"))
53829
+ calculateDirSize(join7(baseDir, "events")),
53830
+ calculateDirSize(join7(baseDir, "streams"))
52916
53831
  ]);
53832
+ const indexResult = await calculateIndexSize(baseDir);
52917
53833
  return {
52918
- totalBytes: eventsResult.bytes + streamsResult.bytes,
53834
+ totalBytes: eventsResult.bytes + streamsResult.bytes + indexResult.bytes,
52919
53835
  eventsBytes: eventsResult.bytes,
52920
53836
  streamsBytes: streamsResult.bytes,
52921
- fileCount: eventsResult.fileCount + streamsResult.fileCount,
53837
+ indexBytes: indexResult.bytes,
53838
+ fileCount: eventsResult.fileCount + streamsResult.fileCount + indexResult.fileCount,
52922
53839
  lastUpdatedAt: new Date().toISOString(),
52923
53840
  isCalculating: false
52924
53841
  };
52925
53842
  }
53843
+ async function calculateIndexSize(baseDir) {
53844
+ if (!existsSync6(baseDir)) {
53845
+ return { bytes: 0, fileCount: 0 };
53846
+ }
53847
+ let bytes = 0;
53848
+ let fileCount = 0;
53849
+ try {
53850
+ const entries = await fsPromises.readdir(baseDir, { withFileTypes: true });
53851
+ for (const entry of entries) {
53852
+ if (!entry.isFile() || !entry.name.startsWith("logs-index.sqlite"))
53853
+ continue;
53854
+ const stats = await fsPromises.stat(join7(baseDir, entry.name));
53855
+ bytes += stats.size;
53856
+ fileCount += 1;
53857
+ }
53858
+ } catch {
53859
+ return { bytes, fileCount };
53860
+ }
53861
+ return { bytes, fileCount };
53862
+ }
52926
53863
  async function getLogStorageInfo(options) {
52927
53864
  const { logConfig, forceRefresh = false, nowMs = Date.now() } = options;
52928
53865
  if (!forceRefresh && cachedStorage && cachedStorage.expiresAt > nowMs) {
@@ -52965,10 +53902,25 @@ function startLogStorageBackgroundTask(logConfig) {
52965
53902
  };
52966
53903
  }
52967
53904
 
52968
- // src/logger.ts
52969
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
52970
- import { join as join7 } from "path";
53905
+ // src/log-tail.ts
53906
+ var subscribers = new Set;
53907
+ function publishLogEvent(event) {
53908
+ for (const subscriber of subscribers) {
53909
+ try {
53910
+ subscriber(event);
53911
+ } catch {}
53912
+ }
53913
+ }
53914
+ function subscribeLogEvents(subscriber) {
53915
+ subscribers.add(subscriber);
53916
+ return () => {
53917
+ subscribers.delete(subscriber);
53918
+ };
53919
+ }
52971
53920
 
53921
+ // src/logger.ts
53922
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
53923
+ import { join as join8 } from "path";
52972
53924
  class Logger {
52973
53925
  baseDir;
52974
53926
  eventsDir;
@@ -52983,8 +53935,8 @@ class Logger {
52983
53935
  this._bodyPolicy = config2.bodyPolicy ?? "off";
52984
53936
  this._streamsEnabled = config2.streams?.enabled !== false;
52985
53937
  this.maxStreamBytes = config2.streams?.maxBytesPerRequest ?? 10 * 1024 * 1024;
52986
- this.eventsDir = join7(baseDir, "events");
52987
- this.streamsDir = join7(baseDir, "streams");
53938
+ this.eventsDir = join8(baseDir, "events");
53939
+ this.streamsDir = join8(baseDir, "streams");
52988
53940
  if (this._enabled)
52989
53941
  this.ensureDirs();
52990
53942
  }
@@ -52996,14 +53948,14 @@ class Logger {
52996
53948
  }
52997
53949
  ensureDirs() {
52998
53950
  for (const dir of [this.baseDir, this.eventsDir, this.streamsDir]) {
52999
- if (!existsSync6(dir))
53000
- mkdirSync2(dir, { recursive: true });
53951
+ if (!existsSync7(dir))
53952
+ mkdirSync3(dir, { recursive: true });
53001
53953
  }
53002
53954
  }
53003
53955
  ensureStreamDateDir(dateStr) {
53004
- const dir = join7(this.streamsDir, dateStr);
53005
- if (!existsSync6(dir))
53006
- mkdirSync2(dir, { recursive: true });
53956
+ const dir = join8(this.streamsDir, dateStr);
53957
+ if (!existsSync7(dir))
53958
+ mkdirSync3(dir, { recursive: true });
53007
53959
  return dir;
53008
53960
  }
53009
53961
  writeEvent(event) {
@@ -53012,9 +53964,21 @@ class Logger {
53012
53964
  try {
53013
53965
  this.ensureDirs();
53014
53966
  const dateStr = event.ts_start.slice(0, 10);
53015
- const filePath = join7(this.eventsDir, `${dateStr}.jsonl`);
53016
- appendFileSync(filePath, `${JSON.stringify(event)}
53017
- `);
53967
+ const filePath = join8(this.eventsDir, `${dateStr}.jsonl`);
53968
+ const offset = existsSync7(filePath) ? statSync2(filePath).size : 0;
53969
+ const line2 = `${JSON.stringify(event)}
53970
+ `;
53971
+ appendFileSync(filePath, line2);
53972
+ const id = encodeOffsetLogEventId(dateStr, offset);
53973
+ enqueueLogEventForIndex({
53974
+ baseDir: this.baseDir,
53975
+ filePath,
53976
+ date: dateStr,
53977
+ offset,
53978
+ byteLength: Buffer.byteLength(line2),
53979
+ event
53980
+ });
53981
+ publishLogEvent({ id, date: dateStr, filePath, offset, event });
53018
53982
  } catch (err) {
53019
53983
  console.error("[logger] \u4E8B\u4EF6\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
53020
53984
  }
@@ -53024,7 +53988,7 @@ class Logger {
53024
53988
  return null;
53025
53989
  try {
53026
53990
  const dir = this.ensureStreamDateDir(dateStr);
53027
- const filePath = join7(dir, `${requestId}.sse.raw`);
53991
+ const filePath = join8(dir, `${requestId}.sse.raw`);
53028
53992
  const toWrite = content.length > this.maxStreamBytes ? `${content.slice(0, this.maxStreamBytes)}
53029
53993
  [TRUNCATED]` : content;
53030
53994
  writeFileSync3(filePath, toWrite);
@@ -53038,6 +54002,7 @@ class Logger {
53038
54002
  var instance = null;
53039
54003
  function initLogger(baseDir, config2) {
53040
54004
  instance = new Logger(baseDir, config2);
54005
+ initLogIndex(baseDir, config2);
53041
54006
  if (instance.enabled) {
53042
54007
  console.log(`[logger] \u65E5\u5FD7\u7CFB\u7EDF\u5DF2\u521D\u59CB\u5316: ${baseDir}`);
53043
54008
  }
@@ -53047,6 +54012,7 @@ function getLogger() {
53047
54012
  }
53048
54013
  function resetLogger() {
53049
54014
  instance = null;
54015
+ disposeLogIndex();
53050
54016
  }
53051
54017
  function collectHeaders(headers) {
53052
54018
  const result = {};
@@ -54252,7 +55218,7 @@ var openAPISpec = {
54252
55218
  // src/plugin-loader.ts
54253
55219
  import { mkdtemp, rm, writeFile } from "fs/promises";
54254
55220
  import { tmpdir } from "os";
54255
- import { join as join8, resolve as resolve5 } from "path";
55221
+ import { join as join9, resolve as resolve5 } from "path";
54256
55222
  function isLocalPath(pkg) {
54257
55223
  return pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pkg);
54258
55224
  }
@@ -54275,7 +55241,7 @@ var remoteTmpDir = null;
54275
55241
  var remoteTmpFiles = [];
54276
55242
  async function ensureRemoteTmpDir() {
54277
55243
  if (!remoteTmpDir) {
54278
- remoteTmpDir = await mkdtemp(join8(tmpdir(), "local-router-plugins-"));
55244
+ remoteTmpDir = await mkdtemp(join9(tmpdir(), "local-router-plugins-"));
54279
55245
  }
54280
55246
  return remoteTmpDir;
54281
55247
  }
@@ -54288,7 +55254,7 @@ async function fetchRemotePlugin(url2) {
54288
55254
  const ext = inferExtension(url2, response.headers.get("content-type"));
54289
55255
  const dir = await ensureRemoteTmpDir();
54290
55256
  const fileName = `plugin_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
54291
- const filePath = join8(dir, fileName);
55257
+ const filePath = join9(dir, fileName);
54292
55258
  await writeFile(filePath, content, "utf-8");
54293
55259
  remoteTmpFiles.push(filePath);
54294
55260
  return filePath;
@@ -54427,7 +55393,7 @@ class PluginManager {
54427
55393
  // src/proxy.ts
54428
55394
  import { appendFile, readFile, unlink } from "fs/promises";
54429
55395
  import { tmpdir as tmpdir2 } from "os";
54430
- import { join as join9 } from "path";
55396
+ import { join as join10 } from "path";
54431
55397
 
54432
55398
  // src/plugin-engine.ts
54433
55399
  async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
@@ -54619,7 +55585,7 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
54619
55585
  };
54620
55586
  }
54621
55587
  function createTempStreamCapturePath(requestId) {
54622
- return join9(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
55588
+ return join10(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
54623
55589
  }
54624
55590
  async function appendTempStreamCapture(filePath, chunk) {
54625
55591
  await appendFile(filePath, chunk);
@@ -55445,7 +56411,6 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
55445
56411
  }
55446
56412
  });
55447
56413
  api2.get("/logs/tail", async (c2) => {
55448
- const config2 = store.get();
55449
56414
  const target = c2.req.raw;
55450
56415
  const windowRaw = c2.req.query("window") ?? "1h";
55451
56416
  if (!isLogQueryWindow(windowRaw)) {
@@ -55473,11 +56438,15 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
55473
56438
  }
55474
56439
  const encoder = new TextEncoder;
55475
56440
  let closed = false;
55476
- let lastSeenTs = Date.now() - 60 * 1000;
56441
+ const maxPendingItems = 500;
55477
56442
  let closeStream = null;
55478
56443
  const stream = new ReadableStream({
55479
56444
  start(controller) {
55480
- let timer = null;
56445
+ let heartbeatTimer = null;
56446
+ let unsubscribe = null;
56447
+ let flushQueued = false;
56448
+ let droppedItems = 0;
56449
+ const pending = [];
55481
56450
  const push2 = (event, payload) => {
55482
56451
  if (closed)
55483
56452
  return;
@@ -55491,60 +56460,106 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
55491
56460
  if (closed)
55492
56461
  return;
55493
56462
  closed = true;
55494
- if (timer) {
55495
- clearInterval(timer);
55496
- timer = null;
56463
+ if (heartbeatTimer) {
56464
+ clearInterval(heartbeatTimer);
56465
+ heartbeatTimer = null;
55497
56466
  }
56467
+ unsubscribe?.();
56468
+ unsubscribe = null;
55498
56469
  target.signal.removeEventListener("abort", close);
55499
56470
  try {
55500
56471
  controller.close();
55501
56472
  } catch {}
55502
56473
  };
55503
56474
  closeStream = close;
55504
- push2("ready", { ok: true, now: new Date().toISOString() });
55505
- timer = setInterval(async () => {
56475
+ const buildTailQuery = () => ({
56476
+ ...resolveLogQueryRange({
56477
+ window: windowRaw,
56478
+ from: c2.req.query("from"),
56479
+ to: c2.req.query("to")
56480
+ }),
56481
+ levels,
56482
+ providers: parseCommaSeparated(c2.req.query("provider")),
56483
+ routeTypes: parseCommaSeparated(c2.req.query("routeType")),
56484
+ models: parseCommaSeparated(c2.req.query("model")),
56485
+ modelIns: parseCommaSeparated(c2.req.query("modelIn")),
56486
+ modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
56487
+ users: parseCommaSeparated(c2.req.query("user")),
56488
+ sessions: parseCommaSeparated(c2.req.query("session")),
56489
+ statusClasses,
56490
+ hasError,
56491
+ q: c2.req.query("q") ?? "",
56492
+ sort: sortRaw,
56493
+ limit: 100,
56494
+ cursor: null
56495
+ });
56496
+ const flush = () => {
56497
+ flushQueued = false;
56498
+ if (closed || pending.length === 0)
56499
+ return;
56500
+ const items = pending.splice(0, pending.length);
56501
+ const overflowMessage = droppedItems > 0 ? `\u5B9E\u65F6\u8FFD\u8E2A\u961F\u5217\u5DF2\u4E22\u5F03 ${droppedItems} \u6761\u4E8B\u4EF6\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u4EE5\u8865\u9F50\u3002` : undefined;
56502
+ droppedItems = 0;
56503
+ push2("events", {
56504
+ items,
56505
+ nextCursor: null,
56506
+ hasMore: false,
56507
+ stats: {
56508
+ total: 0,
56509
+ errorCount: 0,
56510
+ errorRate: 0,
56511
+ avgLatencyMs: 0,
56512
+ p95LatencyMs: 0
56513
+ },
56514
+ meta: {
56515
+ scannedFiles: 0,
56516
+ scannedLines: 0,
56517
+ parseErrors: 0,
56518
+ truncated: false,
56519
+ indexUsed: true,
56520
+ indexFresh: true,
56521
+ usesFts: false,
56522
+ queryMs: 0,
56523
+ rowsReturned: items.length,
56524
+ fallbackReason: overflowMessage,
56525
+ statsMode: "none"
56526
+ }
56527
+ });
56528
+ };
56529
+ const queueFlush = () => {
56530
+ if (flushQueued)
56531
+ return;
56532
+ flushQueued = true;
56533
+ queueMicrotask(flush);
56534
+ };
56535
+ unsubscribe = subscribeLogEvents((published) => {
55506
56536
  if (closed)
55507
56537
  return;
55508
56538
  try {
55509
- const toMs = Date.now();
55510
- const data = await queryLogEvents({ logConfig: config2.log }, {
55511
- fromMs: Math.max(lastSeenTs, toMs - 60 * 60 * 1000),
55512
- toMs,
55513
- levels,
55514
- providers: parseCommaSeparated(c2.req.query("provider")),
55515
- routeTypes: parseCommaSeparated(c2.req.query("routeType")),
55516
- models: parseCommaSeparated(c2.req.query("model")),
55517
- modelIns: parseCommaSeparated(c2.req.query("modelIn")),
55518
- modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
55519
- users: parseCommaSeparated(c2.req.query("user")),
55520
- sessions: parseCommaSeparated(c2.req.query("session")),
55521
- statusClasses,
55522
- hasError,
55523
- q: c2.req.query("q") ?? "",
55524
- sort: sortRaw,
55525
- limit: 100
55526
- });
55527
- if (closed)
56539
+ const query = buildTailQuery();
56540
+ if (!logEventMatchesQuery(published.event, query))
55528
56541
  return;
55529
- if (data.items.length > 0) {
55530
- const maxTs = Math.max(...data.items.map((item) => Date.parse(item.ts)).filter(Number.isFinite));
55531
- if (Number.isFinite(maxTs)) {
55532
- lastSeenTs = Math.max(lastSeenTs, maxTs + 1);
55533
- }
55534
- push2("events", {
55535
- items: data.items,
55536
- stats: data.stats,
55537
- meta: data.meta
55538
- });
55539
- } else {
55540
- push2("heartbeat", { ts: new Date().toISOString() });
56542
+ if (pending.length >= maxPendingItems) {
56543
+ pending.shift();
56544
+ droppedItems += 1;
55541
56545
  }
56546
+ pending.push(createLogEventSummaryFromEvent(published.event, {
56547
+ id: published.id,
56548
+ date: published.date,
56549
+ line: null
56550
+ }));
56551
+ queueFlush();
55542
56552
  } catch (err) {
55543
- if (closed)
55544
- return;
55545
56553
  push2("error", { error: err instanceof Error ? err.message : String(err) });
55546
56554
  }
55547
- }, 3000);
56555
+ });
56556
+ push2("ready", { ok: true, now: new Date().toISOString() });
56557
+ heartbeatTimer = setInterval(() => {
56558
+ if (closed)
56559
+ return;
56560
+ push2("heartbeat", { ts: new Date().toISOString() });
56561
+ }, 15000);
56562
+ heartbeatTimer.unref?.();
55548
56563
  target.signal.addEventListener("abort", close);
55549
56564
  },
55550
56565
  cancel() {
@@ -55764,30 +56779,30 @@ async function startServer(options) {
55764
56779
  }
55765
56780
 
55766
56781
  // src/cli/runtime.ts
55767
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
56782
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
55768
56783
  import { homedir as homedir2 } from "os";
55769
- import { join as join10, resolve as resolve7 } from "path";
56784
+ import { join as join11, resolve as resolve7 } from "path";
55770
56785
  function getRuntimeDirs() {
55771
- const root2 = join10(homedir2(), ".local-router");
56786
+ const root2 = join11(homedir2(), ".local-router");
55772
56787
  return {
55773
56788
  root: root2,
55774
- run: join10(root2, "run"),
55775
- logs: join10(root2, "logs")
56789
+ run: join11(root2, "run"),
56790
+ logs: join11(root2, "logs")
55776
56791
  };
55777
56792
  }
55778
56793
  function getRuntimeFiles() {
55779
56794
  const dirs = getRuntimeDirs();
55780
56795
  return {
55781
- pid: join10(dirs.run, "local-router.pid"),
55782
- state: join10(dirs.run, "status.json"),
55783
- daemonLog: join10(dirs.logs, "daemon.log")
56796
+ pid: join11(dirs.run, "local-router.pid"),
56797
+ state: join11(dirs.run, "status.json"),
56798
+ daemonLog: join11(dirs.logs, "daemon.log")
55784
56799
  };
55785
56800
  }
55786
56801
  function ensureRuntimeDirs() {
55787
56802
  const dirs = getRuntimeDirs();
55788
- mkdirSync3(dirs.root, { recursive: true });
55789
- mkdirSync3(dirs.run, { recursive: true });
55790
- mkdirSync3(dirs.logs, { recursive: true });
56803
+ mkdirSync4(dirs.root, { recursive: true });
56804
+ mkdirSync4(dirs.run, { recursive: true });
56805
+ mkdirSync4(dirs.logs, { recursive: true });
55791
56806
  }
55792
56807
  function writeRuntimeState(state) {
55793
56808
  ensureRuntimeDirs();
@@ -55798,7 +56813,7 @@ function writeRuntimeState(state) {
55798
56813
  }
55799
56814
  function readRuntimeState() {
55800
56815
  const files = getRuntimeFiles();
55801
- if (!existsSync7(files.state)) {
56816
+ if (!existsSync8(files.state)) {
55802
56817
  return null;
55803
56818
  }
55804
56819
  try {
@@ -55953,8 +56968,8 @@ async function startDaemon(flags) {
55953
56968
  }
55954
56969
  ensureRuntimeDirs();
55955
56970
  const files = getRuntimeFiles();
55956
- const stdoutFd = openSync(files.daemonLog, "a");
55957
- const stderrFd = openSync(files.daemonLog, "a");
56971
+ const stdoutFd = openSync2(files.daemonLog, "a");
56972
+ const stderrFd = openSync2(files.daemonLog, "a");
55958
56973
  const childArgs = [process.argv[1] ?? "src/cli.ts", "__run-server", "--mode", "daemon"];
55959
56974
  if (flags.config) {
55960
56975
  childArgs.push("--config", resolveConfigArgPath(flags.config));
@@ -55975,8 +56990,8 @@ async function startDaemon(flags) {
55975
56990
  stderr: stderrFd,
55976
56991
  detached: true
55977
56992
  });
55978
- closeSync(stdoutFd);
55979
- closeSync(stderrFd);
56993
+ closeSync2(stdoutFd);
56994
+ closeSync2(stderrFd);
55980
56995
  child.unref();
55981
56996
  for (let i = 0;i < 24; i += 1) {
55982
56997
  await sleep(250);
@@ -56024,7 +57039,7 @@ async function stopProcess(graceMs = 8000) {
56024
57039
  }
56025
57040
  function readLogDelta(filePath, offset) {
56026
57041
  try {
56027
- const stats = statSync(filePath);
57042
+ const stats = statSync3(filePath);
56028
57043
  if (stats.size <= offset) {
56029
57044
  return { content: "", nextOffset: offset };
56030
57045
  }
@@ -56043,9 +57058,9 @@ function readConfig(configArg) {
56043
57058
  }
56044
57059
  function saveConfig(path, config2) {
56045
57060
  validateConfigOrThrow(config2);
56046
- const backupDir = join11(dirname3(path), ".backups");
56047
- mkdirSync4(backupDir, { recursive: true });
56048
- const backupPath = join11(backupDir, `config-${Date.now()}.json5`);
57061
+ const backupDir = join12(dirname3(path), ".backups");
57062
+ mkdirSync5(backupDir, { recursive: true });
57063
+ const backupPath = join12(backupDir, `config-${Date.now()}.json5`);
56049
57064
  writeFileSync5(backupPath, readFileSync7(path, "utf-8"), "utf-8");
56050
57065
  const content = dist_default.stringify(config2, { space: 2, quote: '"' });
56051
57066
  writeFileSync5(path, content, "utf-8");
@@ -56715,7 +57730,7 @@ async function cmdStatus(args) {
56715
57730
  }
56716
57731
  }
56717
57732
  function printLastLines(filePath, lines) {
56718
- if (!existsSync8(filePath)) {
57733
+ if (!existsSync9(filePath)) {
56719
57734
  console.log(`\u65E5\u5FD7\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`);
56720
57735
  return 0;
56721
57736
  }