@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/entry.js CHANGED
@@ -51789,10 +51789,23 @@ async function getLogMetrics(options) {
51789
51789
  }
51790
51790
 
51791
51791
  // src/log-query.ts
51792
- import { createReadStream as createReadStream2, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
51793
- import { join as join4, resolve as resolve4 } from "path";
51792
+ import { createReadStream as createReadStream3, existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
51793
+ import { join as join5, resolve as resolve4 } from "path";
51794
51794
  import { createInterface as createInterface2 } from "readline";
51795
51795
 
51796
+ // src/log-index.ts
51797
+ import { Database } from "bun:sqlite";
51798
+ import {
51799
+ closeSync,
51800
+ createReadStream as createReadStream2,
51801
+ existsSync as existsSync3,
51802
+ mkdirSync as mkdirSync2,
51803
+ openSync,
51804
+ readSync,
51805
+ statSync
51806
+ } from "fs";
51807
+ import { join as join4 } from "path";
51808
+
51796
51809
  // src/log-session-identity.ts
51797
51810
  var USER_SESSION_DELIMITER = "_account__session_";
51798
51811
  function toRecord(value) {
@@ -51869,6 +51882,824 @@ function resolveLogSessionIdentity(requestBody) {
51869
51882
  };
51870
51883
  }
51871
51884
 
51885
+ // src/log-index.ts
51886
+ var SCHEMA_VERSION = 1;
51887
+ var MAX_INDEX_QUEUE = 20000;
51888
+ var INDEX_BATCH_SIZE = 250;
51889
+ var INDEX_FLUSH_DELAY_MS = 50;
51890
+ var LIKE_SEARCH_THRESHOLD = 2;
51891
+ var FTS_TOKEN_PATTERN = /[\p{L}\p{N}_-]+/gu;
51892
+ var singleton = null;
51893
+ function encodeBase64Url(value) {
51894
+ return Buffer.from(value, "utf-8").toString("base64url");
51895
+ }
51896
+ function decodeBase64Url(value) {
51897
+ return Buffer.from(value, "base64url").toString("utf-8");
51898
+ }
51899
+ function encodeOffsetLogEventId(date5, offset) {
51900
+ return encodeBase64Url(JSON.stringify({ v: 2, d: date5, o: offset }));
51901
+ }
51902
+ function decodeOffsetLogEventId(id) {
51903
+ try {
51904
+ const parsed = JSON.parse(decodeBase64Url(id));
51905
+ if (parsed.v !== 2 || typeof parsed.d !== "string" || !Number.isInteger(parsed.o)) {
51906
+ return null;
51907
+ }
51908
+ const offset = Number(parsed.o);
51909
+ if (offset < 0)
51910
+ return null;
51911
+ return { v: 2, date: parsed.d, offset };
51912
+ } catch {
51913
+ return null;
51914
+ }
51915
+ }
51916
+ function encodeCursor(data) {
51917
+ return encodeBase64Url(JSON.stringify(data));
51918
+ }
51919
+ function decodeCursor(raw2) {
51920
+ const parsed = JSON.parse(decodeBase64Url(raw2));
51921
+ if (parsed.v !== 2 || parsed.sort !== "time_desc" && parsed.sort !== "time_asc" || typeof parsed.id !== "string" || typeof parsed.queryHash !== "string" || !Number.isFinite(parsed.tsMs)) {
51922
+ throw new Error("cursor \u975E\u6CD5");
51923
+ }
51924
+ return parsed;
51925
+ }
51926
+ function toDayStart2(ms) {
51927
+ const date5 = new Date(ms);
51928
+ return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
51929
+ }
51930
+ function listDateStrings2(fromMs, toMs) {
51931
+ const result = [];
51932
+ for (let day = toDayStart2(fromMs);day <= toDayStart2(toMs); day += 24 * 60 * 60 * 1000) {
51933
+ result.push(new Date(day).toISOString().slice(0, 10));
51934
+ }
51935
+ return result;
51936
+ }
51937
+ function getStatusClass2(event) {
51938
+ if (event.error_type)
51939
+ return "network_error";
51940
+ const status = event.upstream_status ?? 0;
51941
+ if (status >= 200 && status < 300)
51942
+ return "2xx";
51943
+ if (status >= 400 && status < 500)
51944
+ return "4xx";
51945
+ if (status >= 500)
51946
+ return "5xx";
51947
+ return "network_error";
51948
+ }
51949
+ function isErrorEvent2(event) {
51950
+ if (event.error_type)
51951
+ return true;
51952
+ const status = event.upstream_status ?? 0;
51953
+ return status < 200 || status >= 400;
51954
+ }
51955
+ function getLevel(event) {
51956
+ return isErrorEvent2(event) ? "error" : "info";
51957
+ }
51958
+ function buildMessage(event) {
51959
+ if (event.error_message)
51960
+ return event.error_message;
51961
+ if (event.error_type)
51962
+ return event.error_type;
51963
+ const status = event.upstream_status ?? 0;
51964
+ return `${event.method} ${event.path} -> ${status}`;
51965
+ }
51966
+ function toPercent2(numerator, denominator) {
51967
+ if (denominator <= 0)
51968
+ return 0;
51969
+ return Number((numerator / denominator * 100).toFixed(2));
51970
+ }
51971
+ function hashQuery(query) {
51972
+ const stable = {
51973
+ fromMs: query.fromMs,
51974
+ toMs: query.toMs,
51975
+ levels: [...query.levels].sort(),
51976
+ providers: [...query.providers].sort(),
51977
+ routeTypes: [...query.routeTypes].sort(),
51978
+ models: [...query.models].sort(),
51979
+ modelIns: [...query.modelIns].sort(),
51980
+ modelOuts: [...query.modelOuts].sort(),
51981
+ users: [...query.users].sort(),
51982
+ sessions: [...query.sessions].sort(),
51983
+ statusClasses: [...query.statusClasses].sort(),
51984
+ hasError: query.hasError,
51985
+ q: query.q
51986
+ };
51987
+ return Bun.hash(JSON.stringify(stable)).toString(36);
51988
+ }
51989
+ function buildSearchText(event) {
51990
+ const identity = resolveLogSessionIdentity(event.request_body);
51991
+ return [
51992
+ event.request_id,
51993
+ event.path,
51994
+ event.provider,
51995
+ event.model_in,
51996
+ event.model_out,
51997
+ event.route_type,
51998
+ identity.userIdRaw ?? "",
51999
+ identity.userKey ?? "",
52000
+ identity.sessionId ?? "",
52001
+ event.error_type ?? "",
52002
+ event.error_message ?? "",
52003
+ buildMessage(event)
52004
+ ].join(" ").toLowerCase();
52005
+ }
52006
+ function buildFtsQuery(q) {
52007
+ const tokens = q.match(FTS_TOKEN_PATTERN)?.map((token2) => token2.trim()).filter(Boolean) ?? [];
52008
+ if (tokens.length === 0)
52009
+ return null;
52010
+ return tokens.map((token2) => `"${token2.replaceAll('"', '""')}"`).join(" AND ");
52011
+ }
52012
+ function shouldUseFts(q) {
52013
+ return q.trim().length >= LIKE_SEARCH_THRESHOLD && buildFtsQuery(q) !== null;
52014
+ }
52015
+ function escapeLikePattern(value) {
52016
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
52017
+ }
52018
+ function eventToRow(input) {
52019
+ const { event } = input;
52020
+ if (!event.ts_start)
52021
+ return null;
52022
+ const tsMs = Date.parse(event.ts_start);
52023
+ if (!Number.isFinite(tsMs))
52024
+ return null;
52025
+ const identity = resolveLogSessionIdentity(event.request_body);
52026
+ const level = getLevel(event);
52027
+ const statusClass = getStatusClass2(event);
52028
+ const latencyMs = Math.max(0, event.latency_ms ?? 0);
52029
+ const model = event.model_out || event.model_in;
52030
+ return {
52031
+ id: input.id,
52032
+ ts_ms: tsMs,
52033
+ ts_start: event.ts_start,
52034
+ level,
52035
+ provider: event.provider,
52036
+ route_type: event.route_type,
52037
+ model,
52038
+ model_in: event.model_in,
52039
+ model_out: event.model_out,
52040
+ path: event.path,
52041
+ request_id: event.request_id,
52042
+ latency_ms: latencyMs,
52043
+ upstream_status: event.upstream_status ?? 0,
52044
+ status_class: statusClass,
52045
+ has_error: level === "error" ? 1 : 0,
52046
+ message: buildMessage(event),
52047
+ error_type: event.error_type,
52048
+ has_metadata: identity.hasMetadata ? 1 : 0,
52049
+ user_id_raw: identity.userIdRaw,
52050
+ user_key: identity.userKey,
52051
+ session_id: identity.sessionId,
52052
+ source_date: input.date,
52053
+ source_file: input.filePath,
52054
+ line_number: input.lineNumber,
52055
+ byte_offset: input.offset,
52056
+ byte_length: input.byteLength,
52057
+ search_text: buildSearchText(event)
52058
+ };
52059
+ }
52060
+ function rowToSummary(row) {
52061
+ return {
52062
+ id: row.id,
52063
+ ts: row.ts_start,
52064
+ level: row.level,
52065
+ provider: row.provider,
52066
+ routeType: row.route_type,
52067
+ model: row.model,
52068
+ modelIn: row.model_in,
52069
+ modelOut: row.model_out,
52070
+ path: row.path,
52071
+ requestId: row.request_id,
52072
+ latencyMs: row.latency_ms,
52073
+ upstreamStatus: row.upstream_status,
52074
+ statusClass: row.status_class,
52075
+ hasError: row.has_error === 1,
52076
+ message: row.message,
52077
+ errorType: row.error_type,
52078
+ hasMetadata: row.has_metadata === 1,
52079
+ userIdRaw: row.user_id_raw,
52080
+ userKey: row.user_key,
52081
+ sessionId: row.session_id
52082
+ };
52083
+ }
52084
+ async function* readJsonlLinesWithOffsets(filePath) {
52085
+ const stream = createReadStream2(filePath);
52086
+ let buffer2 = Buffer.alloc(0);
52087
+ let bufferOffset = 0;
52088
+ let lineNumber = 0;
52089
+ for await (const chunk of stream) {
52090
+ const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
52091
+ buffer2 = buffer2.length === 0 ? chunkBuffer : Buffer.concat([buffer2, chunkBuffer]);
52092
+ let newlineIndex = buffer2.indexOf(10);
52093
+ while (newlineIndex !== -1) {
52094
+ const lineBuffer = buffer2.subarray(0, newlineIndex);
52095
+ const byteLength = newlineIndex + 1;
52096
+ const lineOffset = bufferOffset;
52097
+ lineNumber += 1;
52098
+ yield {
52099
+ line: lineBuffer.toString("utf-8").replace(/\r$/, ""),
52100
+ offset: lineOffset,
52101
+ lineNumber,
52102
+ byteLength
52103
+ };
52104
+ buffer2 = buffer2.subarray(byteLength);
52105
+ bufferOffset += byteLength;
52106
+ newlineIndex = buffer2.indexOf(10);
52107
+ }
52108
+ }
52109
+ if (buffer2.length > 0) {
52110
+ lineNumber += 1;
52111
+ yield {
52112
+ line: buffer2.toString("utf-8").replace(/\r$/, ""),
52113
+ offset: bufferOffset,
52114
+ lineNumber,
52115
+ byteLength: buffer2.length
52116
+ };
52117
+ }
52118
+ }
52119
+ function readLineAtOffset(filePath, offset) {
52120
+ const fd = openSync(filePath, "r");
52121
+ try {
52122
+ const chunks = [];
52123
+ const buffer2 = Buffer.allocUnsafe(64 * 1024);
52124
+ let position = offset;
52125
+ while (true) {
52126
+ const bytesRead = readSync(fd, buffer2, 0, buffer2.length, position);
52127
+ if (bytesRead <= 0)
52128
+ break;
52129
+ const readable = buffer2.subarray(0, bytesRead);
52130
+ const newline = readable.indexOf(10);
52131
+ if (newline >= 0 && newline < bytesRead) {
52132
+ chunks.push(Buffer.from(readable.subarray(0, newline)));
52133
+ break;
52134
+ }
52135
+ chunks.push(Buffer.from(readable));
52136
+ position += bytesRead;
52137
+ }
52138
+ if (chunks.length === 0)
52139
+ return null;
52140
+ return Buffer.concat(chunks).toString("utf-8").replace(/\r$/, "");
52141
+ } finally {
52142
+ closeSync(fd);
52143
+ }
52144
+ }
52145
+ function createEmptyStats() {
52146
+ return {
52147
+ total: 0,
52148
+ errorCount: 0,
52149
+ errorRate: 0,
52150
+ avgLatencyMs: 0,
52151
+ p95LatencyMs: 0
52152
+ };
52153
+ }
52154
+ function createEmptyQueryResult(query, meta3 = {}) {
52155
+ return {
52156
+ items: [],
52157
+ nextCursor: null,
52158
+ hasMore: false,
52159
+ stats: createEmptyStats(),
52160
+ meta: {
52161
+ scannedFiles: 0,
52162
+ scannedLines: 0,
52163
+ parseErrors: 0,
52164
+ truncated: false,
52165
+ indexUsed: true,
52166
+ indexFresh: true,
52167
+ usesFts: shouldUseFts(query.q),
52168
+ queryMs: 0,
52169
+ rowsReturned: 0,
52170
+ statsMode: "exact",
52171
+ ...meta3
52172
+ }
52173
+ };
52174
+ }
52175
+ function appendInClause(clauses, params, column2, values) {
52176
+ if (values.length === 0)
52177
+ return;
52178
+ clauses.push(`${column2} IN (${values.map(() => "?").join(", ")})`);
52179
+ params.push(...values);
52180
+ }
52181
+ function buildWhereClause(query, options = {}) {
52182
+ const clauses = ["e.ts_ms >= ?", "e.ts_ms <= ?"];
52183
+ const params = [query.fromMs, query.toMs];
52184
+ const usesFts = !options.forceLikeSearch && shouldUseFts(query.q);
52185
+ appendInClause(clauses, params, "e.level", query.levels);
52186
+ appendInClause(clauses, params, "e.provider", query.providers);
52187
+ appendInClause(clauses, params, "e.route_type", query.routeTypes);
52188
+ appendInClause(clauses, params, "e.model", query.models);
52189
+ appendInClause(clauses, params, "e.model_in", query.modelIns);
52190
+ appendInClause(clauses, params, "e.model_out", query.modelOuts);
52191
+ appendInClause(clauses, params, "e.status_class", query.statusClasses);
52192
+ if (query.users.length > 0) {
52193
+ clauses.push(`(e.user_id_raw IN (${query.users.map(() => "?").join(", ")}) OR e.user_key IN (${query.users.map(() => "?").join(", ")}))`);
52194
+ params.push(...query.users, ...query.users);
52195
+ }
52196
+ appendInClause(clauses, params, "e.session_id", query.sessions);
52197
+ if (query.hasError !== null) {
52198
+ clauses.push("e.has_error = ?");
52199
+ params.push(query.hasError ? 1 : 0);
52200
+ }
52201
+ if (query.q) {
52202
+ if (usesFts) {
52203
+ const ftsQuery = buildFtsQuery(query.q);
52204
+ clauses.push("e.id IN (SELECT event_id FROM log_events_fts WHERE log_events_fts MATCH ?)");
52205
+ params.push(ftsQuery);
52206
+ } else {
52207
+ clauses.push("e.search_text LIKE ? ESCAPE '\\'");
52208
+ params.push(`%${escapeLikePattern(query.q.toLowerCase())}%`);
52209
+ }
52210
+ }
52211
+ return {
52212
+ whereSql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
52213
+ params,
52214
+ usesFts
52215
+ };
52216
+ }
52217
+
52218
+ class LogIndex {
52219
+ baseDir;
52220
+ config;
52221
+ db;
52222
+ queue = [];
52223
+ flushTimer = null;
52224
+ disposed = false;
52225
+ rebuildingFiles = new Set;
52226
+ dirtyFiles = new Set;
52227
+ insertEventStmt;
52228
+ insertFtsStmt;
52229
+ deleteFtsStmt;
52230
+ upsertFileStmt;
52231
+ constructor(baseDir, config2) {
52232
+ this.baseDir = baseDir;
52233
+ this.config = config2;
52234
+ mkdirSync2(baseDir, { recursive: true });
52235
+ const dbPath = join4(baseDir, "logs-index.sqlite");
52236
+ this.db = new Database(dbPath, { create: true, strict: true });
52237
+ this.configure();
52238
+ this.migrate();
52239
+ this.insertEventStmt = this.db.prepare(`
52240
+ INSERT OR REPLACE INTO log_events (
52241
+ id, ts_ms, ts_start, level, provider, route_type, model, model_in, model_out,
52242
+ path, request_id, latency_ms, upstream_status, status_class, has_error,
52243
+ message, error_type, has_metadata, user_id_raw, user_key, session_id,
52244
+ source_date, source_file, line_number, byte_offset, byte_length, search_text
52245
+ ) VALUES (
52246
+ $id, $ts_ms, $ts_start, $level, $provider, $route_type, $model, $model_in, $model_out,
52247
+ $path, $request_id, $latency_ms, $upstream_status, $status_class, $has_error,
52248
+ $message, $error_type, $has_metadata, $user_id_raw, $user_key, $session_id,
52249
+ $source_date, $source_file, $line_number, $byte_offset, $byte_length, $search_text
52250
+ )
52251
+ `);
52252
+ this.deleteFtsStmt = this.db.prepare("DELETE FROM log_events_fts WHERE event_id = ?");
52253
+ this.insertFtsStmt = this.db.prepare("INSERT INTO log_events_fts(event_id, search_text) VALUES (?, ?)");
52254
+ this.upsertFileStmt = this.db.prepare(`
52255
+ INSERT INTO log_index_files(file_path, source_date, size_bytes, mtime_ms, indexed_at)
52256
+ VALUES (?, ?, ?, ?, ?)
52257
+ ON CONFLICT(file_path) DO UPDATE SET
52258
+ source_date = excluded.source_date,
52259
+ size_bytes = excluded.size_bytes,
52260
+ mtime_ms = excluded.mtime_ms,
52261
+ indexed_at = excluded.indexed_at
52262
+ `);
52263
+ }
52264
+ dispose() {
52265
+ if (this.flushTimer) {
52266
+ clearTimeout(this.flushTimer);
52267
+ this.flushTimer = null;
52268
+ }
52269
+ this.queue = [];
52270
+ this.disposed = true;
52271
+ this.db.close();
52272
+ }
52273
+ enqueue(item) {
52274
+ if (this.disposed)
52275
+ return;
52276
+ if (this.queue.length >= MAX_INDEX_QUEUE) {
52277
+ const dropped = this.queue.shift();
52278
+ if (dropped) {
52279
+ this.dirtyFiles.add(dropped.filePath);
52280
+ }
52281
+ }
52282
+ this.queue.push(item);
52283
+ if (!this.flushTimer) {
52284
+ this.flushTimer = setTimeout(() => {
52285
+ this.flushTimer = null;
52286
+ this.flushQueue();
52287
+ }, INDEX_FLUSH_DELAY_MS);
52288
+ this.flushTimer.unref?.();
52289
+ }
52290
+ }
52291
+ async ensureRangeIndexed(fromMs, toMs) {
52292
+ let scannedFiles = 0;
52293
+ let scannedLines = 0;
52294
+ let parseErrors = 0;
52295
+ const eventsDir = join4(this.baseDir, "events");
52296
+ const dates = listDateStrings2(fromMs, toMs);
52297
+ for (const date5 of dates) {
52298
+ const filePath = join4(eventsDir, `${date5}.jsonl`);
52299
+ if (!existsSync3(filePath))
52300
+ continue;
52301
+ const stats = statSync(filePath);
52302
+ const fileRow = this.db.query("SELECT size_bytes, mtime_ms FROM log_index_files WHERE file_path = ?").get(filePath);
52303
+ const sizeBytes = stats.size;
52304
+ const mtimeMs = Math.trunc(stats.mtimeMs);
52305
+ if (!this.dirtyFiles.has(filePath) && fileRow && fileRow.size_bytes === sizeBytes && fileRow.mtime_ms === mtimeMs) {
52306
+ continue;
52307
+ }
52308
+ const result = await this.rebuildFile(filePath, date5, sizeBytes, mtimeMs);
52309
+ scannedFiles += 1;
52310
+ scannedLines += result.scannedLines;
52311
+ parseErrors += result.parseErrors;
52312
+ }
52313
+ return { scannedFiles, scannedLines, parseErrors };
52314
+ }
52315
+ queryEvents(query, options = {}) {
52316
+ const startedAt = performance.now();
52317
+ const queryHash = hashQuery(query);
52318
+ const decodedCursor = query.cursor ? decodeCursor(query.cursor) : null;
52319
+ if (decodedCursor) {
52320
+ if (decodedCursor.sort !== query.sort || decodedCursor.queryHash !== queryHash) {
52321
+ throw new Error("cursor \u4E0E\u5F53\u524D\u67E5\u8BE2\u6761\u4EF6\u4E0D\u5339\u914D");
52322
+ }
52323
+ }
52324
+ const { whereSql, params, usesFts } = buildWhereClause(query, options);
52325
+ 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 > ?))" : "";
52326
+ const cursorParams = decodedCursor ? [decodedCursor.tsMs, decodedCursor.tsMs, decodedCursor.id] : [];
52327
+ const orderSql = query.sort === "time_desc" ? "ORDER BY e.ts_ms DESC, e.id DESC" : "ORDER BY e.ts_ms ASC, e.id ASC";
52328
+ const limit = Math.max(1, query.limit);
52329
+ let rows;
52330
+ try {
52331
+ rows = this.db.query(`
52332
+ SELECT
52333
+ e.id, e.ts_start, e.level, e.provider, e.route_type, e.model, e.model_in,
52334
+ e.model_out, e.path, e.request_id, e.latency_ms, e.upstream_status,
52335
+ e.status_class, e.has_error, e.message, e.error_type, e.has_metadata,
52336
+ e.user_id_raw, e.user_key, e.session_id, e.ts_ms
52337
+ FROM log_events e
52338
+ ${whereSql}
52339
+ ${cursorClause}
52340
+ ${orderSql}
52341
+ LIMIT ?
52342
+ `).all(...params, ...cursorParams, limit + 1);
52343
+ } catch (err) {
52344
+ if (!usesFts)
52345
+ throw err;
52346
+ const fallback = this.queryEvents(query, { forceLikeSearch: true });
52347
+ return {
52348
+ ...fallback,
52349
+ meta: {
52350
+ ...fallback.meta,
52351
+ usesFts: false,
52352
+ fallbackReason: `FTS \u67E5\u8BE2\u5931\u8D25\uFF0C\u5DF2\u9000\u56DE\u7D22\u5F15 LIKE/\u5185\u5B58\u8FC7\u6EE4: ${err instanceof Error ? err.message : String(err)}`
52353
+ }
52354
+ };
52355
+ }
52356
+ const pageRows = rows.slice(0, limit);
52357
+ const hasMore = rows.length > limit;
52358
+ const lastRow = pageRows[pageRows.length - 1];
52359
+ const stats = this.queryStats(whereSql, params);
52360
+ return {
52361
+ items: pageRows.map(rowToSummary),
52362
+ nextCursor: hasMore && lastRow ? encodeCursor({
52363
+ v: 2,
52364
+ sort: query.sort,
52365
+ tsMs: lastRow.ts_ms,
52366
+ id: lastRow.id,
52367
+ queryHash
52368
+ }) : null,
52369
+ hasMore,
52370
+ stats,
52371
+ meta: {
52372
+ scannedFiles: 0,
52373
+ scannedLines: 0,
52374
+ parseErrors: 0,
52375
+ truncated: false,
52376
+ indexUsed: true,
52377
+ indexFresh: true,
52378
+ usesFts,
52379
+ queryMs: Math.round((performance.now() - startedAt) * 100) / 100,
52380
+ rowsReturned: pageRows.length,
52381
+ statsMode: "exact"
52382
+ }
52383
+ };
52384
+ }
52385
+ getEventRecordByOffsetId(id) {
52386
+ const parsedId = decodeOffsetLogEventId(id);
52387
+ if (!parsedId)
52388
+ return null;
52389
+ const row = this.db.query("SELECT source_date, source_file, line_number, byte_offset FROM log_events WHERE id = ?").get(id);
52390
+ const filePath = row?.source_file ?? join4(this.baseDir, "events", `${parsedId.date}.jsonl`);
52391
+ if (!existsSync3(filePath))
52392
+ return null;
52393
+ const line2 = readLineAtOffset(filePath, row?.byte_offset ?? parsedId.offset);
52394
+ if (!line2?.trim())
52395
+ return null;
52396
+ const event = JSON.parse(line2);
52397
+ return {
52398
+ event,
52399
+ location: {
52400
+ id,
52401
+ date: row?.source_date ?? parsedId.date,
52402
+ file: filePath,
52403
+ line: row?.line_number ?? null,
52404
+ offset: row?.byte_offset ?? parsedId.offset
52405
+ }
52406
+ };
52407
+ }
52408
+ configure() {
52409
+ this.db.exec(`
52410
+ PRAGMA journal_mode = WAL;
52411
+ PRAGMA synchronous = NORMAL;
52412
+ PRAGMA temp_store = MEMORY;
52413
+ PRAGMA busy_timeout = 3000;
52414
+ PRAGMA foreign_keys = ON;
52415
+ `);
52416
+ }
52417
+ migrate() {
52418
+ this.db.exec(`
52419
+ CREATE TABLE IF NOT EXISTS log_index_meta (
52420
+ key TEXT PRIMARY KEY,
52421
+ value TEXT NOT NULL
52422
+ );
52423
+
52424
+ CREATE TABLE IF NOT EXISTS log_index_files (
52425
+ file_path TEXT PRIMARY KEY,
52426
+ source_date TEXT NOT NULL,
52427
+ size_bytes INTEGER NOT NULL,
52428
+ mtime_ms INTEGER NOT NULL,
52429
+ indexed_at INTEGER NOT NULL
52430
+ );
52431
+
52432
+ CREATE TABLE IF NOT EXISTS log_events (
52433
+ id TEXT PRIMARY KEY,
52434
+ ts_ms INTEGER NOT NULL,
52435
+ ts_start TEXT NOT NULL,
52436
+ level TEXT NOT NULL,
52437
+ provider TEXT NOT NULL,
52438
+ route_type TEXT NOT NULL,
52439
+ model TEXT NOT NULL,
52440
+ model_in TEXT NOT NULL,
52441
+ model_out TEXT NOT NULL,
52442
+ path TEXT NOT NULL,
52443
+ request_id TEXT NOT NULL,
52444
+ latency_ms INTEGER NOT NULL,
52445
+ upstream_status INTEGER NOT NULL,
52446
+ status_class TEXT NOT NULL,
52447
+ has_error INTEGER NOT NULL,
52448
+ message TEXT NOT NULL,
52449
+ error_type TEXT,
52450
+ has_metadata INTEGER NOT NULL,
52451
+ user_id_raw TEXT,
52452
+ user_key TEXT,
52453
+ session_id TEXT,
52454
+ source_date TEXT NOT NULL,
52455
+ source_file TEXT NOT NULL,
52456
+ line_number INTEGER,
52457
+ byte_offset INTEGER NOT NULL,
52458
+ byte_length INTEGER NOT NULL,
52459
+ search_text TEXT NOT NULL
52460
+ );
52461
+
52462
+ CREATE VIRTUAL TABLE IF NOT EXISTS log_events_fts
52463
+ USING fts5(event_id UNINDEXED, search_text);
52464
+
52465
+ CREATE INDEX IF NOT EXISTS idx_log_events_time_desc ON log_events(ts_ms DESC, id DESC);
52466
+ CREATE INDEX IF NOT EXISTS idx_log_events_time_asc ON log_events(ts_ms ASC, id ASC);
52467
+ CREATE INDEX IF NOT EXISTS idx_log_events_level_time ON log_events(level, ts_ms DESC);
52468
+ CREATE INDEX IF NOT EXISTS idx_log_events_provider_time ON log_events(provider, ts_ms DESC);
52469
+ CREATE INDEX IF NOT EXISTS idx_log_events_route_time ON log_events(route_type, ts_ms DESC);
52470
+ CREATE INDEX IF NOT EXISTS idx_log_events_model_time ON log_events(model, ts_ms DESC);
52471
+ CREATE INDEX IF NOT EXISTS idx_log_events_status_time ON log_events(status_class, ts_ms DESC);
52472
+ CREATE INDEX IF NOT EXISTS idx_log_events_error_time ON log_events(has_error, ts_ms DESC);
52473
+ CREATE INDEX IF NOT EXISTS idx_log_events_user_time ON log_events(user_key, ts_ms DESC);
52474
+ CREATE INDEX IF NOT EXISTS idx_log_events_session_time ON log_events(session_id, ts_ms DESC);
52475
+ CREATE INDEX IF NOT EXISTS idx_log_events_file ON log_events(source_file);
52476
+ `);
52477
+ this.db.prepare(`
52478
+ INSERT INTO log_index_meta(key, value)
52479
+ VALUES ('schema_version', ?)
52480
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
52481
+ `).run(String(SCHEMA_VERSION));
52482
+ }
52483
+ flushQueue() {
52484
+ if (this.queue.length === 0 || this.disposed)
52485
+ return;
52486
+ const batch = this.queue.splice(0, INDEX_BATCH_SIZE);
52487
+ const transaction = this.db.transaction((items) => {
52488
+ for (const item of items) {
52489
+ this.insertQueueItem(item);
52490
+ }
52491
+ });
52492
+ try {
52493
+ transaction(batch);
52494
+ } catch (err) {
52495
+ console.error("[log-index] \u589E\u91CF\u7D22\u5F15\u5199\u5165\u5931\u8D25:", err);
52496
+ }
52497
+ if (this.queue.length > 0 && !this.flushTimer) {
52498
+ this.flushTimer = setTimeout(() => {
52499
+ this.flushTimer = null;
52500
+ this.flushQueue();
52501
+ }, INDEX_FLUSH_DELAY_MS);
52502
+ this.flushTimer.unref?.();
52503
+ }
52504
+ }
52505
+ insertQueueItem(item) {
52506
+ if (this.rebuildingFiles.has(item.filePath))
52507
+ return;
52508
+ const id = encodeOffsetLogEventId(item.date, item.offset);
52509
+ const row = eventToRow({
52510
+ id,
52511
+ date: item.date,
52512
+ filePath: item.filePath,
52513
+ lineNumber: null,
52514
+ offset: item.offset,
52515
+ byteLength: item.byteLength,
52516
+ event: item.event
52517
+ });
52518
+ if (!row)
52519
+ return;
52520
+ this.insertEventStmt.run(row);
52521
+ this.deleteFtsStmt.run(id);
52522
+ this.insertFtsStmt.run(id, row.search_text);
52523
+ if (!this.dirtyFiles.has(item.filePath)) {
52524
+ try {
52525
+ const stats = statSync(item.filePath);
52526
+ const indexedThrough = item.offset + item.byteLength;
52527
+ this.upsertFileStmt.run(item.filePath, item.date, Math.min(indexedThrough, stats.size), Math.trunc(stats.mtimeMs), Date.now());
52528
+ } catch {}
52529
+ }
52530
+ }
52531
+ async rebuildFile(filePath, date5, sizeBytes, mtimeMs) {
52532
+ if (this.rebuildingFiles.has(filePath)) {
52533
+ return { scannedLines: 0, parseErrors: 0 };
52534
+ }
52535
+ this.rebuildingFiles.add(filePath);
52536
+ let scannedLines = 0;
52537
+ let parseErrors = 0;
52538
+ const rows = [];
52539
+ try {
52540
+ for await (const item of readJsonlLinesWithOffsets(filePath)) {
52541
+ scannedLines += 1;
52542
+ if (!item.line.trim())
52543
+ continue;
52544
+ let event;
52545
+ try {
52546
+ event = JSON.parse(item.line);
52547
+ } catch {
52548
+ parseErrors += 1;
52549
+ continue;
52550
+ }
52551
+ const row = eventToRow({
52552
+ id: encodeOffsetLogEventId(date5, item.offset),
52553
+ date: date5,
52554
+ filePath,
52555
+ lineNumber: item.lineNumber,
52556
+ offset: item.offset,
52557
+ byteLength: item.byteLength,
52558
+ event
52559
+ });
52560
+ if (row)
52561
+ rows.push(row);
52562
+ }
52563
+ const transaction = this.db.transaction((eventRows) => {
52564
+ this.db.prepare("DELETE FROM log_events_fts WHERE event_id IN (SELECT id FROM log_events WHERE source_file = ?)").run(filePath);
52565
+ this.db.prepare("DELETE FROM log_events WHERE source_file = ?").run(filePath);
52566
+ for (const row of eventRows) {
52567
+ this.insertEventStmt.run(row);
52568
+ this.insertFtsStmt.run(row.id, row.search_text);
52569
+ }
52570
+ this.upsertFileStmt.run(filePath, date5, sizeBytes, mtimeMs, Date.now());
52571
+ });
52572
+ transaction(rows);
52573
+ this.dirtyFiles.delete(filePath);
52574
+ return { scannedLines, parseErrors };
52575
+ } finally {
52576
+ this.rebuildingFiles.delete(filePath);
52577
+ }
52578
+ }
52579
+ queryStats(whereSql, params) {
52580
+ const aggregate = this.db.query(`
52581
+ SELECT
52582
+ COUNT(*) AS total,
52583
+ COALESCE(SUM(has_error), 0) AS errorCount,
52584
+ COALESCE(AVG(latency_ms), 0) AS avgLatencyMs
52585
+ FROM log_events e
52586
+ ${whereSql}
52587
+ `).get(...params);
52588
+ const total = Number(aggregate.total) || 0;
52589
+ if (total <= 0)
52590
+ return createEmptyStats();
52591
+ const p95Offset = Math.max(0, Math.ceil(total * 0.95) - 1);
52592
+ const p95Row = this.db.query(`
52593
+ SELECT latency_ms
52594
+ FROM log_events e
52595
+ ${whereSql}
52596
+ ORDER BY latency_ms ASC
52597
+ LIMIT 1 OFFSET ?
52598
+ `).get(...params, p95Offset);
52599
+ const errorCount = Number(aggregate.errorCount) || 0;
52600
+ return {
52601
+ total,
52602
+ errorCount,
52603
+ errorRate: toPercent2(errorCount, total),
52604
+ avgLatencyMs: Math.round(Number(aggregate.avgLatencyMs) || 0),
52605
+ p95LatencyMs: Math.round(p95Row?.latency_ms ?? 0)
52606
+ };
52607
+ }
52608
+ }
52609
+ function initLogIndex(baseDir, config2) {
52610
+ disposeLogIndex();
52611
+ if (config2?.enabled === false)
52612
+ return;
52613
+ try {
52614
+ singleton = new LogIndex(baseDir, config2);
52615
+ } catch (err) {
52616
+ singleton = null;
52617
+ console.error("[log-index] SQLite \u7D22\u5F15\u521D\u59CB\u5316\u5931\u8D25\uFF0C\u5C06\u9000\u56DE JSONL \u626B\u63CF:", err);
52618
+ }
52619
+ }
52620
+ function disposeLogIndex() {
52621
+ if (!singleton)
52622
+ return;
52623
+ try {
52624
+ singleton.dispose();
52625
+ } catch (err) {
52626
+ console.error("[log-index] SQLite \u7D22\u5F15\u5173\u95ED\u5931\u8D25:", err);
52627
+ } finally {
52628
+ singleton = null;
52629
+ }
52630
+ }
52631
+ function getLogIndex(baseDir) {
52632
+ if (!singleton)
52633
+ return null;
52634
+ if (baseDir && singleton.baseDir !== baseDir)
52635
+ return null;
52636
+ return singleton;
52637
+ }
52638
+ function enqueueLogEventForIndex(input) {
52639
+ const index = getLogIndex(input.baseDir);
52640
+ index?.enqueue(input);
52641
+ }
52642
+ async function queryIndexedLogEvents(logConfig, query) {
52643
+ if (!logConfig || logConfig.enabled === false)
52644
+ return createEmptyQueryResult(query);
52645
+ const baseDir = resolveLogBaseDir(logConfig);
52646
+ const index = getLogIndex(baseDir);
52647
+ if (!index)
52648
+ return null;
52649
+ try {
52650
+ const freshness = await index.ensureRangeIndexed(query.fromMs, query.toMs);
52651
+ const result = index.queryEvents(query);
52652
+ return {
52653
+ ...result,
52654
+ meta: {
52655
+ ...result.meta,
52656
+ scannedFiles: freshness.scannedFiles,
52657
+ scannedLines: freshness.scannedLines,
52658
+ parseErrors: freshness.parseErrors
52659
+ }
52660
+ };
52661
+ } catch (err) {
52662
+ if (err instanceof Error && err.message.includes("cursor")) {
52663
+ throw err;
52664
+ }
52665
+ return {
52666
+ ...createEmptyQueryResult(query, {
52667
+ indexUsed: false,
52668
+ indexFresh: false,
52669
+ fallbackReason: err instanceof Error ? err.message : String(err),
52670
+ statsMode: "none"
52671
+ })
52672
+ };
52673
+ }
52674
+ }
52675
+ function getIndexedLogEventDetail(logConfig, id) {
52676
+ if (!logConfig || logConfig.enabled === false)
52677
+ return null;
52678
+ const baseDir = resolveLogBaseDir(logConfig);
52679
+ const indexed = getLogIndex(baseDir)?.getEventRecordByOffsetId(id);
52680
+ if (indexed)
52681
+ return indexed;
52682
+ const parsedId = decodeOffsetLogEventId(id);
52683
+ if (!parsedId)
52684
+ return null;
52685
+ const filePath = join4(baseDir, "events", `${parsedId.date}.jsonl`);
52686
+ if (!existsSync3(filePath))
52687
+ return null;
52688
+ const line2 = readLineAtOffset(filePath, parsedId.offset);
52689
+ if (!line2?.trim())
52690
+ return null;
52691
+ return {
52692
+ event: JSON.parse(line2),
52693
+ location: {
52694
+ id,
52695
+ date: parsedId.date,
52696
+ file: filePath,
52697
+ line: null,
52698
+ offset: parsedId.offset
52699
+ }
52700
+ };
52701
+ }
52702
+
51872
52703
  // src/log-query.ts
