llm-simple-router 1.0.1 → 1.0.3-beta.abb7afa
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/admin/logs.js +6 -17
- package/dist/admin/retry-rules.js +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/logs.d.ts +3 -0
- package/dist/db/logs.js +24 -15
- package/dist/db/migrations/053_add_thinking_level.sql +6 -0
- package/dist/index.js +5 -0
- package/dist/middleware/admin-auth.js +29 -0
- package/dist/proxy/orchestration/orchestrator.d.ts +1 -3
- package/dist/proxy/orchestration/orchestrator.js +3 -20
- package/dist/storage/log-file-compressor.d.ts +1 -1
- package/dist/storage/log-file-compressor.js +55 -13
- package/dist/storage/log-file-writer.d.ts +18 -8
- package/dist/storage/log-file-writer.js +85 -67
- package/frontend-dist/assets/{AuthLayout-C0FD0tHT.js → AuthLayout-Bl_uQ5sH.js} +1 -1
- package/frontend-dist/assets/{Card-hrAXSQVC.js → Card-C0Z5Y2qq.js} +1 -1
- package/frontend-dist/assets/{CardContent-B9VidSM0.js → CardContent-CxIItFGE.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BcQuYJjU.js → CardTitle-Cy-zryBA.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-x8cETmD7.js → CascadingModelSelect-CUy0OgBb.js} +1 -1
- package/frontend-dist/assets/{Checkbox-BIvCbk6g.js → Checkbox-F42SBatn.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-DhJE6UL1.js → CollapsibleContent-DzikstOf.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-CWz_6ZOV.js → CollapsibleTrigger-DiKtULqD.js} +1 -1
- package/frontend-dist/assets/{ConcurrencyControl-DZl5Qi1X.js → ConcurrencyControl-Bg3g8z0S.js} +1 -1
- package/frontend-dist/assets/{Dashboard-efjZJy9D.js → Dashboard-CgWGpp2o.js} +1 -1
- package/frontend-dist/assets/{Input-DFxxh-b-.js → Input-DGPG6o6m.js} +1 -1
- package/frontend-dist/assets/{Label-Co-hKGsA.js → Label-JgI4zID2.js} +1 -1
- package/frontend-dist/assets/{Login-CSS9iTNj.js → Login-BrAwzWzI.js} +1 -1
- package/frontend-dist/assets/{Logs-OvvvqI6n.js → Logs-BSviBoC0.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-fv3TNca8.js → ModelMappings-Do57dsyt.js} +1 -1
- package/frontend-dist/assets/{Monitor-WAw9DC1J.js → Monitor-DpbLyL82.js} +1 -1
- package/frontend-dist/assets/{Providers-BDCpes8K.js → Providers-r9D2q2Pm.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-CTPHjHGK.js → ProxyEnhancement-DWvLyJT8.js} +1 -1
- package/frontend-dist/assets/{QuickSetup-IWIkOP17.js → QuickSetup-CDPCiqrt.js} +1 -1
- package/frontend-dist/assets/{RetryRules-I0PmXv-z.js → RetryRules-Ckrw8ony.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-CcW5brO1.js → RouterKeys-BN3DFl5X.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-BndfXQ2S.js → RovingFocusItem-nL4vc6zh.js} +1 -1
- package/frontend-dist/assets/{Schedules-AEC8oDU2.js → Schedules-BmIqP8ZO.js} +1 -1
- package/frontend-dist/assets/{Settings-QwLwQtfv.js → Settings-56IIZnr8.js} +1 -1
- package/frontend-dist/assets/{Setup-DjaBSXTr.js → Setup-B5etPO21.js} +1 -1
- package/frontend-dist/assets/{Skeleton-BIbIKLTD.js → Skeleton-BGRmPpnQ.js} +1 -1
- package/frontend-dist/assets/{Switch-B1tYE8zv.js → Switch-CwIsNTU0.js} +1 -1
- package/frontend-dist/assets/{TableHeader-BcbJu9nM.js → TableHeader-fM5RGwOA.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-C8NuosLp.js → TabsTrigger-5It8JRGq.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-B-iCowS2.js → TooltipTrigger-DdRPDJPH.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-C4m8rXJT.js → UnifiedRequestDialog-uf8iX6Se.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-Dyx0QfcC.js → VisuallyHiddenInput-SqkUU8kV.js} +1 -1
- package/frontend-dist/assets/arrow-down-DjWf1Vxr.js +1 -0
- package/frontend-dist/assets/{badge-P5nMGhMG.js → badge-CW5RAp35.js} +1 -1
- package/frontend-dist/assets/{button-zW1OChzo.js → button-B63vCS-z.js} +2 -2
- package/frontend-dist/assets/chevron-right-BnreRakh.js +1 -0
- package/frontend-dist/assets/dialog-D5xUmszp.js +1 -0
- package/frontend-dist/assets/{image-Dow5MbB2.js → image-CeVYZvG6.js} +1 -1
- package/frontend-dist/assets/index-BDhTGcco.css +1 -0
- package/frontend-dist/assets/{index-RneerSeS.js → index-efKx2YB4.js} +2 -2
- package/frontend-dist/assets/{model-patches-DYxmmVOo.js → model-patches-CaRhJpJe.js} +1 -1
- package/frontend-dist/assets/{pencil-CFbaC9kg.js → pencil-M2dnkPUB.js} +1 -1
- package/frontend-dist/assets/plus-DMIShKy8.js +1 -0
- package/frontend-dist/assets/search-UtxfMw7n.js +1 -0
- package/frontend-dist/assets/{sparkles-CMVIUBhw.js → sparkles-D5N-Y2_p.js} +1 -1
- package/frontend-dist/assets/{transform-domain-EUW3n3QQ.js → transform-domain-CkV4x8zq.js} +1 -1
- package/frontend-dist/assets/{trash-2-Dr0knJ6W.js → trash-2-BaCRrM0V.js} +1 -1
- package/frontend-dist/assets/{useClipboard-FgfV5ENq.js → useClipboard-DQmXg57D.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-CAjO88wE.js → useLogRetention-B8NWz1yc.js} +1 -1
- package/frontend-dist/assets/{useProviderGroups-D54_h0Tk.js → useProviderGroups-LJ8Q92q6.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +2 -2
- package/frontend-dist/assets/arrow-down-Cq91MPOV.js +0 -1
- package/frontend-dist/assets/chevron-right-hfnLtmm6.js +0 -1
- package/frontend-dist/assets/dialog-DQdqkKtA.js +0 -1
- package/frontend-dist/assets/index-BcYqgllr.css +0 -1
- package/frontend-dist/assets/plus-88ELM2ph.js +0 -1
- package/frontend-dist/assets/search-CpQM_QMJ.js +0 -1
package/dist/admin/logs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore } from "../db/index.js";
|
|
2
|
+
import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore, extractThinkingLevel } from "../db/index.js";
|
|
3
3
|
import { HTTP_NOT_FOUND } from "./constants.js";
|
|
4
4
|
import { API_CODE, apiError } from "./api-response.js";
|
|
5
5
|
const LogQuerySchema = Type.Object({
|
|
@@ -68,22 +68,11 @@ export const adminLogRoutes = (app, options, done) => {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
// JSONL 回填后重新提取 thinking level(覆盖 SQL 层因 client_request 为 null 得出的 'off')
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (log.api_type === "anthropic") {
|
|
77
|
-
if (body.thinking?.type)
|
|
78
|
-
log.thinking_level = body.thinking.type;
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
log.thinking_level = body.reasoning?.effort ?? body.reasoning_effort ?? "off";
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
log.thinking_level = "off";
|
|
71
|
+
// 兼容历史数据:thinking_level === 'off' 且 client_request 存在时重新计算
|
|
72
|
+
if (log.thinking_level === 'off' && log.client_request) {
|
|
73
|
+
const computed = extractThinkingLevel(log.api_type, log.client_request);
|
|
74
|
+
if (computed !== 'off') {
|
|
75
|
+
log.thinking_level = computed;
|
|
87
76
|
}
|
|
88
77
|
}
|
|
89
78
|
return reply.send(log);
|
package/dist/db/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappin
|
|
|
6
6
|
export type { MappingGroup, ProviderModelEntry } from "./mappings.js";
|
|
7
7
|
export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
8
8
|
export type { RetryRule } from "./retry-rules.js";
|
|
9
|
-
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogStreamContent, updateLogClientStatus, estimateLogTableSize, deleteOldestLogs, getLogCount, updateLogPipelineSnapshot, } from "./logs.js";
|
|
9
|
+
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogStreamContent, updateLogClientStatus, estimateLogTableSize, deleteOldestLogs, getLogCount, updateLogPipelineSnapshot, extractThinkingLevel, } from "./logs.js";
|
|
10
10
|
export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
|
|
11
11
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
12
12
|
export type { RouterKey } from "./router-keys.js";
|
package/dist/db/index.js
CHANGED
|
@@ -184,7 +184,7 @@ function runApplicationMigrations(db) {
|
|
|
184
184
|
export { getActiveProviders, getAllProviders, getProviderById, getActiveProviderByName, getActiveProvidersWithModels, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
185
185
|
export { getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
|
186
186
|
export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
187
|
-
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogStreamContent, updateLogClientStatus, estimateLogTableSize, deleteOldestLogs, getLogCount, updateLogPipelineSnapshot, } from "./logs.js";
|
|
187
|
+
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogStreamContent, updateLogClientStatus, estimateLogTableSize, deleteOldestLogs, getLogCount, updateLogPipelineSnapshot, extractThinkingLevel, } from "./logs.js";
|
|
188
188
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
189
189
|
export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBreakdown } from "./metrics.js";
|
|
190
190
|
export { getStats, getLatestMetricTime } from "./stats.js";
|
package/dist/db/logs.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import type { LogFileWriter } from "../storage/log-file-writer.js";
|
|
3
3
|
import { type RetryMatcher } from "../proxy/log-detail-policy.js";
|
|
4
|
+
/** 从 client_request JSON 中提取 thinking_level,按 api_type 分支处理 */
|
|
5
|
+
export declare function extractThinkingLevel(apiType: string, clientRequest: string | null): string;
|
|
4
6
|
export interface RequestLog {
|
|
5
7
|
id: string;
|
|
6
8
|
api_type: string;
|
|
@@ -61,6 +63,7 @@ export interface RequestLogInsert {
|
|
|
61
63
|
failover_trigger?: string | null;
|
|
62
64
|
upstream_api_type?: string | null;
|
|
63
65
|
upstream_base_url?: string | null;
|
|
66
|
+
thinking_level?: string;
|
|
64
67
|
}
|
|
65
68
|
export interface LogWriteContext {
|
|
66
69
|
matcher?: RetryMatcher | null;
|
package/dist/db/logs.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { shouldPreserveDetail } from "../proxy/log-detail-policy.js";
|
|
2
2
|
import { getCachedStmt } from "./helpers.js";
|
|
3
|
+
/** 从 client_request JSON 中提取 thinking_level,按 api_type 分支处理 */
|
|
4
|
+
export function extractThinkingLevel(apiType, clientRequest) {
|
|
5
|
+
if (!clientRequest)
|
|
6
|
+
return "off";
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(clientRequest);
|
|
9
|
+
const body = parsed?.body;
|
|
10
|
+
if (!body || typeof body !== "object")
|
|
11
|
+
return "off";
|
|
12
|
+
if (apiType === "anthropic") {
|
|
13
|
+
return body.thinking?.type ?? "off";
|
|
14
|
+
}
|
|
15
|
+
return body.reasoning?.effort ?? body.reasoning_effort ?? "off";
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return "off";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
3
21
|
// --- request_logs ---
|
|
4
22
|
const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.client_status_code, rl.latency_ms,
|
|
5
23
|
rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.is_failover, rl.original_request_id, rl.original_model,
|
|
@@ -10,11 +28,7 @@ const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status
|
|
|
10
28
|
rm.input_tokens_estimated, rm.client_type, rm.cache_read_tokens_estimated,
|
|
11
29
|
COALESCE(p.name, rl.provider_id) AS provider_name,
|
|
12
30
|
rl.upstream_api_type, rl.upstream_base_url,
|
|
13
|
-
|
|
14
|
-
WHEN rl.client_request IS NULL THEN 'off'
|
|
15
|
-
WHEN rl.api_type = 'anthropic' THEN COALESCE(json_extract(rl.client_request, '$.body.thinking.type'), 'off')
|
|
16
|
-
ELSE COALESCE(json_extract(rl.client_request, '$.body.reasoning.effort'), json_extract(rl.client_request, '$.body.reasoning_effort'), 'off')
|
|
17
|
-
END AS thinking_level`;
|
|
31
|
+
COALESCE(rl.thinking_level, 'off') AS thinking_level`;
|
|
18
32
|
const LOG_LIST_JOIN = `LEFT JOIN providers p ON p.id = rl.provider_id LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id`;
|
|
19
33
|
/** DB INSERT 逻辑 */
|
|
20
34
|
function rawInsertRequestLog(db, log, writeContext) {
|
|
@@ -24,9 +38,9 @@ function rawInsertRequestLog(db, log, writeContext) {
|
|
|
24
38
|
is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
|
|
25
39
|
is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot,
|
|
26
40
|
transport_kind, abort_reason, error_code, headers_sent, resilience_action, resilience_reason, mapping_reason, failover_trigger,
|
|
27
|
-
upstream_api_type, upstream_base_url)
|
|
41
|
+
upstream_api_type, upstream_base_url, thinking_level)
|
|
28
42
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
29
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null, log.transport_kind ?? null, log.abort_reason ?? null, log.error_code ?? null, log.headers_sent ?? null, log.resilience_action ?? null, log.resilience_reason ?? null, log.mapping_reason ?? null, log.failover_trigger ?? null, log.upstream_api_type ?? null, log.upstream_base_url ?? null);
|
|
43
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null, log.transport_kind ?? null, log.abort_reason ?? null, log.error_code ?? null, log.headers_sent ?? null, log.resilience_action ?? null, log.resilience_reason ?? null, log.mapping_reason ?? null, log.failover_trigger ?? null, log.upstream_api_type ?? null, log.upstream_base_url ?? null, log.thinking_level ?? extractThinkingLevel(log.api_type, log.client_request ?? null));
|
|
30
44
|
}
|
|
31
45
|
export function insertRequestLog(db, log, writeContext) {
|
|
32
46
|
// 文件写入:始终同步调用(WriteStream 内部异步,不阻塞事件循环)
|
|
@@ -102,17 +116,12 @@ export function getRequestLogs(db, options) {
|
|
|
102
116
|
.all(...params, options.limit, offset);
|
|
103
117
|
return { data, total };
|
|
104
118
|
}
|
|
105
|
-
const LOG_DETAIL_THINKING_LEVEL = `,
|
|
106
|
-
CASE
|
|
107
|
-
WHEN rl.client_request IS NULL THEN 'off'
|
|
108
|
-
WHEN rl.api_type = 'anthropic' THEN COALESCE(json_extract(rl.client_request, '$.body.thinking.type'), 'off')
|
|
109
|
-
ELSE COALESCE(json_extract(rl.client_request, '$.body.reasoning.effort'), json_extract(rl.client_request, '$.body.reasoning_effort'), 'off')
|
|
110
|
-
END AS thinking_level`;
|
|
111
119
|
export function getRequestLogById(db, id) {
|
|
112
|
-
return db
|
|
120
|
+
return getCachedStmt(db, `SELECT rl.*, rm.input_tokens, rm.output_tokens, rm.cache_read_tokens, rm.ttft_ms,
|
|
113
121
|
rm.tokens_per_second, rm.stop_reason, rm.backend_model, rm.is_complete AS metrics_complete,
|
|
114
122
|
rm.input_tokens_estimated, rm.client_type, rm.cache_read_tokens_estimated,
|
|
115
|
-
COALESCE(p.name, rl.provider_id) AS provider_name
|
|
123
|
+
COALESCE(p.name, rl.provider_id) AS provider_name,
|
|
124
|
+
COALESCE(rl.thinking_level, 'off') AS thinking_level,
|
|
116
125
|
rl.upstream_api_type, rl.upstream_base_url
|
|
117
126
|
FROM request_logs rl
|
|
118
127
|
LEFT JOIN providers p ON p.id = rl.provider_id
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- ADR 0007 优化2:thinking_level 列
|
|
2
|
+
-- 将查询时 json_extract(client_request) 改为写入时计算、存为独立列
|
|
3
|
+
ALTER TABLE request_logs ADD COLUMN thinking_level TEXT NOT NULL DEFAULT 'off';
|
|
4
|
+
|
|
5
|
+
-- 索引:支持按 thinking_level 筛选
|
|
6
|
+
CREATE INDEX idx_request_logs_thinking_level ON request_logs(thinking_level);
|
package/dist/index.js
CHANGED
|
@@ -404,6 +404,11 @@ export async function buildApp(options) {
|
|
|
404
404
|
};
|
|
405
405
|
}
|
|
406
406
|
export async function main() {
|
|
407
|
+
// 启动期一次性 WARN:DEV_SKIP_AUTH=1 跳过 admin token 校验(仅 loopback 放行)
|
|
408
|
+
if (process.env.DEV_SKIP_AUTH === "1") {
|
|
409
|
+
console.warn("\n⚠️ [SECURITY] DEV_SKIP_AUTH=1 — admin API is unauthenticated for loopback requests.\n" +
|
|
410
|
+
" Do NOT use in production. Setup flow is unchanged — password still required.\n");
|
|
411
|
+
}
|
|
407
412
|
const { app, close } = await buildApp();
|
|
408
413
|
const config = getConfig();
|
|
409
414
|
// 全局兜底:防止未捕获异常导致进程崩溃
|
|
@@ -6,6 +6,26 @@ import { verifyPassword } from "../utils/password.js";
|
|
|
6
6
|
import { isForwardedProtoHttps } from "../utils/cookie-secure.js";
|
|
7
7
|
import { API_CODE, apiError } from "../admin/api-response.js";
|
|
8
8
|
const HTTP_UNAUTHORIZED = 401;
|
|
9
|
+
// DEV_SKIP_AUTH=1 时跳过 admin token 校验,但仅放行 loopback 来源的请求。
|
|
10
|
+
// 用途:本地开发时免登录。仅作用于 admin API,不影响 router_keys 代理认证。
|
|
11
|
+
// 不签 cookie、不写 DB、不调 setup;未初始化时仍需人工走 setup 流程。
|
|
12
|
+
// 每次请求时读取 env,便于测试时切换;生产环境绝不允许启用。
|
|
13
|
+
function isDevSkipAuthEnabled() {
|
|
14
|
+
return process.env.DEV_SKIP_AUTH === "1";
|
|
15
|
+
}
|
|
16
|
+
function isLoopbackIp(ip) {
|
|
17
|
+
if (!ip)
|
|
18
|
+
return false;
|
|
19
|
+
// Node dual-stack 下 IPv4 客户端可能得到 "::ffff:127.0.0.1"
|
|
20
|
+
const IPV4_MAPPED_PREFIX = "::ffff:";
|
|
21
|
+
const IPV4_MAPPED_PREFIX_LENGTH = IPV4_MAPPED_PREFIX.length;
|
|
22
|
+
const normalized = ip.startsWith(IPV4_MAPPED_PREFIX)
|
|
23
|
+
? ip.slice(IPV4_MAPPED_PREFIX_LENGTH)
|
|
24
|
+
: ip;
|
|
25
|
+
return (normalized === "127.0.0.1" ||
|
|
26
|
+
normalized === "::1" ||
|
|
27
|
+
normalized === "0:0:0:0:0:0:0:1");
|
|
28
|
+
}
|
|
9
29
|
const adminAuthRaw = (app, options, done) => {
|
|
10
30
|
app.register(cookie);
|
|
11
31
|
app.addHook("onRequest", async (request, reply) => {
|
|
@@ -19,6 +39,15 @@ const adminAuthRaw = (app, options, done) => {
|
|
|
19
39
|
// 非 admin API 路径跳过
|
|
20
40
|
if (!path.startsWith("/admin/api/"))
|
|
21
41
|
return;
|
|
42
|
+
// Dev 模式:仅放行 loopback 访问的 admin API
|
|
43
|
+
if (isDevSkipAuthEnabled()) {
|
|
44
|
+
if (!isLoopbackIp(request.ip)) {
|
|
45
|
+
return reply
|
|
46
|
+
.code(HTTP_UNAUTHORIZED)
|
|
47
|
+
.send(apiError(API_CODE.TOKEN_INVALID, "DEV_SKIP_AUTH requires loopback access"));
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
22
51
|
// 未初始化时返回 needsSetup
|
|
23
52
|
if (!isInitialized(options.db)) {
|
|
24
53
|
return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.NOT_INITIALIZED, "Not initialized"));
|
|
@@ -10,9 +10,7 @@ import type { RequestTracker } from "../../core/monitor/index.js";
|
|
|
10
10
|
import type { AdaptiveController } from "../../core/concurrency/index.js";
|
|
11
11
|
/**
|
|
12
12
|
* 从 clientRequest JSON 中提取 thinking level。
|
|
13
|
-
*
|
|
14
|
-
* Anthropic: body.thinking.type
|
|
15
|
-
* OpenAI / Responses API: body.reasoning.effort > body.reasoning_effort
|
|
13
|
+
* 委托给 db/logs.ts 的 extractThinkingLevel,保持日志写入和 orchestrator 使用同一逻辑。
|
|
16
14
|
*/
|
|
17
15
|
export declare function extractThinkingLevelFromRequest(clientRequest: string | undefined, apiType: "openai" | "openai-responses" | "anthropic"): string;
|
|
18
16
|
export interface OrchestratorConfig {
|
|
@@ -3,32 +3,15 @@ import { ResilienceLayer as ResilienceLayerClass } from "./resilience.js";
|
|
|
3
3
|
import { SemaphoreScope as SemaphoreScopeClass } from "./scope.js";
|
|
4
4
|
import { TrackerScope as TrackerScopeClass } from "./scope.js";
|
|
5
5
|
import { SemaphoreTimeoutError, SemaphoreQueueFullError } from "../../core/errors.js";
|
|
6
|
+
import { extractThinkingLevel } from "../../db/logs.js";
|
|
6
7
|
const DEFAULT_BASE_DELAY_MS = 1000;
|
|
7
8
|
const DEFAULT_FAILOVER_THRESHOLD = 400;
|
|
8
9
|
/**
|
|
9
10
|
* 从 clientRequest JSON 中提取 thinking level。
|
|
10
|
-
*
|
|
11
|
-
* Anthropic: body.thinking.type
|
|
12
|
-
* OpenAI / Responses API: body.reasoning.effort > body.reasoning_effort
|
|
11
|
+
* 委托给 db/logs.ts 的 extractThinkingLevel,保持日志写入和 orchestrator 使用同一逻辑。
|
|
13
12
|
*/
|
|
14
13
|
export function extractThinkingLevelFromRequest(clientRequest, apiType) {
|
|
15
|
-
|
|
16
|
-
if (!clientRequest)
|
|
17
|
-
return "off";
|
|
18
|
-
const parsed = JSON.parse(clientRequest);
|
|
19
|
-
const body = parsed?.body;
|
|
20
|
-
if (!body || typeof body !== "object")
|
|
21
|
-
return "off";
|
|
22
|
-
if (apiType === "anthropic") {
|
|
23
|
-
return body.thinking?.type ?? "off";
|
|
24
|
-
}
|
|
25
|
-
// openai / openai-responses: reasoning.effort 优先于 reasoning_effort
|
|
26
|
-
return body.reasoning?.effort ?? body.reasoning_effort ?? "off";
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// client_request 格式异常时静默降级为 off,不影响代理流程
|
|
30
|
-
return "off";
|
|
31
|
-
}
|
|
14
|
+
return extractThinkingLevel(apiType, clientRequest ?? null);
|
|
32
15
|
}
|
|
33
16
|
/**
|
|
34
17
|
* 工厂函数,消除 openai/anthropic 创建 orchestrator 的重复代码。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** 压缩所有已结束的文件(新格式 + 旧格式)。对外接口,供测试和维护调用 */
|
|
2
2
|
export declare function compressFinishedFiles(baseDir: string, now: Date): number;
|
|
3
3
|
/** 删除超过保留天数的日期目录 */
|
|
4
4
|
export declare function cleanExpiredDirs(baseDir: string, retentionDays: number, now: Date): number;
|
|
@@ -1,12 +1,49 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, writeFileSync, unlinkSync, rmSync, existsSync } from "node:fs";
|
|
1
|
+
import { readdirSync, readFileSync, writeFileSync, unlinkSync, rmSync, existsSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
|
-
import {
|
|
4
|
+
import { localDateStr } from "./types.js";
|
|
5
5
|
const SECONDS_PER_MINUTE = 60;
|
|
6
6
|
const MS_PER_SECOND = 1000;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const COMPRESSION_INTERVAL_MINUTES = 10;
|
|
8
|
+
const COMPRESSION_INTERVAL_MS = COMPRESSION_INTERVAL_MINUTES * SECONDS_PER_MINUTE * MS_PER_SECOND;
|
|
9
|
+
/** 压缩新格式:遍历 {date}/{HH}/*.json,mtime 超过 10 分钟的压缩为 .json.gz */
|
|
10
|
+
function compressNewFormatFiles(baseDir, now) {
|
|
11
|
+
let compressed = 0;
|
|
12
|
+
if (!existsSync(baseDir))
|
|
13
|
+
return 0;
|
|
14
|
+
const dayDirs = readdirSync(baseDir, { withFileTypes: true })
|
|
15
|
+
.filter(d => d.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(d.name));
|
|
16
|
+
for (const dayDir of dayDirs) {
|
|
17
|
+
const dayPath = join(baseDir, dayDir.name);
|
|
18
|
+
const hourDirs = readdirSync(dayPath, { withFileTypes: true })
|
|
19
|
+
.filter(d => d.isDirectory() && /^\d{2}$/.test(d.name));
|
|
20
|
+
for (const hourDir of hourDirs) {
|
|
21
|
+
const hourPath = join(dayPath, hourDir.name);
|
|
22
|
+
const files = readdirSync(hourPath);
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
if (!file.endsWith(".json") || file.endsWith(".json.gz"))
|
|
25
|
+
continue;
|
|
26
|
+
const filePath = join(hourPath, file);
|
|
27
|
+
try {
|
|
28
|
+
const stat = statSync(filePath);
|
|
29
|
+
if (now.getTime() - stat.mtimeMs > COMPRESSION_INTERVAL_MS) {
|
|
30
|
+
const content = readFileSync(filePath);
|
|
31
|
+
writeFileSync(filePath + ".gz", gzipSync(content));
|
|
32
|
+
unlinkSync(filePath);
|
|
33
|
+
compressed++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* 新格式文件可能正在被写入,跳过 */
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return compressed;
|
|
44
|
+
}
|
|
45
|
+
/** 压缩旧格式:将已结束窗口的 .jsonl 文件压缩为 .jsonl.gz */
|
|
46
|
+
function compressLegacyJsonlFiles(baseDir, now) {
|
|
10
47
|
let compressed = 0;
|
|
11
48
|
if (!existsSync(baseDir))
|
|
12
49
|
return 0;
|
|
@@ -25,25 +62,28 @@ export function compressFinishedFiles(baseDir, now) {
|
|
|
25
62
|
const fileMinute = parseInt(match[2], 10);
|
|
26
63
|
// 使用 UTC 时间构建窗口结束时间
|
|
27
64
|
const dateParts = dayDir.name.split("-").map(Number);
|
|
28
|
-
const windowEnd = new Date(Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2], fileHour, fileMinute +
|
|
65
|
+
const windowEnd = new Date(Date.UTC(dateParts[0], dateParts[1] - 1, dateParts[2], fileHour, fileMinute + COMPRESSION_INTERVAL_MINUTES));
|
|
29
66
|
if (now >= windowEnd) {
|
|
30
67
|
const filePath = join(dirPath, file);
|
|
31
68
|
try {
|
|
32
69
|
const content = readFileSync(filePath);
|
|
33
|
-
|
|
34
|
-
writeFileSync(filePath + ".gz", gzipped);
|
|
70
|
+
writeFileSync(filePath + ".gz", gzipSync(content));
|
|
35
71
|
unlinkSync(filePath);
|
|
36
72
|
compressed++;
|
|
37
|
-
// eslint-disable-next-line taste/no-silent-catch
|
|
38
73
|
}
|
|
39
74
|
catch {
|
|
40
|
-
|
|
75
|
+
/* 旧格式文件可能正在被写入,跳过 */
|
|
76
|
+
continue;
|
|
41
77
|
}
|
|
42
78
|
}
|
|
43
79
|
}
|
|
44
80
|
}
|
|
45
81
|
return compressed;
|
|
46
82
|
}
|
|
83
|
+
/** 压缩所有已结束的文件(新格式 + 旧格式)。对外接口,供测试和维护调用 */
|
|
84
|
+
export function compressFinishedFiles(baseDir, now) {
|
|
85
|
+
return compressNewFormatFiles(baseDir, now) + compressLegacyJsonlFiles(baseDir, now);
|
|
86
|
+
}
|
|
47
87
|
/** 删除超过保留天数的日期目录 */
|
|
48
88
|
export function cleanExpiredDirs(baseDir, retentionDays, now) {
|
|
49
89
|
if (!existsSync(baseDir))
|
|
@@ -65,10 +105,12 @@ export function scheduleLogFileMaintenance(baseDir, options) {
|
|
|
65
105
|
const intervalMs = options.intervalMs ?? COMPRESSION_INTERVAL_MS;
|
|
66
106
|
const doMaintenance = () => {
|
|
67
107
|
const now = new Date();
|
|
68
|
-
const
|
|
108
|
+
const newCompressed = compressNewFormatFiles(baseDir, now);
|
|
109
|
+
const legacyCompressed = compressLegacyJsonlFiles(baseDir, now);
|
|
69
110
|
const deleted = cleanExpiredDirs(baseDir, options.retentionDays, now);
|
|
70
|
-
|
|
71
|
-
|
|
111
|
+
const totalCompressed = newCompressed + legacyCompressed;
|
|
112
|
+
if (totalCompressed > 0 || deleted > 0) {
|
|
113
|
+
options.log.info(`Log file maintenance: compressed ${totalCompressed} files (${newCompressed} new + ${legacyCompressed} legacy), deleted ${deleted} dirs`);
|
|
72
114
|
}
|
|
73
115
|
};
|
|
74
116
|
const timer = setInterval(doMaintenance, intervalMs);
|
|
@@ -5,24 +5,34 @@ export interface LogFileWriterOptions {
|
|
|
5
5
|
export declare class LogFileWriter {
|
|
6
6
|
private readonly baseDir;
|
|
7
7
|
private readonly enabled;
|
|
8
|
-
private readonly streams;
|
|
9
8
|
constructor(baseDir: string, options?: LogFileWriterOptions);
|
|
10
9
|
get isEnabled(): boolean;
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 写入单条日志文件。异步 fire-and-forget 模式:
|
|
12
|
+
* 不阻塞事件循环,失败不影响主流程。
|
|
13
|
+
*/
|
|
12
14
|
write(entry: LogFileEntry): void;
|
|
13
15
|
/**
|
|
14
|
-
* 根据 id 和 created_at
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* 根据 id 和 created_at 读取完整记录。
|
|
17
|
+
* Fallback 链:
|
|
18
|
+
* 1. {date}/{HH}/{id}.json → O(1) 直接命中
|
|
19
|
+
* 2. {date}/{HH}/{id}.json.gz → O(1) 解压单文件
|
|
20
|
+
* 3. {date}/{HH-MM}.jsonl → 旧格式线性扫描(精确窗口)
|
|
21
|
+
* 4. {date}/{HH-MM}.jsonl.gz → 旧格式压缩线性扫描
|
|
17
22
|
*/
|
|
18
23
|
read(id: string, createdAt: string): LogFileEntry | null;
|
|
24
|
+
private readSingleJsonFile;
|
|
25
|
+
private readSingleGzFile;
|
|
26
|
+
/**
|
|
27
|
+
* 旧格式 JSONL 回退读取。
|
|
28
|
+
* 根据 created_at 精确计算所在 10 分钟窗口,只扫描 1 个文件。
|
|
29
|
+
*/
|
|
30
|
+
private readFromLegacyJsonl;
|
|
19
31
|
private findByIdInFile;
|
|
20
32
|
private findByIdInGzFile;
|
|
21
33
|
private parseAndFind;
|
|
22
34
|
/**
|
|
23
|
-
*
|
|
24
|
-
* 返回 Promise 以便调用方 await 确保数据落盘。
|
|
25
|
-
* 不 await 调用也是安全的(仅可能丢失最后的缓冲数据)。
|
|
35
|
+
* 停止(保持接口兼容,新格式无 WriteStream 需要关闭)。
|
|
26
36
|
*/
|
|
27
37
|
stop(): Promise<void>;
|
|
28
38
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { gunzipSync } from "node:zlib";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import { localDateStr } from "./types.js";
|
|
6
|
+
/** 数值补零宽度 */
|
|
7
|
+
const PAD_WIDTH = 2;
|
|
8
|
+
/** 旧 JSONL 格式的窗口分钟数 */
|
|
9
|
+
const LEGACY_WINDOW_MINUTES = 10;
|
|
10
|
+
/** 从日期对象生成新格式路径片段:{date}/{HH}/{id}.json */
|
|
11
|
+
function newFilePathParts(d, id) {
|
|
7
12
|
const dateStr = localDateStr(d);
|
|
8
|
-
const hour = d.getUTCHours().toString().padStart(
|
|
9
|
-
|
|
10
|
-
const minuteStr = minute.toString().padStart(TIME_PAD_WIDTH, "0");
|
|
11
|
-
return { dateStr, fileName: `${hour}-${minuteStr}.jsonl` };
|
|
13
|
+
const hour = d.getUTCHours().toString().padStart(PAD_WIDTH, "0");
|
|
14
|
+
return { dateStr, hourDir: join(dateStr, hour), fileName: `${id}.json` };
|
|
12
15
|
}
|
|
13
|
-
const FLUSH_TIMEOUT_MS = 2000;
|
|
14
16
|
export class LogFileWriter {
|
|
15
17
|
baseDir;
|
|
16
18
|
enabled;
|
|
17
|
-
streams = new Map();
|
|
18
19
|
constructor(baseDir, options) {
|
|
19
20
|
this.baseDir = baseDir;
|
|
20
21
|
this.enabled = options?.enabled ?? true;
|
|
@@ -22,54 +23,88 @@ export class LogFileWriter {
|
|
|
22
23
|
get isEnabled() {
|
|
23
24
|
return this.enabled;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
stream = createWriteStream(filePath, { flags: "a", encoding: "utf-8" });
|
|
30
|
-
this.streams.set(filePath, stream);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return stream;
|
|
37
|
-
}
|
|
26
|
+
/**
|
|
27
|
+
* 写入单条日志文件。异步 fire-and-forget 模式:
|
|
28
|
+
* 不阻塞事件循环,失败不影响主流程。
|
|
29
|
+
*/
|
|
38
30
|
write(entry) {
|
|
39
31
|
if (!this.enabled)
|
|
40
32
|
return;
|
|
41
|
-
const {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
const { hourDir, fileName } = newFilePathParts(new Date(entry.created_at), entry.id);
|
|
34
|
+
const filePath = join(this.baseDir, hourDir, fileName);
|
|
35
|
+
// 异步写入(fire-and-forget),不阻塞事件循环
|
|
36
|
+
fs.mkdir(dirname(filePath), { recursive: true })
|
|
37
|
+
.then(() => fs.writeFile(filePath, JSON.stringify(entry)))
|
|
38
|
+
.catch(() => {
|
|
39
|
+
/* 文件写入是辅助通道,失败不影响主流程 */
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 根据 id 和 created_at 读取完整记录。
|
|
44
|
+
* Fallback 链:
|
|
45
|
+
* 1. {date}/{HH}/{id}.json → O(1) 直接命中
|
|
46
|
+
* 2. {date}/{HH}/{id}.json.gz → O(1) 解压单文件
|
|
47
|
+
* 3. {date}/{HH-MM}.jsonl → 旧格式线性扫描(精确窗口)
|
|
48
|
+
* 4. {date}/{HH-MM}.jsonl.gz → 旧格式压缩线性扫描
|
|
49
|
+
*/
|
|
50
|
+
read(id, createdAt) {
|
|
51
|
+
if (!this.enabled)
|
|
52
|
+
return null;
|
|
53
|
+
const d = new Date(createdAt);
|
|
54
|
+
// 新格式路径
|
|
55
|
+
const { dateStr, hourDir } = newFilePathParts(d, id);
|
|
56
|
+
const hourPath = join(this.baseDir, hourDir);
|
|
57
|
+
// 1. 尝试未压缩单条文件
|
|
58
|
+
const jsonPath = join(hourPath, `${id}.json`);
|
|
59
|
+
if (existsSync(jsonPath)) {
|
|
60
|
+
return this.readSingleJsonFile(jsonPath);
|
|
45
61
|
}
|
|
46
|
-
|
|
47
|
-
const
|
|
62
|
+
// 2. 尝试压缩单条文件
|
|
63
|
+
const gzPath = jsonPath + ".gz";
|
|
64
|
+
if (existsSync(gzPath)) {
|
|
65
|
+
return this.readSingleGzFile(gzPath);
|
|
66
|
+
}
|
|
67
|
+
// 3-4. 旧格式 fallback:扫描 JSONL 文件
|
|
68
|
+
return this.readFromLegacyJsonl(dateStr, d, id);
|
|
69
|
+
}
|
|
70
|
+
readSingleJsonFile(filePath) {
|
|
48
71
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
72
|
+
const content = readFileSync(filePath, "utf-8");
|
|
73
|
+
return JSON.parse(content);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* 文件读取失败返回 null */
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
readSingleGzFile(gzPath) {
|
|
81
|
+
try {
|
|
82
|
+
const compressed = readFileSync(gzPath);
|
|
83
|
+
const content = gunzipSync(compressed).toString("utf-8");
|
|
84
|
+
return JSON.parse(content);
|
|
52
85
|
}
|
|
53
86
|
catch {
|
|
54
|
-
|
|
87
|
+
/* 解压或解析失败返回 null */
|
|
88
|
+
return null;
|
|
55
89
|
}
|
|
56
90
|
}
|
|
57
91
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* 返回 null 表示找不到。
|
|
92
|
+
* 旧格式 JSONL 回退读取。
|
|
93
|
+
* 根据 created_at 精确计算所在 10 分钟窗口,只扫描 1 个文件。
|
|
61
94
|
*/
|
|
62
|
-
|
|
63
|
-
if (!this.enabled)
|
|
64
|
-
return null;
|
|
65
|
-
const { dateStr, fileName } = localFilePathParts(new Date(createdAt));
|
|
95
|
+
readFromLegacyJsonl(dateStr, d, id) {
|
|
66
96
|
const dayDir = join(this.baseDir, dateStr);
|
|
67
|
-
|
|
97
|
+
const hour = d.getUTCHours().toString().padStart(PAD_WIDTH, "0");
|
|
98
|
+
const minute = Math.floor(d.getUTCMinutes() / LEGACY_WINDOW_MINUTES) * LEGACY_WINDOW_MINUTES;
|
|
99
|
+
const fileName = `${hour}-${minute.toString().padStart(PAD_WIDTH, "0")}.jsonl`;
|
|
100
|
+
// 尝试未压缩 JSONL
|
|
68
101
|
const filePath = join(dayDir, fileName);
|
|
69
102
|
if (existsSync(filePath)) {
|
|
70
|
-
|
|
103
|
+
const result = this.findByIdInFile(filePath, id);
|
|
104
|
+
if (result)
|
|
105
|
+
return result;
|
|
71
106
|
}
|
|
72
|
-
//
|
|
107
|
+
// 尝试压缩 JSONL
|
|
73
108
|
const gzPath = filePath + ".gz";
|
|
74
109
|
if (existsSync(gzPath)) {
|
|
75
110
|
return this.findByIdInGzFile(gzPath, id);
|
|
@@ -82,6 +117,7 @@ export class LogFileWriter {
|
|
|
82
117
|
return this.parseAndFind(content, id);
|
|
83
118
|
}
|
|
84
119
|
catch {
|
|
120
|
+
/* 文件读取失败返回 null */
|
|
85
121
|
return null;
|
|
86
122
|
}
|
|
87
123
|
}
|
|
@@ -92,6 +128,7 @@ export class LogFileWriter {
|
|
|
92
128
|
return this.parseAndFind(content, id);
|
|
93
129
|
}
|
|
94
130
|
catch {
|
|
131
|
+
/* 解压或解析失败返回 null */
|
|
95
132
|
return null;
|
|
96
133
|
}
|
|
97
134
|
}
|
|
@@ -103,37 +140,18 @@ export class LogFileWriter {
|
|
|
103
140
|
const entry = JSON.parse(line);
|
|
104
141
|
if (entry.id === id)
|
|
105
142
|
return entry;
|
|
106
|
-
// eslint-disable-next-line taste/no-silent-catch
|
|
107
143
|
}
|
|
108
144
|
catch {
|
|
109
|
-
|
|
145
|
+
/* 跳过损坏行 */
|
|
146
|
+
continue;
|
|
110
147
|
}
|
|
111
148
|
}
|
|
112
149
|
return null;
|
|
113
150
|
}
|
|
114
151
|
/**
|
|
115
|
-
*
|
|
116
|
-
* 返回 Promise 以便调用方 await 确保数据落盘。
|
|
117
|
-
* 不 await 调用也是安全的(仅可能丢失最后的缓冲数据)。
|
|
152
|
+
* 停止(保持接口兼容,新格式无 WriteStream 需要关闭)。
|
|
118
153
|
*/
|
|
119
154
|
stop() {
|
|
120
|
-
|
|
121
|
-
return Promise.resolve();
|
|
122
|
-
const pending = [];
|
|
123
|
-
for (const [, stream] of this.streams) {
|
|
124
|
-
if (stream.destroyed)
|
|
125
|
-
continue;
|
|
126
|
-
pending.push(new Promise((resolve) => {
|
|
127
|
-
const timer = setTimeout(resolve, FLUSH_TIMEOUT_MS);
|
|
128
|
-
stream.once("finish", () => { clearTimeout(timer); resolve(); });
|
|
129
|
-
stream.once("error", () => { clearTimeout(timer); resolve(); });
|
|
130
|
-
}));
|
|
131
|
-
try {
|
|
132
|
-
stream.end();
|
|
133
|
-
}
|
|
134
|
-
catch { /* stream 可能已关闭 */ } // eslint-disable-line taste/no-silent-catch
|
|
135
|
-
}
|
|
136
|
-
this.streams.clear();
|
|
137
|
-
return Promise.allSettled(pending).then(() => { });
|
|
155
|
+
return Promise.resolve();
|
|
138
156
|
}
|
|
139
157
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Gt as e,Q as t,Xt as n,Z as r,et as i,gt as a,it as o,kt as s,n as c,rt as l,yt as u}from"./button-
|
|
1
|
+
import{Gt as e,Q as t,Xt as n,Z as r,et as i,gt as a,it as o,kt as s,n as c,rt as l,yt as u}from"./button-B63vCS-z.js";import{lt as d,r as f,ut as p}from"./index-efKx2YB4.js";import{t as m}from"./Card-C0Z5Y2qq.js";import{t as h}from"./CardContent-CxIItFGE.js";var g={class:`min-h-screen flex items-center justify-center bg-background relative`},_={class:`text-center mb-6`},v={class:`text-sm text-muted-foreground mt-1`},y=o({__name:`AuthLayout`,props:{subtitle:{}},setup(o){let{isDark:y,toggleTheme:b}=f();return(f,x)=>(a(),i(`div`,g,[l(e(c),{variant:`ghost`,size:`icon`,class:`absolute top-4 right-4 text-muted-foreground hover:text-foreground`,onClick:e(b)},{default:s(()=>[e(y)?(a(),t(e(d),{key:1,class:`w-4 h-4`})):(a(),t(e(p),{key:0,class:`w-4 h-4`}))]),_:1},8,[`onClick`]),l(e(m),{class:`w-full max-w-sm shadow-lg`},{default:s(()=>[l(e(h),{class:`pt-6`},{default:s(()=>[r(`div`,_,[x[0]||=r(`div`,{class:`w-12 h-12 bg-primary rounded-lg mx-auto mb-3 flex items-center justify-center`},[r(`svg`,{class:`w-7 h-7 text-white`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[r(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z`})])],-1),x[1]||=r(`h1`,{class:`text-xl font-semibold text-foreground`},` LLM Simple Router `,-1),r(`p`,v,n(o.subtitle),1)]),u(f.$slots,`default`)]),_:3})]),_:3})]))}});export{y as t};
|