llm-simple-router 0.5.1 → 0.5.2
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/config/recommended-providers.json +76 -0
- package/config/recommended-retry-rules.json +10 -0
- package/dist/admin/api-response.d.ts +27 -0
- package/dist/admin/api-response.js +40 -0
- package/dist/admin/constants.d.ts +0 -2
- package/dist/admin/constants.js +0 -3
- package/dist/admin/groups.js +9 -5
- package/dist/admin/logs.js +3 -2
- package/dist/admin/mappings.js +7 -6
- package/dist/admin/metrics.js +23 -5
- package/dist/admin/monitor.js +2 -1
- package/dist/admin/providers.js +13 -4
- package/dist/admin/proxy-enhancement.js +11 -6
- package/dist/admin/recommended.js +1 -9
- package/dist/admin/retry-rules.js +8 -4
- package/dist/admin/router-keys.js +5 -1
- package/dist/admin/settings-import-export.js +3 -2
- package/dist/admin/settings.js +7 -5
- package/dist/admin/setup.js +3 -2
- package/dist/admin/stats.js +20 -3
- package/dist/admin/upgrade.js +8 -7
- package/dist/admin/usage.js +12 -24
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +11 -0
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.js +2 -2
- package/dist/db/mappings.js +5 -8
- package/dist/db/metrics.js +3 -4
- package/dist/db/providers.d.ts +8 -0
- package/dist/db/providers.js +6 -0
- package/dist/db/retry-rules.d.ts +1 -0
- package/dist/db/retry-rules.js +3 -0
- package/dist/db/stats.d.ts +1 -2
- package/dist/db/stats.js +7 -11
- package/dist/index.js +52 -34
- package/dist/metrics/metrics-extractor.js +1 -1
- package/dist/metrics/sse-parser.js +2 -0
- package/dist/middleware/admin-auth.js +6 -5
- package/dist/middleware/auth.js +1 -10
- package/dist/monitor/request-tracker.d.ts +1 -0
- package/dist/monitor/request-tracker.js +9 -45
- package/dist/monitor/runtime-collector.js +1 -1
- package/dist/monitor/stream-content-accumulator.d.ts +14 -0
- package/dist/monitor/stream-content-accumulator.js +58 -0
- package/dist/proxy/anthropic.d.ts +2 -1
- package/dist/proxy/anthropic.js +3 -3
- package/dist/proxy/enhancement/directive-parser.d.ts +18 -0
- package/dist/proxy/{directive-parser.js → enhancement/directive-parser.js} +44 -0
- package/dist/proxy/{enhancement-handler.js → enhancement/enhancement-handler.js} +152 -32
- package/dist/proxy/enhancement/index.d.ts +3 -0
- package/dist/proxy/enhancement/index.js +3 -0
- package/dist/proxy/{response-cleaner.js → enhancement/response-cleaner.js} +14 -0
- package/dist/proxy/log-helpers.d.ts +1 -1
- package/dist/proxy/mapping-resolver.js +4 -4
- package/dist/proxy/openai.d.ts +2 -1
- package/dist/proxy/openai.js +4 -4
- package/dist/proxy/orchestrator.d.ts +0 -1
- package/dist/proxy/orchestrator.js +1 -3
- package/dist/proxy/proxy-core.d.ts +0 -4
- package/dist/proxy/proxy-core.js +0 -2
- package/dist/proxy/proxy-handler.d.ts +1 -1
- package/dist/proxy/proxy-handler.js +52 -132
- package/dist/proxy/proxy-logging.d.ts +0 -2
- package/dist/proxy/proxy-logging.js +1 -3
- package/dist/proxy/resilience.d.ts +5 -2
- package/dist/proxy/resilience.js +16 -7
- package/dist/proxy/strategy/failover.js +2 -7
- package/dist/proxy/strategy/random.js +2 -2
- package/dist/proxy/strategy/round-robin.js +2 -2
- package/dist/proxy/strategy/scheduled.js +1 -8
- package/dist/proxy/strategy/targets-rule.d.ts +1 -0
- package/dist/proxy/strategy/targets-rule.js +5 -0
- package/dist/proxy/transport-fn.d.ts +25 -0
- package/dist/proxy/transport-fn.js +55 -0
- package/dist/proxy/transport.d.ts +0 -25
- package/dist/proxy/transport.js +0 -38
- package/dist/upgrade/checker.d.ts +1 -1
- package/dist/upgrade/checker.js +16 -1
- package/dist/upgrade/deployment.js +2 -2
- package/dist/utils/password.js +4 -2
- package/dist/utils/time-range.d.ts +9 -0
- package/dist/utils/time-range.js +40 -0
- package/frontend-dist/assets/{CardContent-Deyvo1TQ.js → CardContent-WrBnGhTg.js} +1 -1
- package/frontend-dist/assets/{CardTitle-DujSYXja.js → CardTitle-BcDYk7cq.js} +1 -1
- package/frontend-dist/assets/Checkbox-MZf0YsDG.js +1 -0
- package/frontend-dist/assets/{CollapsibleTrigger-ByCvAsW0.js → CollapsibleTrigger-CrOH9HlW.js} +1 -1
- package/frontend-dist/assets/{Collection-V6gcBlwC.js → Collection-DcTx_Y54.js} +1 -1
- package/frontend-dist/assets/Dashboard-D0oDrSLr.js +3 -0
- package/frontend-dist/assets/{DialogTitle-D0nwX87v.js → DialogTitle-Cl5Cd7QH.js} +1 -1
- package/frontend-dist/assets/{Input-D0kpZB31.js → Input-O0ebU-Va.js} +1 -1
- package/frontend-dist/assets/{Label-BvYK0rd6.js → Label-C_S0y7Um.js} +1 -1
- package/frontend-dist/assets/Login-DGY7uF8P.js +1 -0
- package/frontend-dist/assets/Logs-ls8pv89b.js +1 -0
- package/frontend-dist/assets/{ModelMappings-BoG2P9Rh.js → ModelMappings-DGlf0S4s.js} +1 -1
- package/frontend-dist/assets/{Monitor-W441wik3.js → Monitor-BSI87grz.js} +1 -1
- package/frontend-dist/assets/{PopperContent-DVJ4IxLF.js → PopperContent-C6Q7hDmf.js} +1 -1
- package/frontend-dist/assets/{Providers-D2rzb_Qk.js → Providers-ZkRpj8_m.js} +1 -1
- package/frontend-dist/assets/ProxyEnhancement-DFPI1W6Z.js +5 -0
- package/frontend-dist/assets/RetryRules-DtM31qsl.js +1 -0
- package/frontend-dist/assets/RouterKeys-D63tRFKm.js +1 -0
- package/frontend-dist/assets/RovingFocusItem-BJoylAKU.js +1 -0
- package/frontend-dist/assets/{SelectValue-CAEBdE04.js → SelectValue-CLp5z6_I.js} +1 -1
- package/frontend-dist/assets/{Settings-3lR8QVQt.js → Settings-DSgRKbTQ.js} +2 -2
- package/frontend-dist/assets/Setup-BDmj6CRk.js +1 -0
- package/frontend-dist/assets/{Switch-CST3045A.js → Switch-Wz-t_zkv.js} +1 -1
- package/frontend-dist/assets/TableHeader-DGtcqGkw.js +1 -0
- package/frontend-dist/assets/TabsTrigger-CPCi2HIa.js +1 -0
- package/frontend-dist/assets/{Teleport-DVgMe9KS.js → Teleport-DdjYHlNK.js} +1 -1
- package/frontend-dist/assets/TooltipTrigger-H_QoPY1n.js +1 -0
- package/frontend-dist/assets/{UnifiedRequestDialog-Fe2TfhTD.js → UnifiedRequestDialog-BAAfMJJl.js} +1 -1
- package/frontend-dist/assets/{VisuallyHidden-CjuTDGlC.js → VisuallyHidden-Cyk-jWwh.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-BaW-2aEF.js → VisuallyHiddenInput-CYjNe_H8.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-Bv6dVarS.js → alert-dialog-Bi3dliLl.js} +1 -1
- package/frontend-dist/assets/{badge-CEfcely6.js → badge-Kkta3e9W.js} +1 -1
- package/frontend-dist/assets/{button-BmxhlpN-.js → button-BQ3s7yNh.js} +2 -2
- package/frontend-dist/assets/{createLucideIcon-UWoYUKtZ.js → createLucideIcon-D1tkPDOQ.js} +1 -1
- package/frontend-dist/assets/{dialog-QaGxKbze.js → dialog-DoIATUYw.js} +1 -1
- package/frontend-dist/assets/{file-text-D38GtYz2.js → file-text-Dt6QP1bZ.js} +1 -1
- package/frontend-dist/assets/{index-D484ZFa9.js → index-BY0E7CHR.js} +1 -1
- package/frontend-dist/assets/index-Bnrh1mFY.css +1 -0
- package/frontend-dist/assets/{lib-CSYRBKqn.js → lib-CxwxnlwW.js} +1 -1
- package/frontend-dist/assets/{ohash.D__AXeF1-BUMsW586.js → ohash.D__AXeF1-b0PiKZB_.js} +1 -1
- package/frontend-dist/assets/{useClipboard-CuE5xXIg.js → useClipboard-Cnnz6AAN.js} +1 -1
- package/frontend-dist/assets/useLogRetention-DYP5LOAc.js +1 -0
- package/frontend-dist/assets/useNonce-DKbOCfgM.js +1 -0
- package/frontend-dist/assets/x-CAoitXRt.js +1 -0
- package/frontend-dist/index.html +18 -18
- package/package.json +2 -1
- package/dist/proxy/directive-parser.d.ts +0 -7
- package/frontend-dist/assets/Checkbox-BJxf-QuV.js +0 -1
- package/frontend-dist/assets/Dashboard-xqf6PcmE.js +0 -3
- package/frontend-dist/assets/Login-C9oPKRcu.js +0 -1
- package/frontend-dist/assets/Logs-DVgenFav.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-DahQkV1g.js +0 -5
- package/frontend-dist/assets/RetryRules-Bg9p50oc.js +0 -1
- package/frontend-dist/assets/RouterKeys-C1LhXbqf.js +0 -1
- package/frontend-dist/assets/Setup-Dzj1XvgF.js +0 -1
- package/frontend-dist/assets/TableHeader-CIrxcNRh.js +0 -1
- package/frontend-dist/assets/TabsContent-B4nroq3-.js +0 -1
- package/frontend-dist/assets/TabsTrigger-FsELRpyc.js +0 -1
- package/frontend-dist/assets/index-CMBzqUyT.css +0 -1
- package/frontend-dist/assets/useLogRetention-DesMKwIU.js +0 -1
- package/frontend-dist/assets/useNonce-FLqOooWA.js +0 -1
- package/frontend-dist/assets/x-BEUXSxcj.js +0 -1
- /package/dist/proxy/{enhancement-handler.d.ts → enhancement/enhancement-handler.d.ts} +0 -0
- /package/dist/proxy/{response-cleaner.d.ts → enhancement/response-cleaner.d.ts} +0 -0
- /package/frontend-dist/assets/{format-CPdJtjZ5.js → format-DOVIVsQC.js} +0 -0
package/dist/admin/setup.js
CHANGED
|
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
|
|
3
3
|
import { getSetting, setSetting, isInitialized } from "../db/settings.js";
|
|
4
4
|
import { hashPassword } from "../utils/password.js";
|
|
5
5
|
import { HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
|
|
6
|
+
import { API_CODE, apiError } from "./api-response.js";
|
|
6
7
|
const CRYPTO_BYTES_LENGTH = 32;
|
|
7
8
|
const MIN_PASSWORD_LENGTH = 6;
|
|
8
9
|
export const adminSetupRoutes = (app, options, done) => {
|
|
@@ -13,7 +14,7 @@ export const adminSetupRoutes = (app, options, done) => {
|
|
|
13
14
|
app.post("/admin/api/setup/initialize", async (request, reply) => {
|
|
14
15
|
const { password } = request.body;
|
|
15
16
|
if (!password || password.length < MIN_PASSWORD_LENGTH) {
|
|
16
|
-
return reply.code(HTTP_BAD_REQUEST).send(
|
|
17
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, `Password must be at least ${MIN_PASSWORD_LENGTH} characters`));
|
|
17
18
|
}
|
|
18
19
|
// 事务中原子检查防竞态
|
|
19
20
|
const alreadyInitialized = db.transaction(() => {
|
|
@@ -28,7 +29,7 @@ export const adminSetupRoutes = (app, options, done) => {
|
|
|
28
29
|
return false;
|
|
29
30
|
})();
|
|
30
31
|
if (alreadyInitialized) {
|
|
31
|
-
return reply.code(HTTP_CONFLICT).send(
|
|
32
|
+
return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.ALREADY_INITIALIZED, "Already initialized"));
|
|
32
33
|
}
|
|
33
34
|
// 自动登录:签发 JWT
|
|
34
35
|
const TOKEN_EXPIRY_SECONDS = 172800; // 48 hours,与 admin-auth 保持一致
|
package/dist/admin/stats.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getStats } from "../db/index.js";
|
|
3
|
+
import { resolveTimeRange } from "../utils/time-range.js";
|
|
3
4
|
const StatsQuerySchema = Type.Object({
|
|
4
|
-
period: Type.Optional(Type.
|
|
5
|
+
period: Type.Optional(Type.Union([
|
|
6
|
+
Type.Literal("window"),
|
|
7
|
+
Type.Literal("weekly"),
|
|
8
|
+
Type.Literal("monthly"),
|
|
9
|
+
])),
|
|
10
|
+
start_time: Type.Optional(Type.String()),
|
|
11
|
+
end_time: Type.Optional(Type.String()),
|
|
5
12
|
router_key_id: Type.Optional(Type.String()),
|
|
6
13
|
});
|
|
7
14
|
export const adminStatsRoutes = (app, options, done) => {
|
|
8
15
|
app.get("/admin/api/stats", { schema: { querystring: StatsQuerySchema } }, async (request, reply) => {
|
|
9
16
|
const query = request.query;
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
let startTime;
|
|
18
|
+
let endTime;
|
|
19
|
+
if (query.start_time && query.end_time) {
|
|
20
|
+
startTime = query.start_time;
|
|
21
|
+
endTime = query.end_time;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const range = resolveTimeRange((query.period ?? "weekly"), options.db, query.router_key_id);
|
|
25
|
+
startTime = range.startTime;
|
|
26
|
+
endTime = range.endTime;
|
|
27
|
+
}
|
|
28
|
+
const stats = getStats(options.db, startTime, endTime, query.router_key_id);
|
|
12
29
|
return reply.send(stats);
|
|
13
30
|
});
|
|
14
31
|
done();
|
package/dist/admin/upgrade.js
CHANGED
|
@@ -6,6 +6,7 @@ import { execSync } from 'node:child_process';
|
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../constants.js';
|
|
9
|
+
import { API_CODE, apiError } from './api-response.js';
|
|
9
10
|
const GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-simple-router/main/config';
|
|
10
11
|
const GITEE_CONFIG_BASE = 'https://gitee.com/zzzzswszzzz/llm-simple-router/raw/main/config';
|
|
11
12
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // eslint-disable-line no-magic-numbers
|
|
@@ -48,7 +49,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
48
49
|
app.put('/admin/api/upgrade/sync-source', async (req, reply) => {
|
|
49
50
|
const { source } = req.body;
|
|
50
51
|
if (source !== 'github' && source !== 'gitee') {
|
|
51
|
-
return reply.code(HTTP_BAD_REQUEST).send(
|
|
52
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'source must be github or gitee'));
|
|
52
53
|
}
|
|
53
54
|
setConfigSyncSource(db, source);
|
|
54
55
|
return reply.send({ ok: true });
|
|
@@ -56,14 +57,14 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
56
57
|
app.post('/admin/api/upgrade/execute', async (req, reply) => {
|
|
57
58
|
const deployment = detectDeployment();
|
|
58
59
|
if (deployment !== 'npm') {
|
|
59
|
-
return reply.code(HTTP_BAD_REQUEST).send(
|
|
60
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, '仅支持 npm 全局安装模式下自动升级'));
|
|
60
61
|
}
|
|
61
62
|
const { version } = req.body;
|
|
62
63
|
if (!version) {
|
|
63
|
-
return reply.code(HTTP_BAD_REQUEST).send(
|
|
64
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'version is required'));
|
|
64
65
|
}
|
|
65
66
|
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
66
|
-
return reply.code(HTTP_BAD_REQUEST).send(
|
|
67
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, '无效版本号格式'));
|
|
67
68
|
}
|
|
68
69
|
try {
|
|
69
70
|
execSync(`npm install -g llm-simple-router@${version}`, {
|
|
@@ -74,13 +75,13 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
74
75
|
}
|
|
75
76
|
catch (err) {
|
|
76
77
|
const msg = err instanceof Error ? err.message : String(err);
|
|
77
|
-
return reply.code(HTTP_INTERNAL_ERROR).send(
|
|
78
|
+
return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `升级失败: ${msg}`));
|
|
78
79
|
}
|
|
79
80
|
});
|
|
80
81
|
app.post('/admin/api/upgrade/sync-config', async (req, reply) => {
|
|
81
82
|
const { source } = req.body;
|
|
82
83
|
if (source !== 'github' && source !== 'gitee') {
|
|
83
|
-
return reply.code(HTTP_BAD_REQUEST).send(
|
|
84
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, 'source must be github or gitee'));
|
|
84
85
|
}
|
|
85
86
|
const base = getConfigBaseUrl(source);
|
|
86
87
|
const configDir = path.resolve(process.cwd(), 'config');
|
|
@@ -106,7 +107,7 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
106
107
|
}
|
|
107
108
|
catch (err) {
|
|
108
109
|
const msg = err instanceof Error ? err.message : String(err);
|
|
109
|
-
return reply.code(HTTP_INTERNAL_ERROR).send(
|
|
110
|
+
return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `同步失败: ${msg}`));
|
|
110
111
|
}
|
|
111
112
|
});
|
|
112
113
|
done();
|
package/dist/admin/usage.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveTimeRange } from "../utils/time-range.js";
|
|
4
4
|
const UsageQuerySchema = Type.Object({
|
|
5
5
|
router_key_id: Type.Optional(Type.String()),
|
|
6
6
|
});
|
|
7
|
-
function
|
|
8
|
-
const d = new Date(date);
|
|
9
|
-
const day = d.getDay();
|
|
10
|
-
// 周日 getDay()=0,需要回退到上周一;其余日期减到周一
|
|
11
|
-
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // eslint-disable-line no-magic-numbers
|
|
12
|
-
d.setDate(diff);
|
|
13
|
-
return d;
|
|
14
|
-
}
|
|
15
|
-
function getDailyUsage(db, start, end, routerKeyId) {
|
|
7
|
+
function getDailyUsage(db, startTime, endTime, routerKeyId) {
|
|
16
8
|
const routerKeyFilter = routerKeyId
|
|
17
9
|
? " AND rl.router_key_id = ?"
|
|
18
10
|
: "";
|
|
19
11
|
const params = routerKeyId
|
|
20
|
-
? [
|
|
21
|
-
: [
|
|
12
|
+
? [startTime, endTime, routerKeyId]
|
|
13
|
+
: [startTime, endTime];
|
|
22
14
|
return db.prepare(`
|
|
23
15
|
SELECT
|
|
24
16
|
date(rm.created_at) AS date,
|
|
@@ -39,11 +31,10 @@ export const adminUsageRoutes = (app, options, done) => {
|
|
|
39
31
|
const { db } = options;
|
|
40
32
|
app.get("/admin/api/usage/windows", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
41
33
|
const query = request.query;
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const windows = getWindowsInRange(db, toSqliteDatetime(today), toSqliteDatetime(tomorrow), query.router_key_id);
|
|
34
|
+
const range = resolveTimeRange("window", db, query.router_key_id);
|
|
35
|
+
const windows = getWindowsInRange(db, range.startTime, range.endTime, query.router_key_id);
|
|
36
|
+
if (windows.length === 0)
|
|
37
|
+
return [];
|
|
47
38
|
return windows.map(w => ({
|
|
48
39
|
window: w,
|
|
49
40
|
usage: getWindowUsage(db, w.start_time, w.end_time, query.router_key_id),
|
|
@@ -51,16 +42,13 @@ export const adminUsageRoutes = (app, options, done) => {
|
|
|
51
42
|
});
|
|
52
43
|
app.get("/admin/api/usage/weekly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
53
44
|
const query = request.query;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
monday.setHours(0, 0, 0, 0);
|
|
57
|
-
return getDailyUsage(db, monday, now, query.router_key_id);
|
|
45
|
+
const range = resolveTimeRange("weekly", db, query.router_key_id);
|
|
46
|
+
return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id);
|
|
58
47
|
});
|
|
59
48
|
app.get("/admin/api/usage/monthly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
60
49
|
const query = request.query;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
return getDailyUsage(db, firstOfMonth, now, query.router_key_id);
|
|
50
|
+
const range = resolveTimeRange("monthly", db, query.router_key_id);
|
|
51
|
+
return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id);
|
|
64
52
|
});
|
|
65
53
|
done();
|
|
66
54
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export interface Config {
|
|
|
4
4
|
LOG_LEVEL: string;
|
|
5
5
|
TZ: string;
|
|
6
6
|
STREAM_TIMEOUT_MS: number;
|
|
7
|
-
RETRY_MAX_ATTEMPTS: number;
|
|
8
7
|
RETRY_BASE_DELAY_MS: number;
|
|
9
8
|
}
|
|
10
9
|
export declare function resetConfig(): void;
|
|
11
10
|
export declare function getBaseConfig(): Config;
|
|
11
|
+
/** @deprecated Use getBaseConfig directly */
|
|
12
12
|
export declare function getConfig(): Config;
|
package/dist/config.js
CHANGED
|
@@ -18,11 +18,11 @@ export function getBaseConfig() {
|
|
|
18
18
|
LOG_LEVEL: process.env.LOG_LEVEL || "info",
|
|
19
19
|
TZ: process.env.TZ || "Asia/Shanghai",
|
|
20
20
|
STREAM_TIMEOUT_MS: parseInt(process.env.STREAM_TIMEOUT_MS || "3000000", 10),
|
|
21
|
-
RETRY_MAX_ATTEMPTS: parseInt(process.env.RETRY_MAX_ATTEMPTS || "3", 10),
|
|
22
21
|
RETRY_BASE_DELAY_MS: parseInt(process.env.RETRY_BASE_DELAY_MS || "1000", 10),
|
|
23
22
|
};
|
|
24
23
|
return cachedConfig;
|
|
25
24
|
}
|
|
25
|
+
/** @deprecated Use getBaseConfig directly */
|
|
26
26
|
export function getConfig() {
|
|
27
27
|
return getBaseConfig();
|
|
28
28
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,3 +6,6 @@ export declare const HTTP_CONFLICT = 409;
|
|
|
6
6
|
export declare const HTTP_INTERNAL_ERROR = 500;
|
|
7
7
|
export declare const HTTP_BAD_GATEWAY = 502;
|
|
8
8
|
export declare const HTTP_SERVICE_UNAVAILABLE = 503;
|
|
9
|
+
export declare const PROXY_API_TYPES: Record<string, string>;
|
|
10
|
+
export declare function getProxyApiType(url: string): string | null;
|
|
11
|
+
export declare const MS_PER_SECOND = 1000;
|
package/dist/constants.js
CHANGED
|
@@ -7,3 +7,14 @@ export const HTTP_CONFLICT = 409;
|
|
|
7
7
|
export const HTTP_INTERNAL_ERROR = 500;
|
|
8
8
|
export const HTTP_BAD_GATEWAY = 502;
|
|
9
9
|
export const HTTP_SERVICE_UNAVAILABLE = 503;
|
|
10
|
+
// api_type 路由映射:proxy path → api type,用于全局 hook/errorHandler 中识别代理请求
|
|
11
|
+
export const PROXY_API_TYPES = {
|
|
12
|
+
"/v1/chat/completions": "openai",
|
|
13
|
+
"/v1/models": "openai",
|
|
14
|
+
"/v1/messages": "anthropic",
|
|
15
|
+
};
|
|
16
|
+
export function getProxyApiType(url) {
|
|
17
|
+
const path = url.split("?")[0];
|
|
18
|
+
return PROXY_API_TYPES[path] ?? null;
|
|
19
|
+
}
|
|
20
|
+
export const MS_PER_SECOND = 1000;
|
package/dist/db/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
export declare function initDatabase(dbPath: string): Database.Database;
|
|
3
|
-
export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
3
|
+
export { getActiveProviders, getAllProviders, getProviderById, getActiveProviderByName, getActiveProvidersWithModels, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
4
4
|
export type { Provider } from "./providers.js";
|
|
5
5
|
export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
|
6
6
|
export type { ModelMapping, MappingGroup, ProviderModelEntry } from "./mappings.js";
|
|
7
|
-
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
7
|
+
export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
8
8
|
export type { RetryRule } from "./retry-rules.js";
|
|
9
9
|
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, estimateLogTableSize, deleteOldestLogs, getLogCount, } from "./logs.js";
|
|
10
10
|
export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
|
|
@@ -13,7 +13,7 @@ export type { RouterKey } from "./router-keys.js";
|
|
|
13
13
|
export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
|
|
14
14
|
export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric, MetricsRow, MetricsInsert } from "./metrics.js";
|
|
15
15
|
export { getStats } from "./stats.js";
|
|
16
|
-
export type { Stats
|
|
16
|
+
export type { Stats } from "./stats.js";
|
|
17
17
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
18
18
|
export { getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, } from "./settings.js";
|
|
19
19
|
export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
|
package/dist/db/index.js
CHANGED
|
@@ -53,9 +53,9 @@ export function initDatabase(dbPath) {
|
|
|
53
53
|
return db;
|
|
54
54
|
}
|
|
55
55
|
// --- Re-export from per-table modules ---
|
|
56
|
-
export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
56
|
+
export { getActiveProviders, getAllProviders, getProviderById, getActiveProviderByName, getActiveProvidersWithModels, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
57
57
|
export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
|
58
|
-
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
58
|
+
export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
59
59
|
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, estimateLogTableSize, deleteOldestLogs, getLogCount, } from "./logs.js";
|
|
60
60
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
61
61
|
export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
|
package/dist/db/mappings.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
+
import { isTarget } from "../proxy/strategy/targets-rule.js";
|
|
2
3
|
import { buildUpdateQuery, deleteById } from "./helpers.js";
|
|
3
4
|
const MAPPING_FIELDS = new Set(["client_model", "backend_model", "provider_id", "is_active"]);
|
|
4
5
|
const GROUP_FIELDS = new Set(["client_model", "strategy", "rule"]);
|
|
@@ -70,24 +71,20 @@ export function getActiveProviderModels(db) {
|
|
|
70
71
|
}
|
|
71
72
|
return results;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
return typeof obj === "object" && obj !== null &&
|
|
75
|
-
typeof obj.backend_model === "string" &&
|
|
76
|
-
typeof obj.provider_id === "string";
|
|
77
|
-
}
|
|
74
|
+
// --- 从 mapping_groups rule JSON 中提取 target 条目 ---
|
|
78
75
|
function extractTargets(rule) {
|
|
79
76
|
const results = [];
|
|
80
|
-
if (
|
|
77
|
+
if (isTarget(rule.default))
|
|
81
78
|
results.push(rule.default);
|
|
82
79
|
if (Array.isArray(rule.targets)) {
|
|
83
80
|
for (const t of rule.targets) {
|
|
84
|
-
if (
|
|
81
|
+
if (isTarget(t))
|
|
85
82
|
results.push(t);
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
85
|
if (Array.isArray(rule.windows)) {
|
|
89
86
|
for (const w of rule.windows) {
|
|
90
|
-
if (w && typeof w === "object" &&
|
|
87
|
+
if (w && typeof w === "object" && isTarget(w.target)) {
|
|
91
88
|
results.push(w.target);
|
|
92
89
|
}
|
|
93
90
|
}
|
package/dist/db/metrics.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
+
import { MS_PER_SECOND } from "../constants.js";
|
|
2
3
|
export function insertMetrics(db, m) {
|
|
3
4
|
const id = randomUUID();
|
|
4
5
|
db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
|
|
@@ -21,8 +22,6 @@ const BUCKET_SECONDS = {
|
|
|
21
22
|
"7d": 3600,
|
|
22
23
|
"30d": 14400,
|
|
23
24
|
};
|
|
24
|
-
// unix epoch 秒转毫秒的乘数
|
|
25
|
-
const MS_PER_SEC = 1000;
|
|
26
25
|
// 时间跨度(秒)→ 桶大小(秒)的阶梯映射,与 BUCKET_SECONDS 保持对齐
|
|
27
26
|
const BUCKET_THRESHOLDS = [
|
|
28
27
|
{ maxSec: 3600, bucketSec: 60 }, // ≤1h: 1min
|
|
@@ -33,7 +32,7 @@ const BUCKET_THRESHOLDS = [
|
|
|
33
32
|
const FALLBACK_BUCKET_SEC = 14400; // >7d: 4h
|
|
34
33
|
function calculateBucketSeconds(startTime, endTime) {
|
|
35
34
|
const ms = new Date(endTime).getTime() - new Date(startTime).getTime();
|
|
36
|
-
const sec = ms /
|
|
35
|
+
const sec = ms / MS_PER_SECOND;
|
|
37
36
|
const match = BUCKET_THRESHOLDS.find((t) => sec <= t.maxSec);
|
|
38
37
|
return match ? match.bucketSec : FALLBACK_BUCKET_SEC;
|
|
39
38
|
}
|
|
@@ -127,7 +126,7 @@ export function getMetricsTimeseries(db, period, metric, providerId, backendMode
|
|
|
127
126
|
ORDER BY bucket_key ASC
|
|
128
127
|
`).all(bucketSec, bucketSec, ...params);
|
|
129
128
|
return rows.map((r) => ({
|
|
130
|
-
time_bucket: new Date(r.bucket_key *
|
|
129
|
+
time_bucket: new Date(r.bucket_key * MS_PER_SECOND).toISOString(),
|
|
131
130
|
avg_value: r.avg_value,
|
|
132
131
|
count: r.count,
|
|
133
132
|
}));
|
package/dist/db/providers.d.ts
CHANGED
|
@@ -36,3 +36,11 @@ export declare function createProvider(db: Database.Database, provider: {
|
|
|
36
36
|
}): string;
|
|
37
37
|
export declare function updateProvider(db: Database.Database, id: string, fields: Partial<Pick<Provider, "name" | "api_type" | "base_url" | "api_key" | "api_key_preview" | "models" | "is_active" | "max_concurrency" | "queue_timeout_ms" | "max_queue_size">>): void;
|
|
38
38
|
export declare function deleteProvider(db: Database.Database, id: string): void;
|
|
39
|
+
export declare function getActiveProviderByName(db: Database.Database, name: string): {
|
|
40
|
+
id: string;
|
|
41
|
+
models: string;
|
|
42
|
+
} | undefined;
|
|
43
|
+
export declare function getActiveProvidersWithModels(db: Database.Database): {
|
|
44
|
+
id: string;
|
|
45
|
+
models: string;
|
|
46
|
+
}[];
|
package/dist/db/providers.js
CHANGED
|
@@ -32,3 +32,9 @@ export function updateProvider(db, id, fields) {
|
|
|
32
32
|
export function deleteProvider(db, id) {
|
|
33
33
|
deleteById(db, "providers", id);
|
|
34
34
|
}
|
|
35
|
+
export function getActiveProviderByName(db, name) {
|
|
36
|
+
return db.prepare("SELECT id, models FROM providers WHERE name = ? AND is_active = 1").get(name);
|
|
37
|
+
}
|
|
38
|
+
export function getActiveProvidersWithModels(db) {
|
|
39
|
+
return db.prepare("SELECT id, models FROM providers WHERE is_active = 1").all();
|
|
40
|
+
}
|
package/dist/db/retry-rules.d.ts
CHANGED
|
@@ -25,3 +25,4 @@ export declare function createRetryRule(db: Database.Database, rule: {
|
|
|
25
25
|
}): string;
|
|
26
26
|
export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms">>): void;
|
|
27
27
|
export declare function deleteRetryRule(db: Database.Database, id: string): void;
|
|
28
|
+
export declare function getRetryRuleById(db: Database.Database, id: string): RetryRule | undefined;
|
package/dist/db/retry-rules.js
CHANGED
package/dist/db/stats.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
-
export type StatsPeriod = "1h" | "6h" | "24h" | "7d" | "30d";
|
|
3
2
|
export interface Stats {
|
|
4
3
|
totalRequests: number;
|
|
5
4
|
successRate: number;
|
|
6
5
|
avgTps: number;
|
|
7
6
|
totalTokens: number;
|
|
8
7
|
}
|
|
9
|
-
export declare function getStats(db: Database.Database,
|
|
8
|
+
export declare function getStats(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string): Stats;
|
package/dist/db/stats.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export function getStats(db, period, routerKeyId) {
|
|
9
|
-
const offset = PERIOD_OFFSET[period];
|
|
10
|
-
const conditions = ["rm.is_complete = 1", "rm.created_at >= datetime('now', ?)"];
|
|
11
|
-
const params = [offset];
|
|
1
|
+
export function getStats(db, startTime, endTime, routerKeyId) {
|
|
2
|
+
const conditions = [
|
|
3
|
+
"rm.is_complete = 1",
|
|
4
|
+
"rm.created_at >= datetime(?)",
|
|
5
|
+
"rm.created_at < datetime(?)",
|
|
6
|
+
];
|
|
7
|
+
const params = [startTime, endTime];
|
|
12
8
|
if (routerKeyId) {
|
|
13
9
|
conditions.push("rl.router_key_id = ?");
|
|
14
10
|
params.push(routerKeyId);
|
package/dist/index.js
CHANGED
|
@@ -5,19 +5,10 @@ import { existsSync } from "node:fs";
|
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
6
|
import Fastify from "fastify";
|
|
7
7
|
import { insertRequestLog } from "./db/logs.js";
|
|
8
|
-
import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR,
|
|
8
|
+
import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, getProxyApiType } from "./constants.js";
|
|
9
|
+
import { API_CODE, apiError, isAdminApiResponse, statusToApiCode } from "./admin/api-response.js";
|
|
9
10
|
const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
|
|
10
11
|
const PROVIDER_DEFAULT_MAX_QUEUE_SIZE = 100;
|
|
11
|
-
// 代理路由路径 → api_type,用于在全局 hook/errorHandler 中识别代理请求
|
|
12
|
-
const PROXY_API_TYPES = {
|
|
13
|
-
"/v1/chat/completions": "openai",
|
|
14
|
-
"/v1/messages": "anthropic",
|
|
15
|
-
"/v1/models": "openai",
|
|
16
|
-
};
|
|
17
|
-
function getProxyApiType(url) {
|
|
18
|
-
const path = url.split("?")[0];
|
|
19
|
-
return PROXY_API_TYPES[path] ?? null;
|
|
20
|
-
}
|
|
21
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
13
|
const __dirname = path.dirname(__filename);
|
|
23
14
|
import { getConfig } from "./config.js";
|
|
@@ -80,32 +71,59 @@ export async function buildApp(options) {
|
|
|
80
71
|
.join("; ");
|
|
81
72
|
return new Error(message);
|
|
82
73
|
});
|
|
83
|
-
//
|
|
74
|
+
// 统一错误处理:代理路由保持 {error:{message}},Admin API 使用信封格式
|
|
84
75
|
app.setErrorHandler((error, request, reply) => {
|
|
85
76
|
const fastifyError = error;
|
|
86
77
|
const status = fastifyError.statusCode ?? HTTP_INTERNAL_ERROR;
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
78
|
+
// 代理路由保持原有格式,并记录到 request_logs
|
|
79
|
+
if (!isAdminApiResponse(request.url)) {
|
|
80
|
+
const proxyApiType = getProxyApiType(request.url);
|
|
81
|
+
if (proxyApiType) {
|
|
82
|
+
request.log.error({ statusCode: status, err: error }, `Proxy request error: ${fastifyError.message}`);
|
|
83
|
+
const body = request.body;
|
|
84
|
+
insertRequestLog(db, {
|
|
85
|
+
id: randomUUID(),
|
|
86
|
+
api_type: proxyApiType,
|
|
87
|
+
model: body?.model || null,
|
|
88
|
+
provider_id: null,
|
|
89
|
+
status_code: status,
|
|
90
|
+
latency_ms: 0,
|
|
91
|
+
is_stream: 0,
|
|
92
|
+
error_message: fastifyError.message,
|
|
93
|
+
created_at: new Date().toISOString(),
|
|
94
|
+
client_request: JSON.stringify({ headers: request.headers }),
|
|
95
|
+
router_key_id: request.routerKey?.id ?? null,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return reply.code(status).send({ error: { message: fastifyError.message } });
|
|
104
99
|
}
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
// Admin API — 统一信封错误格式
|
|
101
|
+
const code = statusToApiCode(status);
|
|
102
|
+
return reply.code(status).send(apiError(code, fastifyError.message));
|
|
103
|
+
});
|
|
104
|
+
// onSend hook:自动包装 Admin API 成功响应为信封格式
|
|
105
|
+
app.addHook('onSend', async (request, reply, payload) => {
|
|
106
|
+
if (!isAdminApiResponse(request.url, reply.getHeader('content-type'))) {
|
|
107
|
+
return payload;
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
// 已是错误信封(errorHandler 已包装)或已是信封格式 — 跳过
|
|
110
|
+
if (typeof payload === 'string') {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(payload);
|
|
113
|
+
if ('code' in parsed)
|
|
114
|
+
return payload; // errorHandler 或路由已手动包装
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return payload;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// 包装成功响应
|
|
121
|
+
const wrapped = {
|
|
122
|
+
code: API_CODE.SUCCESS,
|
|
123
|
+
message: 'ok',
|
|
124
|
+
data: typeof payload === 'string' ? JSON.parse(payload) : payload,
|
|
125
|
+
};
|
|
126
|
+
return JSON.stringify(wrapped);
|
|
109
127
|
});
|
|
110
128
|
loadRecommendedConfig();
|
|
111
129
|
startUpgradeChecker(options?.upgradeCheckerOptions);
|
|
@@ -147,20 +165,20 @@ export async function buildApp(options) {
|
|
|
147
165
|
app.register(openaiProxy, {
|
|
148
166
|
db,
|
|
149
167
|
streamTimeoutMs: config.STREAM_TIMEOUT_MS,
|
|
150
|
-
retryMaxAttempts: config.RETRY_MAX_ATTEMPTS,
|
|
151
168
|
retryBaseDelayMs: config.RETRY_BASE_DELAY_MS,
|
|
152
169
|
matcher,
|
|
153
170
|
semaphoreManager,
|
|
154
171
|
tracker,
|
|
172
|
+
usageWindowTracker,
|
|
155
173
|
});
|
|
156
174
|
app.register(anthropicProxy, {
|
|
157
175
|
db,
|
|
158
176
|
streamTimeoutMs: config.STREAM_TIMEOUT_MS,
|
|
159
|
-
retryMaxAttempts: config.RETRY_MAX_ATTEMPTS,
|
|
160
177
|
retryBaseDelayMs: config.RETRY_BASE_DELAY_MS,
|
|
161
178
|
matcher,
|
|
162
179
|
semaphoreManager,
|
|
163
180
|
tracker,
|
|
181
|
+
usageWindowTracker,
|
|
164
182
|
});
|
|
165
183
|
app.register(adminRoutes, { db, matcher, tracker, semaphoreManager });
|
|
166
184
|
// 前端静态文件服务(生产环境)
|
|
@@ -3,6 +3,7 @@ import cookie from "@fastify/cookie";
|
|
|
3
3
|
import jwt from "jsonwebtoken";
|
|
4
4
|
import { isInitialized, getSetting } from "../db/settings.js";
|
|
5
5
|
import { verifyPassword } from "../utils/password.js";
|
|
6
|
+
import { API_CODE, apiError } from "../admin/api-response.js";
|
|
6
7
|
const HTTP_UNAUTHORIZED = 401;
|
|
7
8
|
const adminAuthRaw = (app, options, done) => {
|
|
8
9
|
app.register(cookie);
|
|
@@ -19,11 +20,11 @@ const adminAuthRaw = (app, options, done) => {
|
|
|
19
20
|
return;
|
|
20
21
|
// 未初始化时返回 needsSetup
|
|
21
22
|
if (!isInitialized(options.db)) {
|
|
22
|
-
return reply.code(HTTP_UNAUTHORIZED).send(
|
|
23
|
+
return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.NOT_INITIALIZED, "Not initialized"));
|
|
23
24
|
}
|
|
24
25
|
const token = request.cookies["admin_token"];
|
|
25
26
|
if (!token) {
|
|
26
|
-
reply.code(HTTP_UNAUTHORIZED).send(
|
|
27
|
+
reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.TOKEN_INVALID, "Not authenticated"));
|
|
27
28
|
return reply;
|
|
28
29
|
}
|
|
29
30
|
const secret = getSetting(options.db, "jwt_secret");
|
|
@@ -32,7 +33,7 @@ const adminAuthRaw = (app, options, done) => {
|
|
|
32
33
|
}
|
|
33
34
|
catch (err) {
|
|
34
35
|
request.log.debug({ err }, "invalid JWT token");
|
|
35
|
-
reply.code(HTTP_UNAUTHORIZED).send(
|
|
36
|
+
reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.TOKEN_INVALID, "Invalid or expired token"));
|
|
36
37
|
return reply;
|
|
37
38
|
}
|
|
38
39
|
});
|
|
@@ -44,12 +45,12 @@ export const adminLoginRoutes = (app, options, done) => {
|
|
|
44
45
|
app.post("/admin/api/login", async (request, reply) => {
|
|
45
46
|
const { password } = request.body;
|
|
46
47
|
if (!password) {
|
|
47
|
-
return reply.code(HTTP_UNAUTHORIZED).send(
|
|
48
|
+
return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.WRONG_PASSWORD, "Invalid password"));
|
|
48
49
|
}
|
|
49
50
|
// DB 模式:scrypt hash 验证
|
|
50
51
|
const hash = getSetting(options.db, "admin_password_hash");
|
|
51
52
|
if (!hash || !verifyPassword(password, hash)) {
|
|
52
|
-
return reply.code(HTTP_UNAUTHORIZED).send(
|
|
53
|
+
return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.WRONG_PASSWORD, "Invalid password"));
|
|
53
54
|
}
|
|
54
55
|
const secret = getSetting(options.db, "jwt_secret");
|
|
55
56
|
const token = jwt.sign({ role: "admin" }, secret, { expiresIn: TOKEN_EXPIRY_SECONDS });
|
package/dist/middleware/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "crypto";
|
|
|
2
2
|
import fp from "fastify-plugin";
|
|
3
3
|
import { isInitialized } from "../db/settings.js";
|
|
4
4
|
import { insertRequestLog } from "../db/logs.js";
|
|
5
|
+
import { getProxyApiType } from "../constants.js";
|
|
5
6
|
const SKIP_PATHS = ["/health", "/admin"];
|
|
6
7
|
const HTTP_UNAUTHORIZED = 401;
|
|
7
8
|
const HTTP_SERVICE_UNAVAILABLE = 503;
|
|
@@ -19,16 +20,6 @@ function unauthorizedReply(reply) {
|
|
|
19
20
|
},
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
|
-
// 代理路由路径 → api_type 映射,用于记录被认证拒绝的请求
|
|
23
|
-
const PROXY_API_TYPES = {
|
|
24
|
-
"/v1/chat/completions": "openai",
|
|
25
|
-
"/v1/messages": "anthropic",
|
|
26
|
-
"/v1/models": "openai",
|
|
27
|
-
};
|
|
28
|
-
function getProxyApiType(url) {
|
|
29
|
-
const path = url.split("?")[0];
|
|
30
|
-
return PROXY_API_TYPES[path] ?? null;
|
|
31
|
-
}
|
|
32
23
|
function logRejectedAuth(db, apiType, statusCode, errorMessage, request) {
|
|
33
24
|
insertRequestLog(db, {
|
|
34
25
|
id: randomUUID(),
|