51873
52704
  var WINDOW_MS2 = {
51874
52705
  "1h": 60 * 60 * 1000,
@@ -51880,49 +52711,57 @@ var MAX_QUERY_LIMIT = 200;
51880
52711
  var DEFAULT_QUERY_LIMIT = 50;
51881
52712
  var MAX_EXPORT_ROWS = 5000;
51882
52713
  var MAX_Q_LENGTH = 200;
51883
- function encodeBase64Url(value) {
52714
+ function encodeBase64Url2(value) {
51884
52715
  return Buffer.from(value, "utf-8").toString("base64url");
51885
52716
  }
51886
- function decodeBase64Url(value) {
52717
+ function decodeBase64Url2(value) {
51887
52718
  return Buffer.from(value, "base64url").toString("utf-8");
51888
52719
  }
51889
- function encodeCursor(data) {
51890
- return encodeBase64Url(JSON.stringify(data));
52720
+ function encodeCursor2(data) {
52721
+ return encodeBase64Url2(JSON.stringify(data));
51891
52722
  }
51892
- function decodeCursor(raw2) {
51893
- const parsed = JSON.parse(decodeBase64Url(raw2));
52723
+ function decodeCursor2(raw2) {
52724
+ const parsed = JSON.parse(decodeBase64Url2(raw2));
51894
52725
  if (!Number.isInteger(parsed.offset) || parsed.offset < 0) {
51895
52726
  throw new Error("cursor \u975E\u6CD5");
51896
52727
  }
51897
52728
  return parsed;
51898
52729
  }
52730
+ function isLegacyOffsetCursor(raw2) {
52731
+ try {
52732
+ const parsed = JSON.parse(decodeBase64Url2(raw2));
52733
+ return Number.isInteger(parsed.offset) && Number(parsed.offset) >= 0;
52734
+ } catch {
52735
+ return false;
52736
+ }
52737
+ }
51899
52738
  function encodeEventId(date5, line2) {
51900
- return encodeBase64Url(JSON.stringify({ d: date5, l: line2 }));
52739
+ return encodeBase64Url2(JSON.stringify({ d: date5, l: line2 }));
51901
52740
  }
51902
52741
  function decodeEventId(id) {
51903
- const parsed = JSON.parse(decodeBase64Url(id));
52742
+ const parsed = JSON.parse(decodeBase64Url2(id));
51904
52743
  if (typeof parsed.d !== "string" || !Number.isInteger(parsed.l) || Number(parsed.l) <= 0) {
51905
52744
  throw new Error("id \u975E\u6CD5");
51906
52745
  }
51907
52746
  return { date: parsed.d, line: Number(parsed.l) };
51908
52747
  }
51909
- function toPercent2(numerator, denominator) {
52748
+ function toPercent3(numerator, denominator) {
51910
52749
  if (denominator <= 0)
51911
52750
  return 0;
51912
52751
  return Number((numerator / denominator * 100).toFixed(2));
51913
52752
  }
51914
- function toDayStart2(ms) {
52753
+ function toDayStart3(ms) {
51915
52754
  const date5 = new Date(ms);
51916
52755
  return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
51917
52756
  }
51918
- function listDateStrings2(fromMs, toMs) {
52757
+ function listDateStrings3(fromMs, toMs) {
51919
52758
  const result = [];
51920
- for (let day = toDayStart2(fromMs);day <= toDayStart2(toMs); day += 24 * 60 * 60 * 1000) {
52759
+ for (let day = toDayStart3(fromMs);day <= toDayStart3(toMs); day += 24 * 60 * 60 * 1000) {
51921
52760
  result.push(new Date(day).toISOString().slice(0, 10));
51922
52761
  }
51923
52762
  return result;
51924
52763
  }
51925
- function getStatusClass2(event) {
52764
+ function getStatusClass3(event) {
51926
52765
  if (event.error_type)
51927
52766
  return "network_error";
51928
52767
  const status = event.upstream_status ?? 0;
@@ -51934,16 +52773,16 @@ function getStatusClass2(event) {
51934
52773
  return "5xx";
51935
52774
  return "network_error";
51936
52775
  }
51937
- function isErrorEvent2(event) {
52776
+ function isErrorEvent3(event) {
51938
52777
  if (event.error_type)
51939
52778
  return true;
51940
52779
  const status = event.upstream_status ?? 0;
51941
52780
  return status < 200 || status >= 400;
51942
52781
  }
51943
- function getLevel(event) {
51944
- return isErrorEvent2(event) ? "error" : "info";
52782
+ function getLevel2(event) {
52783
+ return isErrorEvent3(event) ? "error" : "info";
51945
52784
  }
51946
- function buildMessage(event) {
52785
+ function buildMessage2(event) {
51947
52786
  if (event.error_message)
51948
52787
  return event.error_message;
51949
52788
  if (event.error_type)
@@ -51968,7 +52807,7 @@ function containsKeyword(event, q) {
51968
52807
  identity.sessionId ?? "",
51969
52808
  event.error_type ?? "",
51970
52809
  event.error_message ?? "",
51971
- buildMessage(event)
52810
+ buildMessage2(event)
51972
52811
  ].join(" ").toLowerCase();
51973
52812
  return haystack.includes(keyword);
51974
52813
  }
@@ -52017,7 +52856,7 @@ function finalizeStats(stats) {
52017
52856
  return {
52018
52857
  total: stats.total,
52019
52858
  errorCount: stats.errorCount,
52020
- errorRate: toPercent2(stats.errorCount, stats.total),
52859
+ errorRate: toPercent3(stats.errorCount, stats.total),
52021
52860
  avgLatencyMs: Math.round(stats.latencySum / stats.total),
52022
52861
  p95LatencyMs: percentileFromCounts(stats.latencyCounts, stats.total, 0.95)
52023
52862
  };
@@ -52037,17 +52876,17 @@ function insertBoundedEvent(items, item, sort, maxKeep) {
52037
52876
  items.pop();
52038
52877
  }
52039
52878
  }
52040
- function clampLimit(limit) {
52879
+ function clampLimit(limit, maxLimit = MAX_QUERY_LIMIT) {
52041
52880
  if (!Number.isFinite(limit))
52042
52881
  return DEFAULT_QUERY_LIMIT;
52043
52882
  const integer2 = Math.floor(limit);
52044
52883
  if (integer2 <= 0)
52045
52884
  return DEFAULT_QUERY_LIMIT;
52046
- return Math.min(MAX_QUERY_LIMIT, integer2);
52885
+ return Math.min(maxLimit, integer2);
52047
52886
  }
52048
- function normalizeQuery(input) {
52887
+ function normalizeQuery(input, maxLimit = MAX_QUERY_LIMIT) {
52049
52888
  const sort = input.sort ?? "time_desc";
52050
- const limit = clampLimit(input.limit);
52889
+ const limit = clampLimit(input.limit, maxLimit);
52051
52890
  const qRaw = (input.q ?? "").trim();
52052
52891
  const q = qRaw.length > MAX_Q_LENGTH ? qRaw.slice(0, MAX_Q_LENGTH) : qRaw;
52053
52892
  return {
@@ -52087,7 +52926,7 @@ function eventToSummary(item) {
52087
52926
  upstreamStatus: event.upstream_status ?? 0,
52088
52927
  statusClass: item.statusClass,
52089
52928
  hasError: item.level === "error",
52090
- message: buildMessage(event),
52929
+ message: buildMessage2(event),
52091
52930
  errorType: event.error_type,
52092
52931
  hasMetadata: identity.hasMetadata,
52093
52932
  userIdRaw: identity.userIdRaw,
@@ -52095,6 +52934,57 @@ function eventToSummary(item) {
52095
52934
  sessionId: identity.sessionId
52096
52935
  };
52097
52936
  }
52937
+ function createLogEventSummaryFromEvent(event, location) {
52938
+ const ts = Date.parse(event.ts_start);
52939
+ return eventToSummary({
52940
+ id: location.id,
52941
+ date: location.date,
52942
+ line: location.line ?? 0,
52943
+ ts: Number.isFinite(ts) ? ts : 0,
52944
+ level: getLevel2(event),
52945
+ statusClass: getStatusClass3(event),
52946
+ event
52947
+ });
52948
+ }
52949
+ function logEventMatchesQuery(event, query) {
52950
+ if (!event.ts_start)
52951
+ return false;
52952
+ const ts = Date.parse(event.ts_start);
52953
+ if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
52954
+ return false;
52955
+ const level = getLevel2(event);
52956
+ const statusClass = getStatusClass3(event);
52957
+ if (query.levels.length > 0 && !query.levels.includes(level))
52958
+ return false;
52959
+ if (query.providers.length > 0 && !query.providers.includes(event.provider))
52960
+ return false;
52961
+ if (query.routeTypes.length > 0 && !query.routeTypes.includes(event.route_type))
52962
+ return false;
52963
+ const eventModel = event.model_out || event.model_in;
52964
+ if (query.models.length > 0 && !query.models.includes(eventModel))
52965
+ return false;
52966
+ if (query.modelIns.length > 0 && !query.modelIns.includes(event.model_in))
52967
+ return false;
52968
+ if (query.modelOuts.length > 0 && !query.modelOuts.includes(event.model_out))
52969
+ return false;
52970
+ const identity = resolveLogSessionIdentity(event.request_body);
52971
+ if (query.users.length > 0) {
52972
+ const matchedByRaw = identity.userIdRaw ? query.users.includes(identity.userIdRaw) : false;
52973
+ const matchedByUserKey = identity.userKey ? query.users.includes(identity.userKey) : false;
52974
+ if (!matchedByRaw && !matchedByUserKey)
52975
+ return false;
52976
+ }
52977
+ if (query.sessions.length > 0) {
52978
+ if (!identity.sessionId || !query.sessions.includes(identity.sessionId))
52979
+ return false;
52980
+ }
52981
+ if (query.statusClasses.length > 0 && !query.statusClasses.includes(statusClass))
52982
+ return false;
52983
+ const hasError = level === "error";
52984
+ if (query.hasError !== null && query.hasError !== hasError)
52985
+ return false;
52986
+ return containsKeyword(event, query.q);
52987
+ }
52098
52988
  function detectBodyPolicy(event) {
52099
52989
  const hasRequestBody = event.request_body !== undefined;
52100
52990
  const hasResponseBody = event.response_body !== undefined;
@@ -52148,14 +53038,14 @@ function readStreamContent(baseDir, streamFile) {
52148
53038
  if (!looksLikeStreamFile) {
52149
53039
  return { content: null, warning: "stream_file \u4E0D\u662F .sse.raw \u6587\u4EF6\uFF0C\u5DF2\u8DF3\u8FC7\u8BFB\u53D6\u3002" };
52150
53040
  }
52151
- if (existsSync3(resolvedFromFile)) {
53041
+ if (existsSync4(resolvedFromFile)) {
52152
53042
  return { content: readFileSync3(resolvedFromFile, "utf-8"), warning: null };
52153
53043
  }
52154
53044
  const fallbackPath = resolve4(resolvedBase, streamFile);
52155
53045
  if (!fallbackPath.startsWith(`${resolvedBase}/`) && fallbackPath !== resolvedBase) {
52156
53046
  return { content: null, warning: "stream_file \u8DEF\u5F84\u975E\u6CD5\uFF0C\u5DF2\u62D2\u7EDD\u8BFB\u53D6\u3002" };
52157
53047
  }
52158
- if (!existsSync3(fallbackPath)) {
53048
+ if (!existsSync4(fallbackPath)) {
52159
53049
  return { content: null, warning: "stream_file \u4E0D\u5B58\u5728\uFF0C\u53EF\u80FD\u5DF2\u88AB\u6E05\u7406\u3002" };
52160
53050
  }
52161
53051
  return { content: readFileSync3(fallbackPath, "utf-8"), warning: null };
@@ -52168,8 +53058,8 @@ function readStreamContent(baseDir, streamFile) {
52168
53058
  }
52169
53059
  async function buildLogEventDetail(id, parsed, location, context2) {
52170
53060
  const event = parsed;
52171
- const level = getLevel(event);
52172
- const statusClass = getStatusClass2(event);
53061
+ const level = getLevel2(event);
53062
+ const statusClass = getStatusClass3(event);
52173
53063
  const bodyPolicy = detectBodyPolicy(event);
52174
53064
  const requestBodyAvailable = event.request_body !== undefined;
52175
53065
  const responseBodyAvailable = event.response_body !== undefined;
@@ -52241,8 +53131,8 @@ async function buildLogEventDetail(id, parsed, location, context2) {
52241
53131
  };
52242
53132
  }
52243
53133
  async function scanEvents(baseDir, query) {
52244
- const eventsDir = join4(baseDir, "events");
52245
- if (!existsSync3(eventsDir)) {
53134
+ const eventsDir = join5(baseDir, "events");
53135
+ if (!existsSync4(eventsDir)) {
52246
53136
  return {
52247
53137
  items: [],
52248
53138
  stats: {
@@ -52260,8 +53150,8 @@ async function scanEvents(baseDir, query) {
52260
53150
  }
52261
53151
  };
52262
53152
  }
52263
- const dates = listDateStrings2(query.fromMs, query.toMs);
52264
- const offset = query.cursor ? decodeCursor(query.cursor).offset : 0;
53153
+ const dates = listDateStrings3(query.fromMs, query.toMs);
53154
+ const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
52265
53155
  const maxKeep = offset + query.limit;
52266
53156
  const items = [];
52267
53157
  const runningStats = createRunningStats();
@@ -52274,11 +53164,11 @@ async function scanEvents(baseDir, query) {
52274
53164
  truncated = true;
52275
53165
  break;
52276
53166
  }
52277
- const filePath = join4(eventsDir, `${date5}.jsonl`);
52278
- if (!existsSync3(filePath))
53167
+ const filePath = join5(eventsDir, `${date5}.jsonl`);
53168
+ if (!existsSync4(filePath))
52279
53169
  continue;
52280
53170
  scannedFiles += 1;
52281
- const stream = createReadStream2(filePath, { encoding: "utf-8" });
53171
+ const stream = createReadStream3(filePath, { encoding: "utf-8" });
52282
53172
  const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
52283
53173
  let lineNumber = 0;
52284
53174
  for await (const line2 of rl) {
@@ -52304,8 +53194,8 @@ async function scanEvents(baseDir, query) {
52304
53194
  const ts = Date.parse(event.ts_start);
52305
53195
  if (!Number.isFinite(ts) || ts < query.fromMs || ts > query.toMs)
52306
53196
  continue;
52307
- const level = getLevel(event);
52308
- const statusClass = getStatusClass2(event);
53197
+ const level = getLevel2(event);
53198
+ const statusClass = getStatusClass3(event);
52309
53199
  if (query.levels.length > 0 && !query.levels.includes(level))
52310
53200
  continue;
52311
53201
  if (query.providers.length > 0 && !query.providers.includes(event.provider))
@@ -52395,6 +53285,9 @@ function validateSort(value) {
52395
53285
  return value === "time_desc" || value === "time_asc";
52396
53286
  }
52397
53287
  async function queryLogEvents(context2, input) {
53288
+ return queryLogEventsInternal(context2, input, MAX_QUERY_LIMIT);
53289
+ }
53290
+ async function queryLogEventsInternal(context2, input, maxLimit) {
52398
53291
  const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
52399
53292
  if (!logEnabled) {
52400
53293
  return {
@@ -52417,30 +53310,51 @@ async function queryLogEvents(context2, input) {
52417
53310
  };
52418
53311
  }
52419
53312
  const baseDir = resolveLogBaseDir(context2.logConfig);
52420
- const query = normalizeQuery(input);
52421
- const offset = query.cursor ? decodeCursor(query.cursor).offset : 0;
53313
+ const query = normalizeQuery(input, maxLimit);
53314
+ const indexed = await queryIndexedLogEvents(context2.logConfig, query);
53315
+ if (indexed?.meta.indexUsed) {
53316
+ return indexed;
53317
+ }
53318
+ if (query.cursor && !isLegacyOffsetCursor(query.cursor)) {
53319
+ 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}` : ""}`);
53320
+ }
53321
+ const offset = query.cursor ? decodeCursor2(query.cursor).offset : 0;
52422
53322
  const scanned = await scanEvents(baseDir, query);
52423
53323
  const pageItems = scanned.items.slice(offset, offset + query.limit);
52424
53324
  const hasMore = scanned.stats.total > offset + query.limit;
52425
53325
  const nextOffset = offset + pageItems.length;
52426
53326
  return {
52427
53327
  items: pageItems.map(eventToSummary),
52428
- nextCursor: hasMore ? encodeCursor({ offset: nextOffset }) : null,
53328
+ nextCursor: hasMore ? encodeCursor2({ offset: nextOffset }) : null,
52429
53329
  hasMore,
52430
53330
  stats: scanned.stats,
52431
- meta: scanned.meta
53331
+ meta: {
53332
+ ...scanned.meta,
53333
+ indexUsed: false,
53334
+ indexFresh: false,
53335
+ fallbackReason: indexed?.meta.fallbackReason,
53336
+ statsMode: "exact"
53337
+ }
52432
53338
  };
52433
53339
  }
52434
53340
  async function getLogEventDetailById(context2, id) {
52435
53341
  const logEnabled = !!context2.logConfig && context2.logConfig.enabled !== false;
52436
53342
  if (!logEnabled)
52437
53343
  return null;
53344
+ const indexed = getIndexedLogEventDetail(context2.logConfig, id);
53345
+ if (indexed) {
53346
+ return buildLogEventDetail(id, indexed.event, {
53347
+ date: indexed.location.date,
53348
+ line: indexed.location.line ?? 0,
53349
+ file: indexed.location.file
53350
+ }, context2);
53351
+ }
52438
53352
  const { date: date5, line: line2 } = decodeEventId(id);
52439
53353
  const baseDir = resolveLogBaseDir(context2.logConfig);
52440
- const filePath = join4(baseDir, "events", `${date5}.jsonl`);
52441
- if (!existsSync3(filePath))
53354
+ const filePath = join5(baseDir, "events", `${date5}.jsonl`);
53355
+ if (!existsSync4(filePath))
52442
53356
  return null;
52443
- const stream = createReadStream2(filePath, { encoding: "utf-8" });
53357
+ const stream = createReadStream3(filePath, { encoding: "utf-8" });
52444
53358
  const rl = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
52445
53359
  let lineNumber = 0;
52446
53360
  for await (const lineText of rl) {
@@ -52550,11 +53464,11 @@ function createJsonExportStream(data) {
52550
53464
  });
52551
53465
  }
52552
53466
  async function exportLogEvents(context2, input, format) {
52553
- const data = await queryLogEvents(context2, {
53467
+ const data = await queryLogEventsInternal(context2, {
52554
53468
  ...input,
52555
53469
  cursor: null,
52556
53470
  limit: MAX_EXPORT_ROWS
52557
- });
53471
+ }, MAX_EXPORT_ROWS);
52558
53472
  const now2 = new Date().toISOString().replace(/[:.]/g, "-");
52559
53473
  if (format === "csv") {
52560
53474
  return {
@@ -52593,18 +53507,18 @@ function parseBooleanFlag(value) {
52593
53507
  }
52594
53508
 
52595
53509
  // src/log-sessions.ts
52596
- import { createReadStream as createReadStream3, existsSync as existsSync4 } from "fs";
52597
- import { join as join5 } from "path";
53510
+ import { createReadStream as createReadStream4, existsSync as existsSync5 } from "fs";
53511
+ import { join as join6 } from "path";
52598
53512
  import { createInterface as createInterface3 } from "readline";
52599
53513
  var MAX_LINES_SCANNED3 = 250000;
52600
53514
  var MAX_Q_LENGTH2 = 200;
52601
- function toDayStart3(ms) {
53515
+ function toDayStart4(ms) {
52602
53516
  const date5 = new Date(ms);
52603
53517
  return Date.UTC(date5.getUTCFullYear(), date5.getUTCMonth(), date5.getUTCDate());
52604
53518
  }
52605
- function listDateStrings3(fromMs, toMs) {
53519
+ function listDateStrings4(fromMs, toMs) {
52606
53520
  const result = [];
52607
- for (let day = toDayStart3(fromMs);day <= toDayStart3(toMs); day += 24 * 60 * 60 * 1000) {
53521
+ for (let day = toDayStart4(fromMs);day <= toDayStart4(toMs); day += 24 * 60 * 60 * 1000) {
52608
53522
  result.push(new Date(day).toISOString().slice(0, 10));
52609
53523
  }
52610
53524
  return result;
@@ -52715,8 +53629,8 @@ async function queryLogSessions(context2, input) {
52715
53629
  return createEmptyResult(normalized.fromMs, normalized.toMs);
52716
53630
  }
52717
53631
  const baseDir = resolveLogBaseDir(context2.logConfig);
52718
- const eventsDir = join5(baseDir, "events");
52719
- if (!existsSync4(eventsDir)) {
53632
+ const eventsDir = join6(baseDir, "events");
53633
+ if (!existsSync5(eventsDir)) {
52720
53634
  return createEmptyResult(normalized.fromMs, normalized.toMs);
52721
53635
  }
52722
53636
  const usersMap = new Map;
@@ -52728,17 +53642,17 @@ async function queryLogSessions(context2, input) {
52728
53642
  let scannedLines = 0;
52729
53643
  let parseErrors = 0;
52730
53644
  let truncated = false;
52731
- const dateStrings = listDateStrings3(normalized.fromMs, normalized.toMs);
53645
+ const dateStrings = listDateStrings4(normalized.fromMs, normalized.toMs);
52732
53646
  for (const date5 of dateStrings) {
52733
53647
  if (scannedLines >= MAX_LINES_SCANNED3) {
52734
53648
  truncated = true;
52735
53649
  break;
52736
53650
  }
52737
- const filePath = join5(eventsDir, `${date5}.jsonl`);
52738
- if (!existsSync4(filePath))
53651
+ const filePath = join6(eventsDir, `${date5}.jsonl`);
53652
+ if (!existsSync5(filePath))
52739
53653
  continue;
52740
53654
  scannedFiles += 1;
52741
- const stream = createReadStream3(filePath, { encoding: "utf-8" });
53655
+ const stream = createReadStream4(filePath, { encoding: "utf-8" });
52742
53656
  const rl = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
52743
53657
  for await (const line2 of rl) {
52744
53658
  if (scannedLines >= MAX_LINES_SCANNED3) {
@@ -52843,8 +53757,8 @@ async function queryLogSessions(context2, input) {
52843
53757
  }
52844
53758
 
52845
53759
  // src/log-storage.ts
52846
- import { existsSync as existsSync5, promises as fsPromises } from "fs";
52847
- import { join as join6 } from "path";
53760
+ import { existsSync as existsSync6, promises as fsPromises } from "fs";
53761
+ import { join as join7 } from "path";
52848
53762
  var cachedStorage = null;
52849
53763
  var calculationPromise = null;
52850
53764
  var lastCalculationTime = 0;
@@ -52852,7 +53766,7 @@ var CACHE_TTL_MS2 = 60 * 60 * 1000;
52852
53766
  var CALCULATION_INTERVAL_MS = 60 * 60 * 1000;
52853
53767
  var MIN_CALCULATION_INTERVAL_MS = 5 * 60 * 1000;
52854
53768
  async function calculateDirSize(dirPath) {
52855
- if (!existsSync5(dirPath)) {
53769
+ if (!existsSync6(dirPath)) {
52856
53770
  return { bytes: 0, fileCount: 0 };
52857
53771
  }
52858
53772
  let bytes = 0;
@@ -52861,7 +53775,7 @@ async function calculateDirSize(dirPath) {
52861
53775
  try {
52862
53776
  const entries = await fsPromises.readdir(currentPath, { withFileTypes: true });
52863
53777
  for (const entry of entries) {
52864
- const fullPath = join6(currentPath, entry.name);
53778
+ const fullPath = join7(currentPath, entry.name);
52865
53779
  if (entry.isDirectory()) {
52866
53780
  await walk(fullPath);
52867
53781
  } else if (entry.isFile()) {
@@ -52884,6 +53798,7 @@ async function doCalculateStorage(logConfig) {
52884
53798
  totalBytes: 0,
52885
53799
  eventsBytes: 0,
52886
53800
  streamsBytes: 0,
53801
+ indexBytes: 0,
52887
53802
  fileCount: 0,
52888
53803
  lastUpdatedAt: new Date().toISOString(),
52889
53804
  isCalculating: false
@@ -52891,18 +53806,40 @@ async function doCalculateStorage(logConfig) {
52891
53806
  }
52892
53807
  const baseDir = resolveLogBaseDir(logConfig);
52893
53808
  const [eventsResult, streamsResult] = await Promise.all([
52894
- calculateDirSize(join6(baseDir, "events")),
52895
- calculateDirSize(join6(baseDir, "streams"))
53809
+ calculateDirSize(join7(baseDir, "events")),
53810
+ calculateDirSize(join7(baseDir, "streams"))
52896
53811
  ]);
53812
+ const indexResult = await calculateIndexSize(baseDir);
52897
53813
  return {
52898
- totalBytes: eventsResult.bytes + streamsResult.bytes,
53814
+ totalBytes: eventsResult.bytes + streamsResult.bytes + indexResult.bytes,
52899
53815
  eventsBytes: eventsResult.bytes,
52900
53816
  streamsBytes: streamsResult.bytes,
52901
- fileCount: eventsResult.fileCount + streamsResult.fileCount,
53817
+ indexBytes: indexResult.bytes,
53818
+ fileCount: eventsResult.fileCount + streamsResult.fileCount + indexResult.fileCount,
52902
53819
  lastUpdatedAt: new Date().toISOString(),
52903
53820
  isCalculating: false
52904
53821
  };
52905
53822
  }
53823
+ async function calculateIndexSize(baseDir) {
53824
+ if (!existsSync6(baseDir)) {
53825
+ return { bytes: 0, fileCount: 0 };
53826
+ }
53827
+ let bytes = 0;
53828
+ let fileCount = 0;
53829
+ try {
53830
+ const entries = await fsPromises.readdir(baseDir, { withFileTypes: true });
53831
+ for (const entry of entries) {
53832
+ if (!entry.isFile() || !entry.name.startsWith("logs-index.sqlite"))
53833
+ continue;
53834
+ const stats = await fsPromises.stat(join7(baseDir, entry.name));
53835
+ bytes += stats.size;
53836
+ fileCount += 1;
53837
+ }
53838
+ } catch {
53839
+ return { bytes, fileCount };
53840
+ }
53841
+ return { bytes, fileCount };
53842
+ }
52906
53843
  async function getLogStorageInfo(options) {
52907
53844
  const { logConfig, forceRefresh = false, nowMs = Date.now() } = options;
52908
53845
  if (!forceRefresh && cachedStorage && cachedStorage.expiresAt > nowMs) {
@@ -52945,10 +53882,25 @@ function startLogStorageBackgroundTask(logConfig) {
52945
53882
  };
52946
53883
  }
52947
53884
 
52948
- // src/logger.ts
52949
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
52950
- import { join as join7 } from "path";
53885
+ // src/log-tail.ts
53886
+ var subscribers = new Set;
53887
+ function publishLogEvent(event) {
53888
+ for (const subscriber of subscribers) {
53889
+ try {
53890
+ subscriber(event);
53891
+ } catch {}
53892
+ }
53893
+ }
53894
+ function subscribeLogEvents(subscriber) {
53895
+ subscribers.add(subscriber);
53896
+ return () => {
53897
+ subscribers.delete(subscriber);
53898
+ };
53899
+ }
52951
53900
 
53901
+ // src/logger.ts
53902
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
53903
+ import { join as join8 } from "path";
52952
53904
  class Logger {
52953
53905
  baseDir;
52954
53906
  eventsDir;
@@ -52963,8 +53915,8 @@ class Logger {
52963
53915
  this._bodyPolicy = config2.bodyPolicy ?? "off";
52964
53916
  this._streamsEnabled = config2.streams?.enabled !== false;
52965
53917
  this.maxStreamBytes = config2.streams?.maxBytesPerRequest ?? 10 * 1024 * 1024;
52966
- this.eventsDir = join7(baseDir, "events");
52967
- this.streamsDir = join7(baseDir, "streams");
53918
+ this.eventsDir = join8(baseDir, "events");
53919
+ this.streamsDir = join8(baseDir, "streams");
52968
53920
  if (this._enabled)
52969
53921
  this.ensureDirs();
52970
53922
  }
@@ -52976,14 +53928,14 @@ class Logger {
52976
53928
  }
52977
53929
  ensureDirs() {
52978
53930
  for (const dir of [this.baseDir, this.eventsDir, this.streamsDir]) {
52979
- if (!existsSync6(dir))
52980
- mkdirSync2(dir, { recursive: true });
53931
+ if (!existsSync7(dir))
53932
+ mkdirSync3(dir, { recursive: true });
52981
53933
  }
52982
53934
  }
52983
53935
  ensureStreamDateDir(dateStr) {
52984
- const dir = join7(this.streamsDir, dateStr);
52985
- if (!existsSync6(dir))
52986
- mkdirSync2(dir, { recursive: true });
53936
+ const dir = join8(this.streamsDir, dateStr);
53937
+ if (!existsSync7(dir))
53938
+ mkdirSync3(dir, { recursive: true });
52987
53939
  return dir;
52988
53940
  }
52989
53941
  writeEvent(event) {
@@ -52992,9 +53944,21 @@ class Logger {
52992
53944
  try {
52993
53945
  this.ensureDirs();
52994
53946
  const dateStr = event.ts_start.slice(0, 10);
52995
- const filePath = join7(this.eventsDir, `${dateStr}.jsonl`);
52996
- appendFileSync(filePath, `${JSON.stringify(event)}
52997
- `);
53947
+ const filePath = join8(this.eventsDir, `${dateStr}.jsonl`);
53948
+ const offset = existsSync7(filePath) ? statSync2(filePath).size : 0;
53949
+ const line2 = `${JSON.stringify(event)}
53950
+ `;
53951
+ appendFileSync(filePath, line2);
53952
+ const id = encodeOffsetLogEventId(dateStr, offset);
53953
+ enqueueLogEventForIndex({
53954
+ baseDir: this.baseDir,
53955
+ filePath,
53956
+ date: dateStr,
53957
+ offset,
53958
+ byteLength: Buffer.byteLength(line2),
53959
+ event
53960
+ });
53961
+ publishLogEvent({ id, date: dateStr, filePath, offset, event });
52998
53962
  } catch (err) {
52999
53963
  console.error("[logger] \u4E8B\u4EF6\u65E5\u5FD7\u5199\u5165\u5931\u8D25:", err);
53000
53964
  }
@@ -53004,7 +53968,7 @@ class Logger {
53004
53968
  return null;
53005
53969
  try {
53006
53970
  const dir = this.ensureStreamDateDir(dateStr);
53007
- const filePath = join7(dir, `${requestId}.sse.raw`);
53971
+ const filePath = join8(dir, `${requestId}.sse.raw`);
53008
53972
  const toWrite = content.length > this.maxStreamBytes ? `${content.slice(0, this.maxStreamBytes)}
53009
53973
  [TRUNCATED]` : content;
53010
53974
  writeFileSync3(filePath, toWrite);
@@ -53018,6 +53982,7 @@ class Logger {
53018
53982
  var instance = null;
53019
53983
  function initLogger(baseDir, config2) {
53020
53984
  instance = new Logger(baseDir, config2);
53985
+ initLogIndex(baseDir, config2);
53021
53986
  if (instance.enabled) {
53022
53987
  console.log(`[logger] \u65E5\u5FD7\u7CFB\u7EDF\u5DF2\u521D\u59CB\u5316: ${baseDir}`);
53023
53988
  }
@@ -53027,6 +53992,7 @@ function getLogger() {
53027
53992
  }
53028
53993
  function resetLogger() {
53029
53994
  instance = null;
53995
+ disposeLogIndex();
53030
53996
  }
53031
53997
  function collectHeaders(headers) {
53032
53998
  const result = {};
@@ -54232,7 +55198,7 @@ var openAPISpec = {
54232
55198
  // src/plugin-loader.ts
54233
55199
  import { mkdtemp, rm, writeFile } from "fs/promises";
54234
55200
  import { tmpdir } from "os";
54235
- import { join as join8, resolve as resolve5 } from "path";
55201
+ import { join as join9, resolve as resolve5 } from "path";
54236
55202
  function isLocalPath(pkg) {
54237
55203
  return pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pkg);
54238
55204
  }
@@ -54255,7 +55221,7 @@ var remoteTmpDir = null;
54255
55221
  var remoteTmpFiles = [];
54256
55222
  async function ensureRemoteTmpDir() {
54257
55223
  if (!remoteTmpDir) {
54258
- remoteTmpDir = await mkdtemp(join8(tmpdir(), "local-router-plugins-"));
55224
+ remoteTmpDir = await mkdtemp(join9(tmpdir(), "local-router-plugins-"));
54259
55225
  }
54260
55226
  return remoteTmpDir;
54261
55227
  }
@@ -54268,7 +55234,7 @@ async function fetchRemotePlugin(url2) {
54268
55234
  const ext = inferExtension(url2, response.headers.get("content-type"));
54269
55235
  const dir = await ensureRemoteTmpDir();
54270
55236
  const fileName = `plugin_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`;
54271
- const filePath = join8(dir, fileName);
55237
+ const filePath = join9(dir, fileName);
54272
55238
  await writeFile(filePath, content, "utf-8");
54273
55239
  remoteTmpFiles.push(filePath);
54274
55240
  return filePath;
@@ -54407,7 +55373,7 @@ class PluginManager {
54407
55373
  // src/proxy.ts
54408
55374
  import { appendFile, readFile, unlink } from "fs/promises";
54409
55375
  import { tmpdir as tmpdir2 } from "os";
54410
- import { join as join9 } from "path";
55376
+ import { join as join10 } from "path";
54411
55377
 
54412
55378
  // src/plugin-engine.ts
54413
55379
  async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
@@ -54599,7 +55565,7 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
54599
55565
  };
54600
55566
  }
54601
55567
  function createTempStreamCapturePath(requestId) {
54602
- return join9(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
55568
+ return join10(tmpdir2(), `local-router-stream-${requestId}-${Date.now()}.sse.raw`);
54603
55569
  }
54604
55570
  async function appendTempStreamCapture(filePath, chunk) {
54605
55571
  await appendFile(filePath, chunk);
@@ -55425,7 +56391,6 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
55425
56391
  }
55426
56392
  });
55427
56393
  api2.get("/logs/tail", async (c2) => {
55428
- const config2 = store.get();
55429
56394
  const target = c2.req.raw;
55430
56395
  const windowRaw = c2.req.query("window") ?? "1h";
55431
56396
  if (!isLogQueryWindow(windowRaw)) {
@@ -55453,11 +56418,15 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
55453
56418
  }
55454
56419
  const encoder = new TextEncoder;
55455
56420
  let closed = false;
55456
- let lastSeenTs = Date.now() - 60 * 1000;
56421
+ const maxPendingItems = 500;
55457
56422
  let closeStream = null;
55458
56423
  const stream = new ReadableStream({
55459
56424
  start(controller) {
55460
- let timer = null;
56425
+ let heartbeatTimer = null;
56426
+ let unsubscribe = null;
56427
+ let flushQueued = false;
56428
+ let droppedItems = 0;
56429
+ const pending = [];
55461
56430
  const push2 = (event, payload) => {
55462
56431
  if (closed)
55463
56432
  return;
@@ -55471,60 +56440,106 @@ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
55471
56440
  if (closed)
55472
56441
  return;
55473
56442
  closed = true;
55474
- if (timer) {
55475
- clearInterval(timer);
55476
- timer = null;
56443
+ if (heartbeatTimer) {
56444
+ clearInterval(heartbeatTimer);
56445
+ heartbeatTimer = null;
55477
56446
  }
56447
+ unsubscribe?.();
56448
+ unsubscribe = null;
55478
56449
  target.signal.removeEventListener("abort", close);
55479
56450
  try {
55480
56451
  controller.close();
55481
56452
  } catch {}
55482
56453
  };
55483
56454
  closeStream = close;
55484
- push2("ready", { ok: true, now: new Date().toISOString() });
55485
- timer = setInterval(async () => {
56455
+ const buildTailQuery = () => ({
56456
+ ...resolveLogQueryRange({
56457
+ window: windowRaw,
56458
+ from: c2.req.query("from"),
56459
+ to: c2.req.query("to")
56460
+ }),
56461
+ levels,
56462
+ providers: parseCommaSeparated(c2.req.query("provider")),
56463
+ routeTypes: parseCommaSeparated(c2.req.query("routeType")),
56464
+ models: parseCommaSeparated(c2.req.query("model")),
56465
+ modelIns: parseCommaSeparated(c2.req.query("modelIn")),
56466
+ modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
56467
+ users: parseCommaSeparated(c2.req.query("user")),
56468
+ sessions: parseCommaSeparated(c2.req.query("session")),
56469
+ statusClasses,
56470
+ hasError,
56471
+ q: c2.req.query("q") ?? "",
56472
+ sort: sortRaw,
56473
+ limit: 100,
56474
+ cursor: null
56475
+ });
56476
+ const flush = () => {
56477
+ flushQueued = false;
56478
+ if (closed || pending.length === 0)
56479
+ return;
56480
+ const items = pending.splice(0, pending.length);
56481
+ 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;
56482
+ droppedItems = 0;
56483
+ push2("events", {
56484
+ items,
56485
+ nextCursor: null,
56486
+ hasMore: false,
56487
+ stats: {
56488
+ total: 0,
56489
+ errorCount: 0,
56490
+ errorRate: 0,
56491
+ avgLatencyMs: 0,
56492
+ p95LatencyMs: 0
56493
+ },
56494
+ meta: {
56495
+ scannedFiles: 0,
56496
+ scannedLines: 0,
56497
+ parseErrors: 0,
56498
+ truncated: false,
56499
+ indexUsed: true,
56500
+ indexFresh: true,
56501
+ usesFts: false,
56502
+ queryMs: 0,
56503
+ rowsReturned: items.length,
56504
+ fallbackReason: overflowMessage,
56505
+ statsMode: "none"
56506
+ }
56507
+ });
56508
+ };
56509
+ const queueFlush = () => {
56510
+ if (flushQueued)
56511
+ return;
56512
+ flushQueued = true;
56513
+ queueMicrotask(flush);
56514
+ };
56515
+ unsubscribe = subscribeLogEvents((published) => {
55486
56516
  if (closed)
55487
56517
  return;
55488
56518
  try {
55489
- const toMs = Date.now();
55490
- const data = await queryLogEvents({ logConfig: config2.log }, {
55491
- fromMs: Math.max(lastSeenTs, toMs - 60 * 60 * 1000),
55492
- toMs,
55493
- levels,
55494
- providers: parseCommaSeparated(c2.req.query("provider")),
55495
- routeTypes: parseCommaSeparated(c2.req.query("routeType")),
55496
- models: parseCommaSeparated(c2.req.query("model")),
55497
- modelIns: parseCommaSeparated(c2.req.query("modelIn")),
55498
- modelOuts: parseCommaSeparated(c2.req.query("modelOut")),
55499
- users: parseCommaSeparated(c2.req.query("user")),
55500
- sessions: parseCommaSeparated(c2.req.query("session")),
55501
- statusClasses,
55502
- hasError,
55503
- q: c2.req.query("q") ?? "",
55504
- sort: sortRaw,
55505
- limit: 100
55506
- });
55507
- if (closed)
56519
+ const query = buildTailQuery();
56520
+ if (!logEventMatchesQuery(published.event, query))
55508
56521
  return;
55509
- if (data.items.length > 0) {
55510
- const maxTs = Math.max(...data.items.map((item) => Date.parse(item.ts)).filter(Number.isFinite));
55511
- if (Number.isFinite(maxTs)) {
55512
- lastSeenTs = Math.max(lastSeenTs, maxTs + 1);
55513
- }
55514
- push2("events", {
55515
- items: data.items,
55516
- stats: data.stats,
55517
- meta: data.meta
55518
- });
55519
- } else {
55520
- push2("heartbeat", { ts: new Date().toISOString() });
56522
+ if (pending.length >= maxPendingItems) {
56523
+ pending.shift();
56524
+ droppedItems += 1;
55521
56525
  }
56526
+ pending.push(createLogEventSummaryFromEvent(published.event, {
56527
+ id: published.id,
56528
+ date: published.date,
56529
+ line: null
56530
+ }));
56531
+ queueFlush();
55522
56532
  } catch (err) {
55523
- if (closed)
55524
- return;
55525
56533
  push2("error", { error: err instanceof Error ? err.message : String(err) });
55526
56534
  }
55527
- }, 3000);
56535
+ });
56536
+ push2("ready", { ok: true, now: new Date().toISOString() });
56537
+ heartbeatTimer = setInterval(() => {
56538
+ if (closed)
56539
+ return;
56540
+ push2("heartbeat", { ts: new Date().toISOString() });
56541
+ }, 15000);
56542
+ heartbeatTimer.unref?.();
55528
56543
  target.signal.addEventListener("abort", close);
55529
56544
  },
55530
56545
  cancel() {