llm-simple-router 0.5.5 → 0.5.6
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/metrics.js +4 -4
- package/dist/admin/stats.js +5 -3
- package/dist/admin/usage.js +40 -19
- package/dist/db/metrics.d.ts +2 -0
- package/dist/db/metrics.js +4 -7
- package/dist/db/migrations/026_metrics_independent.sql +54 -0
- package/dist/db/stats.d.ts +3 -2
- package/dist/db/stats.js +15 -6
- package/dist/db/usage-windows.d.ts +5 -4
- package/dist/db/usage-windows.js +48 -20
- package/dist/proxy/proxy-handler.js +11 -2
- package/dist/proxy/proxy-logging.d.ts +1 -1
- package/dist/proxy/proxy-logging.js +5 -2
- package/dist/proxy/usage-window-tracker.d.ts +5 -3
- package/dist/proxy/usage-window-tracker.js +21 -22
- package/dist/utils/time-range.d.ts +1 -1
- package/dist/utils/time-range.js +13 -7
- package/frontend-dist/assets/{CardContent-CVofwD9T.js → CardContent-ByybpNZM.js} +1 -1
- package/frontend-dist/assets/{CardTitle-MLH-EpHz.js → CardTitle-Cv39_iQu.js} +1 -1
- package/frontend-dist/assets/{Checkbox-KiNnjeB_.js → Checkbox-F8_Gy_s5.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-CbiBtEE8.js → CollapsibleTrigger-BPzLViBo.js} +1 -1
- package/frontend-dist/assets/{Collection-D9NeXDOI.js → Collection-Dafpcl-w.js} +1 -1
- package/frontend-dist/assets/Dashboard-BTwf4ZtI.js +3 -0
- package/frontend-dist/assets/{DialogTitle-CTcpSq_w.js → DialogTitle-BMNhmnin.js} +1 -1
- package/frontend-dist/assets/{Input-DDF6744B.js → Input-BkzqSK7i.js} +1 -1
- package/frontend-dist/assets/{Label-_cw0EkS1.js → Label-DwqBcp6d.js} +1 -1
- package/frontend-dist/assets/{Login-Cca1HpLA.js → Login-B7sSST00.js} +1 -1
- package/frontend-dist/assets/Logs-DJc0hZ8C.js +1 -0
- package/frontend-dist/assets/ModelMappings-BavaEbnL.js +1 -0
- package/frontend-dist/assets/{Monitor-BXpf9sjE.js → Monitor-B4hCGdS-.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-DIOWDISK.js → PopoverTrigger-vgVugQwU.js} +1 -1
- package/frontend-dist/assets/{PopperContent-DIEU7Y6E.js → PopperContent-tf2A4fsa.js} +1 -1
- package/frontend-dist/assets/{Providers-CvYtuvfg.js → Providers-BmrsbthR.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-BxTbZGYB.js → ProxyEnhancement-BQ02PeEF.js} +1 -1
- package/frontend-dist/assets/{RetryRules-COK28WDb.js → RetryRules-H460Dyek.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-DH7nUWDa.js → RouterKeys-D8rXsmpq.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-DyNl4G42.js → RovingFocusItem-Bo0dNNmj.js} +1 -1
- package/frontend-dist/assets/{SelectValue-DyjOpyaF.js → SelectValue-BU16UnrX.js} +1 -1
- package/frontend-dist/assets/{Settings-BmG999ol.js → Settings-YiR7zqua.js} +1 -1
- package/frontend-dist/assets/{Setup-D1xUsqUD.js → Setup-D7EXZ1Nv.js} +1 -1
- package/frontend-dist/assets/{Switch-DrDIdQ_V.js → Switch-CA_wdlEs.js} +1 -1
- package/frontend-dist/assets/{TableHeader-BNPqkg9S.js → TableHeader-BuObvzlS.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-D-fGrdG3.js → TabsTrigger-DZIFRVA_.js} +1 -1
- package/frontend-dist/assets/{Teleport-BYzsRHC6.js → Teleport-FxAUQAZT.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-C3v2O7fX.js → TooltipTrigger-D9nCGsBG.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-DJOIbAUb.js → UnifiedRequestDialog-BPv5B17F.js} +1 -1
- package/frontend-dist/assets/{VisuallyHidden-Bz0LPzC7.js → VisuallyHidden-clBSgYdG.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-Bf9hgsd7.js → VisuallyHiddenInput-CmDbYWUO.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-Bn1IauFR.js → alert-dialog-BvbdNhnK.js} +1 -1
- package/frontend-dist/assets/{badge-dyL-tG0V.js → badge-CcCt1-ig.js} +1 -1
- package/frontend-dist/assets/{button-B6f3Nfab.js → button-DeOsxcjG.js} +2 -2
- package/frontend-dist/assets/chevron-down-D_DCDFPY.js +1 -0
- package/frontend-dist/assets/{dialog-1FvPG-71.js → dialog-CPO2KcC1.js} +1 -1
- package/frontend-dist/assets/{file-text-BFyk5DT1.js → file-text-C-6LFEhP.js} +1 -1
- package/frontend-dist/assets/index-DHONWydQ.css +1 -0
- package/frontend-dist/assets/{index-B2SjbR82.js → index-DW58MMV6.js} +1 -1
- package/frontend-dist/assets/{lib-C_27TgFv.js → lib-DkM_rWnj.js} +1 -1
- package/frontend-dist/assets/loader-circle-BS4uI1Z4.js +1 -0
- package/frontend-dist/assets/{ohash.D__AXeF1-BcwqVPVj.js → ohash.D__AXeF1-CBYQgVou.js} +1 -1
- package/frontend-dist/assets/{useClipboard-DCoGBvxy.js → useClipboard-NBCgpr6Z.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-BGcJntJ-.js → useLogRetention-B_u8u74J.js} +1 -1
- package/frontend-dist/assets/useNonce-D1dqoOZO.js +1 -0
- package/frontend-dist/assets/x-DVLhwc3Q.js +1 -0
- package/frontend-dist/index.html +19 -19
- package/package.json +2 -2
- package/frontend-dist/assets/Dashboard-CuBkXd58.js +0 -3
- package/frontend-dist/assets/Logs-CUIPRZEO.js +0 -1
- package/frontend-dist/assets/ModelMappings-DEswiX1e.js +0 -1
- package/frontend-dist/assets/chevron-down-DKTxr1tk.js +0 -1
- package/frontend-dist/assets/circle-question-mark-WJzB-je7.js +0 -1
- package/frontend-dist/assets/index-uA-D1xVT.css +0 -1
- package/frontend-dist/assets/loader-circle-DEqD4BxX.js +0 -1
- package/frontend-dist/assets/useNonce-Ccld5giw.js +0 -1
- package/frontend-dist/assets/x-iwmjHBXS.js +0 -1
package/dist/admin/metrics.js
CHANGED
|
@@ -33,13 +33,13 @@ const TimeseriesQuerySchema = Type.Object({
|
|
|
33
33
|
end_time: Type.Optional(Type.String()),
|
|
34
34
|
});
|
|
35
35
|
const DASHBOARD_PERIODS = new Set(["window", "weekly", "monthly"]);
|
|
36
|
-
function resolveMetricsTime(query, db, routerKeyId) {
|
|
36
|
+
function resolveMetricsTime(query, db, routerKeyId, providerId) {
|
|
37
37
|
if (query.start_time && query.end_time) {
|
|
38
38
|
return { startTime: query.start_time, endTime: query.end_time, legacyPeriod: "30d" };
|
|
39
39
|
}
|
|
40
40
|
const period = query.period ?? "weekly";
|
|
41
41
|
if (DASHBOARD_PERIODS.has(period)) {
|
|
42
|
-
const range = resolveTimeRange(period, db, routerKeyId);
|
|
42
|
+
const range = resolveTimeRange(period, db, routerKeyId, providerId);
|
|
43
43
|
return { startTime: range.startTime, endTime: range.endTime, legacyPeriod: "5h" };
|
|
44
44
|
}
|
|
45
45
|
return { legacyPeriod: period };
|
|
@@ -48,14 +48,14 @@ export const adminMetricsRoutes = (app, options, done) => {
|
|
|
48
48
|
const { db } = options;
|
|
49
49
|
app.get("/admin/api/metrics/summary", { schema: { querystring: SummaryQuerySchema } }, async (request, reply) => {
|
|
50
50
|
const query = request.query;
|
|
51
|
-
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id);
|
|
51
|
+
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id, query.provider_id);
|
|
52
52
|
const summary = getMetricsSummary(db, legacyPeriod, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
|
|
53
53
|
return reply.send(summary);
|
|
54
54
|
});
|
|
55
55
|
app.get("/admin/api/metrics/timeseries", { schema: { querystring: TimeseriesQuerySchema } }, async (request, reply) => {
|
|
56
56
|
const query = request.query;
|
|
57
57
|
const metric = query.metric;
|
|
58
|
-
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id);
|
|
58
|
+
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id, query.provider_id);
|
|
59
59
|
const timeseries = getMetricsTimeseries(db, legacyPeriod, metric, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
|
|
60
60
|
return reply.send(timeseries);
|
|
61
61
|
});
|
package/dist/admin/stats.js
CHANGED
|
@@ -10,6 +10,8 @@ const StatsQuerySchema = Type.Object({
|
|
|
10
10
|
start_time: Type.Optional(Type.String()),
|
|
11
11
|
end_time: Type.Optional(Type.String()),
|
|
12
12
|
router_key_id: Type.Optional(Type.String()),
|
|
13
|
+
provider_id: Type.Optional(Type.String()),
|
|
14
|
+
backend_model: Type.Optional(Type.String()),
|
|
13
15
|
});
|
|
14
16
|
export const adminStatsRoutes = (app, options, done) => {
|
|
15
17
|
app.get("/admin/api/stats", { schema: { querystring: StatsQuerySchema } }, async (request, reply) => {
|
|
@@ -21,12 +23,12 @@ export const adminStatsRoutes = (app, options, done) => {
|
|
|
21
23
|
endTime = query.end_time;
|
|
22
24
|
}
|
|
23
25
|
else {
|
|
24
|
-
const range = resolveTimeRange((query.period ?? "weekly"), options.db, query.router_key_id);
|
|
26
|
+
const range = resolveTimeRange((query.period ?? "weekly"), options.db, query.router_key_id, query.provider_id);
|
|
25
27
|
startTime = range.startTime;
|
|
26
28
|
endTime = range.endTime;
|
|
27
29
|
}
|
|
28
|
-
const stats = getStats(options.db, startTime, endTime, query.router_key_id);
|
|
29
|
-
return reply.send(stats);
|
|
30
|
+
const stats = getStats(options.db, startTime, endTime, query.router_key_id, query.provider_id, query.backend_model);
|
|
31
|
+
return reply.send({ ...stats, startTime, endTime });
|
|
30
32
|
});
|
|
31
33
|
done();
|
|
32
34
|
};
|
package/dist/admin/usage.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
|
|
3
|
+
import { getProviderById } from "../db/providers.js";
|
|
3
4
|
import { resolveTimeRange } from "../utils/time-range.js";
|
|
4
5
|
const UsageQuerySchema = Type.Object({
|
|
5
6
|
router_key_id: Type.Optional(Type.String()),
|
|
7
|
+
provider_id: Type.Optional(Type.String()),
|
|
6
8
|
});
|
|
7
|
-
function getDailyUsage(db, startTime, endTime, routerKeyId) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
function getDailyUsage(db, startTime, endTime, routerKeyId, providerId) {
|
|
10
|
+
const conditions = [
|
|
11
|
+
"rm.is_complete = 1",
|
|
12
|
+
"rm.created_at >= datetime(?)",
|
|
13
|
+
"rm.created_at < datetime(?)",
|
|
14
|
+
];
|
|
15
|
+
const params = [startTime, endTime];
|
|
16
|
+
if (routerKeyId) {
|
|
17
|
+
conditions.push("rm.router_key_id = ?");
|
|
18
|
+
params.push(routerKeyId);
|
|
19
|
+
}
|
|
20
|
+
if (providerId) {
|
|
21
|
+
conditions.push("rm.provider_id = ?");
|
|
22
|
+
params.push(providerId);
|
|
23
|
+
}
|
|
14
24
|
return db.prepare(`
|
|
15
25
|
SELECT
|
|
16
26
|
date(rm.created_at) AS date,
|
|
@@ -18,37 +28,48 @@ function getDailyUsage(db, startTime, endTime, routerKeyId) {
|
|
|
18
28
|
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
|
|
19
29
|
COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
|
|
20
30
|
FROM request_metrics rm
|
|
21
|
-
|
|
22
|
-
WHERE rm.is_complete = 1
|
|
23
|
-
AND rm.created_at >= datetime(?)
|
|
24
|
-
AND rm.created_at < datetime(?)
|
|
25
|
-
${routerKeyFilter}
|
|
31
|
+
WHERE ${conditions.join(" AND ")}
|
|
26
32
|
GROUP BY date(rm.created_at)
|
|
27
33
|
ORDER BY date ASC
|
|
28
34
|
`).all(...params);
|
|
29
35
|
}
|
|
36
|
+
function resolveProviderName(db, providerId) {
|
|
37
|
+
if (!providerId)
|
|
38
|
+
return null;
|
|
39
|
+
return getProviderById(db, providerId)?.name ?? null;
|
|
40
|
+
}
|
|
30
41
|
export const adminUsageRoutes = (app, options, done) => {
|
|
31
42
|
const { db } = options;
|
|
32
43
|
app.get("/admin/api/usage/windows", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
33
44
|
const query = request.query;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
if (query.provider_id) {
|
|
46
|
+
const range = resolveTimeRange("window", db, query.router_key_id, query.provider_id);
|
|
47
|
+
const windows = getWindowsInRange(db, range.startTime, range.endTime, query.router_key_id, query.provider_id);
|
|
48
|
+
if (windows.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
return windows.map(w => ({
|
|
51
|
+
window: { ...w, provider_name: resolveProviderName(db, w.provider_id) },
|
|
52
|
+
usage: getWindowUsage(db, w.start_time, w.end_time, query.router_key_id, query.provider_id),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
const allWindows = getWindowsInRange(db, "1970-01-01", "2099-12-31", query.router_key_id)
|
|
56
|
+
.filter((w) => w.provider_id !== null);
|
|
57
|
+
if (allWindows.length === 0)
|
|
37
58
|
return [];
|
|
38
|
-
return
|
|
39
|
-
window: w,
|
|
59
|
+
return allWindows.map(w => ({
|
|
60
|
+
window: { ...w, provider_name: resolveProviderName(db, w.provider_id) },
|
|
40
61
|
usage: getWindowUsage(db, w.start_time, w.end_time, query.router_key_id),
|
|
41
62
|
}));
|
|
42
63
|
});
|
|
43
64
|
app.get("/admin/api/usage/weekly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
44
65
|
const query = request.query;
|
|
45
66
|
const range = resolveTimeRange("weekly", db, query.router_key_id);
|
|
46
|
-
return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id);
|
|
67
|
+
return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id, query.provider_id);
|
|
47
68
|
});
|
|
48
69
|
app.get("/admin/api/usage/monthly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
49
70
|
const query = request.query;
|
|
50
71
|
const range = resolveTimeRange("monthly", db, query.router_key_id);
|
|
51
|
-
return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id);
|
|
72
|
+
return getDailyUsage(db, range.startTime, range.endTime, query.router_key_id, query.provider_id);
|
|
52
73
|
});
|
|
53
74
|
done();
|
|
54
75
|
};
|
package/dist/db/metrics.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type MetricsInsert = {
|
|
|
23
23
|
provider_id: string;
|
|
24
24
|
backend_model: string;
|
|
25
25
|
api_type: string;
|
|
26
|
+
router_key_id?: string | null;
|
|
27
|
+
status_code?: number | null;
|
|
26
28
|
input_tokens?: number | null;
|
|
27
29
|
output_tokens?: number | null;
|
|
28
30
|
cache_creation_tokens?: number | null;
|
package/dist/db/metrics.js
CHANGED
|
@@ -2,8 +2,8 @@ import { randomUUID } from "crypto";
|
|
|
2
2
|
import { MS_PER_SECOND } from "../constants.js";
|
|
3
3
|
export function insertMetrics(db, m) {
|
|
4
4
|
const id = randomUUID();
|
|
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)
|
|
6
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.input_tokens ?? null, m.output_tokens ?? null, m.cache_creation_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.total_duration_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, m.is_complete ?? 1);
|
|
5
|
+
db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, router_key_id, status_code, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
|
|
6
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.router_key_id ?? null, m.status_code ?? null, m.input_tokens ?? null, m.output_tokens ?? null, m.cache_creation_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.total_duration_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, m.is_complete ?? 1);
|
|
7
7
|
return id;
|
|
8
8
|
}
|
|
9
9
|
const PERIOD_OFFSET = {
|
|
@@ -64,8 +64,7 @@ export function getMetricsSummary(db, period, providerId, backendModel, routerKe
|
|
|
64
64
|
params.push(backendModel);
|
|
65
65
|
}
|
|
66
66
|
if (routerKeyId) {
|
|
67
|
-
|
|
68
|
-
conditions.push("rl.router_key_id = ?");
|
|
67
|
+
conditions.push("rm.router_key_id = ?");
|
|
69
68
|
params.push(routerKeyId);
|
|
70
69
|
}
|
|
71
70
|
return db.prepare(`
|
|
@@ -108,19 +107,17 @@ export function getMetricsTimeseries(db, period, metric, providerId, backendMode
|
|
|
108
107
|
params.push(backendModel);
|
|
109
108
|
}
|
|
110
109
|
if (routerKeyId) {
|
|
111
|
-
conditions.push("
|
|
110
|
+
conditions.push("rm.router_key_id = ?");
|
|
112
111
|
params.push(routerKeyId);
|
|
113
112
|
}
|
|
114
113
|
const where = conditions.join(" AND ");
|
|
115
114
|
const expr = METRIC_EXPR[metric];
|
|
116
|
-
const joinClause = routerKeyId ? "LEFT JOIN request_logs rl ON rl.id = rm.request_log_id" : "";
|
|
117
115
|
const rows = db.prepare(`
|
|
118
116
|
SELECT
|
|
119
117
|
(unixepoch(rm.created_at) / ?) * ? AS bucket_key,
|
|
120
118
|
${expr} AS avg_value,
|
|
121
119
|
COUNT(*) AS count
|
|
122
120
|
FROM request_metrics rm
|
|
123
|
-
${joinClause}
|
|
124
121
|
WHERE ${where}
|
|
125
122
|
GROUP BY bucket_key
|
|
126
123
|
ORDER BY bucket_key ASC
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Metrics 独立化:request_metrics 增加路由维度列,解除级联删除依赖
|
|
2
|
+
-- usage_windows 增加 provider_id 支持按 provider 维度追踪使用量
|
|
3
|
+
|
|
4
|
+
-- 1. 重建 request_metrics:CASCADE -> SET NULL,同时新增 router_key_id / status_code
|
|
5
|
+
CREATE TABLE request_metrics_new (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
request_log_id TEXT UNIQUE REFERENCES request_logs(id) ON DELETE SET NULL,
|
|
8
|
+
provider_id TEXT NOT NULL,
|
|
9
|
+
backend_model TEXT NOT NULL,
|
|
10
|
+
api_type TEXT NOT NULL,
|
|
11
|
+
input_tokens INTEGER,
|
|
12
|
+
output_tokens INTEGER,
|
|
13
|
+
cache_creation_tokens INTEGER,
|
|
14
|
+
cache_read_tokens INTEGER,
|
|
15
|
+
ttft_ms INTEGER,
|
|
16
|
+
total_duration_ms INTEGER,
|
|
17
|
+
tokens_per_second REAL,
|
|
18
|
+
stop_reason TEXT,
|
|
19
|
+
is_complete INTEGER NOT NULL DEFAULT 1,
|
|
20
|
+
router_key_id TEXT,
|
|
21
|
+
status_code INTEGER,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
INSERT INTO request_metrics_new
|
|
26
|
+
(id, request_log_id, provider_id, backend_model, api_type,
|
|
27
|
+
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
|
|
28
|
+
ttft_ms, total_duration_ms, tokens_per_second, stop_reason,
|
|
29
|
+
is_complete, created_at)
|
|
30
|
+
SELECT
|
|
31
|
+
id, request_log_id, provider_id, backend_model, api_type,
|
|
32
|
+
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
|
|
33
|
+
ttft_ms, total_duration_ms, tokens_per_second, stop_reason,
|
|
34
|
+
is_complete, created_at
|
|
35
|
+
FROM request_metrics;
|
|
36
|
+
|
|
37
|
+
-- 回填 router_key_id 和 status_code 从 request_logs
|
|
38
|
+
UPDATE request_metrics_new
|
|
39
|
+
SET
|
|
40
|
+
router_key_id = rl.router_key_id,
|
|
41
|
+
status_code = rl.status_code
|
|
42
|
+
FROM request_logs rl
|
|
43
|
+
WHERE rl.id = request_metrics_new.request_log_id;
|
|
44
|
+
|
|
45
|
+
DROP TABLE request_metrics;
|
|
46
|
+
ALTER TABLE request_metrics_new RENAME TO request_metrics;
|
|
47
|
+
|
|
48
|
+
-- 重建原有索引
|
|
49
|
+
CREATE INDEX idx_metrics_time_provider_model ON request_metrics(created_at, provider_id, backend_model);
|
|
50
|
+
CREATE INDEX idx_metrics_api_type_created_at ON request_metrics(api_type, created_at);
|
|
51
|
+
|
|
52
|
+
-- 2. usage_windows 增加 provider_id 列
|
|
53
|
+
ALTER TABLE usage_windows ADD COLUMN provider_id TEXT;
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_usage_windows_provider_id ON usage_windows(provider_id);
|
package/dist/db/stats.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface Stats {
|
|
|
3
3
|
totalRequests: number;
|
|
4
4
|
successRate: number;
|
|
5
5
|
avgTps: number;
|
|
6
|
-
|
|
6
|
+
totalInputTokens: number;
|
|
7
|
+
totalOutputTokens: number;
|
|
7
8
|
}
|
|
8
|
-
export declare function getStats(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string): Stats;
|
|
9
|
+
export declare function getStats(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string, providerId?: string, backendModel?: string): Stats;
|
package/dist/db/stats.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function getStats(db, startTime, endTime, routerKeyId) {
|
|
1
|
+
export function getStats(db, startTime, endTime, routerKeyId, providerId, backendModel) {
|
|
2
2
|
const conditions = [
|
|
3
3
|
"rm.is_complete = 1",
|
|
4
4
|
"rm.created_at >= datetime(?)",
|
|
@@ -6,18 +6,26 @@ export function getStats(db, startTime, endTime, routerKeyId) {
|
|
|
6
6
|
];
|
|
7
7
|
const params = [startTime, endTime];
|
|
8
8
|
if (routerKeyId) {
|
|
9
|
-
conditions.push("
|
|
9
|
+
conditions.push("rm.router_key_id = ?");
|
|
10
10
|
params.push(routerKeyId);
|
|
11
11
|
}
|
|
12
|
+
if (providerId) {
|
|
13
|
+
conditions.push("rm.provider_id = ?");
|
|
14
|
+
params.push(providerId);
|
|
15
|
+
}
|
|
16
|
+
if (backendModel) {
|
|
17
|
+
conditions.push("rm.backend_model = ?");
|
|
18
|
+
params.push(backendModel);
|
|
19
|
+
}
|
|
12
20
|
const where = conditions.join(" AND ");
|
|
13
21
|
const row = db.prepare(`
|
|
14
22
|
SELECT
|
|
15
23
|
COUNT(*) AS total_requests,
|
|
16
|
-
SUM(CASE WHEN
|
|
24
|
+
SUM(CASE WHEN rm.status_code >= 200 AND rm.status_code < 300 THEN 1 ELSE 0 END) AS success_count,
|
|
17
25
|
AVG(rm.tokens_per_second) AS avg_tps,
|
|
18
|
-
COALESCE(SUM(rm.input_tokens), 0)
|
|
26
|
+
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
|
|
27
|
+
COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
|
|
19
28
|
FROM request_metrics rm
|
|
20
|
-
JOIN request_logs rl ON rl.id = rm.request_log_id
|
|
21
29
|
WHERE ${where}
|
|
22
30
|
`).get(...params);
|
|
23
31
|
const total = row?.total_requests ?? 0;
|
|
@@ -25,6 +33,7 @@ export function getStats(db, startTime, endTime, routerKeyId) {
|
|
|
25
33
|
totalRequests: total,
|
|
26
34
|
successRate: total > 0 ? (row?.success_count ?? 0) / total : 0,
|
|
27
35
|
avgTps: row?.avg_tps ?? 0,
|
|
28
|
-
|
|
36
|
+
totalInputTokens: row?.total_input_tokens ?? 0,
|
|
37
|
+
totalOutputTokens: row?.total_output_tokens ?? 0,
|
|
29
38
|
};
|
|
30
39
|
}
|
|
@@ -2,6 +2,7 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
export interface UsageWindow {
|
|
3
3
|
id: string;
|
|
4
4
|
router_key_id: string | null;
|
|
5
|
+
provider_id: string | null;
|
|
5
6
|
start_time: string;
|
|
6
7
|
end_time: string;
|
|
7
8
|
created_at: string;
|
|
@@ -12,8 +13,8 @@ export interface WindowUsage {
|
|
|
12
13
|
total_output_tokens: number;
|
|
13
14
|
}
|
|
14
15
|
export declare function insertWindow(db: Database.Database, w: Omit<UsageWindow, "created_at">): string;
|
|
15
|
-
export declare function getLatestWindow(db: Database.Database, routerKeyId?: string): UsageWindow | null;
|
|
16
|
-
/** 返回与 [start, end)
|
|
17
|
-
export declare function getWindowsInRange(db: Database.Database, start: string, end: string, routerKeyId?: string): UsageWindow[];
|
|
16
|
+
export declare function getLatestWindow(db: Database.Database, routerKeyId?: string, providerId?: string): UsageWindow | null;
|
|
17
|
+
/** 返回与 [start, end) 区间有重叠的窗口。可选参数不传表示不过滤该维度(与 getLatestWindow 的 IS NULL 语义不同) */
|
|
18
|
+
export declare function getWindowsInRange(db: Database.Database, start: string, end: string, routerKeyId?: string, providerId?: string): UsageWindow[];
|
|
18
19
|
/** 聚合指定时间窗口内的请求计数和 token 用量 */
|
|
19
|
-
export declare function getWindowUsage(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string): WindowUsage;
|
|
20
|
+
export declare function getWindowUsage(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string, providerId?: string): WindowUsage;
|
package/dist/db/usage-windows.js
CHANGED
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
export function insertWindow(db, w) {
|
|
3
3
|
const id = w.id || randomUUID();
|
|
4
|
-
db.prepare("INSERT INTO usage_windows (id, router_key_id, start_time, end_time) VALUES (?, ?, ?, ?)").run(id, w.router_key_id ?? null, w.start_time, w.end_time);
|
|
4
|
+
db.prepare("INSERT INTO usage_windows (id, router_key_id, provider_id, start_time, end_time) VALUES (?, ?, ?, ?, ?)").run(id, w.router_key_id ?? null, w.provider_id ?? null, w.start_time, w.end_time);
|
|
5
5
|
return id;
|
|
6
6
|
}
|
|
7
|
-
export function getLatestWindow(db, routerKeyId) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export function getLatestWindow(db, routerKeyId, providerId) {
|
|
8
|
+
const conditions = [];
|
|
9
|
+
const params = [];
|
|
10
|
+
if (routerKeyId) {
|
|
11
|
+
conditions.push("router_key_id = ?");
|
|
12
|
+
params.push(routerKeyId);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
conditions.push("router_key_id IS NULL");
|
|
16
|
+
}
|
|
17
|
+
if (providerId) {
|
|
18
|
+
conditions.push("provider_id = ?");
|
|
19
|
+
params.push(providerId);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
conditions.push("provider_id IS NULL");
|
|
23
|
+
}
|
|
24
|
+
const sql = `SELECT * FROM usage_windows WHERE ${conditions.join(" AND ")} ORDER BY start_time DESC LIMIT 1`;
|
|
12
25
|
return db.prepare(sql).get(...params) ?? null;
|
|
13
26
|
}
|
|
14
|
-
/** 返回与 [start, end)
|
|
15
|
-
export function getWindowsInRange(db, start, end, routerKeyId) {
|
|
27
|
+
/** 返回与 [start, end) 区间有重叠的窗口。可选参数不传表示不过滤该维度(与 getLatestWindow 的 IS NULL 语义不同) */
|
|
28
|
+
export function getWindowsInRange(db, start, end, routerKeyId, providerId) {
|
|
29
|
+
const conditions = ["start_time < ?", "end_time > ?"];
|
|
30
|
+
const params = [end, start];
|
|
16
31
|
if (routerKeyId) {
|
|
17
|
-
|
|
32
|
+
conditions.push("router_key_id = ?");
|
|
33
|
+
params.push(routerKeyId);
|
|
34
|
+
}
|
|
35
|
+
if (providerId) {
|
|
36
|
+
conditions.push("provider_id = ?");
|
|
37
|
+
params.push(providerId);
|
|
18
38
|
}
|
|
19
|
-
return db.prepare(
|
|
39
|
+
return db.prepare(`SELECT * FROM usage_windows WHERE ${conditions.join(" AND ")} ORDER BY start_time ASC`).all(...params);
|
|
20
40
|
}
|
|
21
41
|
/** 聚合指定时间窗口内的请求计数和 token 用量 */
|
|
22
|
-
export function getWindowUsage(db, startTime, endTime, routerKeyId) {
|
|
23
|
-
const
|
|
42
|
+
export function getWindowUsage(db, startTime, endTime, routerKeyId, providerId) {
|
|
43
|
+
const conditions = [
|
|
44
|
+
"rm.is_complete = 1",
|
|
45
|
+
"rm.created_at >= datetime(?)",
|
|
46
|
+
"rm.created_at < datetime(?)",
|
|
47
|
+
];
|
|
48
|
+
const params = [startTime, endTime];
|
|
49
|
+
if (routerKeyId) {
|
|
50
|
+
conditions.push("rm.router_key_id = ?");
|
|
51
|
+
params.push(routerKeyId);
|
|
52
|
+
}
|
|
53
|
+
if (providerId) {
|
|
54
|
+
conditions.push("rm.provider_id = ?");
|
|
55
|
+
params.push(providerId);
|
|
56
|
+
}
|
|
57
|
+
return db.prepare(`
|
|
24
58
|
SELECT
|
|
25
59
|
COUNT(*) AS request_count,
|
|
26
60
|
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
|
|
27
61
|
COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
|
|
28
62
|
FROM request_metrics rm
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
AND rm.created_at >= datetime(?)
|
|
32
|
-
AND rm.created_at < datetime(?)`;
|
|
33
|
-
if (routerKeyId) {
|
|
34
|
-
return db.prepare(`${baseSql} AND rl.router_key_id = ?`).get(startTime, endTime, routerKeyId);
|
|
35
|
-
}
|
|
36
|
-
return db.prepare(baseSql).get(startTime, endTime);
|
|
63
|
+
WHERE ${conditions.join(" AND ")}
|
|
64
|
+
`).get(...params);
|
|
37
65
|
}
|
|
@@ -16,6 +16,15 @@ import { applyProviderPatches } from "./patch/index.js";
|
|
|
16
16
|
const HTTP_ERROR_THRESHOLD = 400;
|
|
17
17
|
const MAX_LOG_FIELD_LENGTH = 80;
|
|
18
18
|
const UPSTREAM_ERROR_STATUS = 502;
|
|
19
|
+
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
20
|
+
function getTransportStatusCode(result) {
|
|
21
|
+
if (result.kind === "success" || result.kind === "error" || result.kind === "stream_error")
|
|
22
|
+
return result.statusCode;
|
|
23
|
+
if (result.kind === "stream_success" || result.kind === "stream_abort")
|
|
24
|
+
return result.statusCode;
|
|
25
|
+
// kind === "throw":无 HTTP 状态码
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
19
28
|
function rejectAndReply(reply, params, error, errorMessage, providerId) {
|
|
20
29
|
insertRejectedLog({
|
|
21
30
|
db: params.db, logId: params.logId, apiType: params.apiType, model: params.model,
|
|
@@ -150,11 +159,11 @@ async function executeFailoverLoop(ctx) {
|
|
|
150
159
|
clientReq, upstreamReqBase, logId, routerKeyId, originalModel, sessionId,
|
|
151
160
|
failover: { isFailoverIteration, rootLogId: rootLogId },
|
|
152
161
|
}, resilienceResult.attempts, resilienceResult.result, startTime);
|
|
153
|
-
collectTransportMetrics(deps.db, apiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request);
|
|
162
|
+
collectTransportMetrics(deps.db, apiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request, routerKeyId, getTransportStatusCode(resilienceResult.result));
|
|
154
163
|
const tr = resilienceResult.result;
|
|
155
164
|
const succeeded = tr.kind === "success" || tr.kind === "stream_success" || tr.kind === "stream_abort";
|
|
156
165
|
if (succeeded)
|
|
157
|
-
deps.usageWindowTracker?.recordRequest(routerKeyId ?? undefined);
|
|
166
|
+
deps.usageWindowTracker?.recordRequest(provider.id, routerKeyId ?? undefined);
|
|
158
167
|
if (isStream && deps.tracker) {
|
|
159
168
|
const sc = deps.tracker.get(logId)?.streamContent;
|
|
160
169
|
const blocks = sc?.blocks;
|
|
@@ -23,4 +23,4 @@ export declare function logResilienceResult(db: Database.Database, params: {
|
|
|
23
23
|
sessionId?: string | null;
|
|
24
24
|
failover?: FailoverContext;
|
|
25
25
|
}, attempts: ResilienceAttempt[], result: TransportResult, startTime: number): string;
|
|
26
|
-
export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest): void;
|
|
26
|
+
export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest, routerKeyId?: string | null, statusCode?: number | null): void;
|
|
@@ -116,8 +116,11 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
|
|
|
116
116
|
}
|
|
117
117
|
return lastSuccessLogId;
|
|
118
118
|
}
|
|
119
|
-
export function collectTransportMetrics(db, apiType, result, isStream, lastSuccessLogId, providerId, backendModel, request) {
|
|
120
|
-
const base = {
|
|
119
|
+
export function collectTransportMetrics(db, apiType, result, isStream, lastSuccessLogId, providerId, backendModel, request, routerKeyId, statusCode) {
|
|
120
|
+
const base = {
|
|
121
|
+
request_log_id: lastSuccessLogId, provider_id: providerId, backend_model: backendModel, api_type: apiType,
|
|
122
|
+
router_key_id: routerKeyId ?? null, status_code: statusCode ?? null,
|
|
123
|
+
};
|
|
121
124
|
try {
|
|
122
125
|
if (isStream && (result.kind === "stream_success" || result.kind === "stream_abort")) {
|
|
123
126
|
if (result.metrics) {
|
|
@@ -3,9 +3,11 @@ export declare class UsageWindowTracker {
|
|
|
3
3
|
private db;
|
|
4
4
|
constructor(db: Database.Database);
|
|
5
5
|
/** 请求成功后调用,按需创建新窗口 */
|
|
6
|
-
recordRequest(routerKeyId?: string): void;
|
|
7
|
-
/**
|
|
6
|
+
recordRequest(providerId: string, routerKeyId?: string): void;
|
|
7
|
+
/** 启动时按活跃 provider 补齐缺失的窗口 */
|
|
8
8
|
reconcileOnStartup(): void;
|
|
9
|
+
/** 为单个 provider 补齐窗口 */
|
|
10
|
+
private reconcileProvider;
|
|
9
11
|
/** 从 baseTime 开始,每 5h 一个窗口,直到覆盖 lastLogTime */
|
|
10
|
-
private
|
|
12
|
+
private backfillProviderWindows;
|
|
11
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { getLatestWindow, insertWindow } from "../db/usage-windows.js";
|
|
3
|
+
import { getAllProviders } from "../db/providers.js";
|
|
3
4
|
import { toSqliteDatetime, parseSqliteDatetime as parseDate } from "../utils/datetime.js";
|
|
4
5
|
// eslint-disable-next-line no-magic-numbers
|
|
5
6
|
const WINDOW_DURATION_MS = 5 * 3600_000;
|
|
@@ -9,49 +10,46 @@ export class UsageWindowTracker {
|
|
|
9
10
|
this.db = db;
|
|
10
11
|
}
|
|
11
12
|
/** 请求成功后调用,按需创建新窗口 */
|
|
12
|
-
recordRequest(routerKeyId) {
|
|
13
|
+
recordRequest(providerId, routerKeyId) {
|
|
13
14
|
const now = new Date();
|
|
14
|
-
const latest = getLatestWindow(this.db, routerKeyId);
|
|
15
|
+
const latest = getLatestWindow(this.db, routerKeyId, providerId);
|
|
15
16
|
if (!latest || now > parseDate(latest.end_time)) {
|
|
16
17
|
const startTime = truncateToMinute(now);
|
|
17
18
|
insertWindow(this.db, {
|
|
18
19
|
id: randomUUID(),
|
|
19
20
|
router_key_id: routerKeyId ?? null,
|
|
21
|
+
provider_id: providerId,
|
|
20
22
|
start_time: toSqliteDatetime(startTime),
|
|
21
23
|
end_time: toSqliteDatetime(new Date(startTime.getTime() + WINDOW_DURATION_MS)),
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
|
-
/**
|
|
27
|
+
/** 启动时按活跃 provider 补齐缺失的窗口 */
|
|
26
28
|
reconcileOnStartup() {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const providers = getAllProviders(this.db).filter((p) => p.is_active);
|
|
30
|
+
for (const provider of providers) {
|
|
31
|
+
this.reconcileProvider(provider.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** 为单个 provider 补齐窗口 */
|
|
35
|
+
reconcileProvider(providerId) {
|
|
36
|
+
const latest = getLatestWindow(this.db, undefined, providerId);
|
|
37
|
+
const lastLog = this.db.prepare("SELECT created_at FROM request_logs WHERE provider_id = ? ORDER BY created_at DESC LIMIT 1").get(providerId);
|
|
30
38
|
if (!lastLog)
|
|
31
39
|
return;
|
|
32
40
|
if (!latest) {
|
|
33
|
-
|
|
34
|
-
const firstLog = this.db.prepare("SELECT created_at FROM request_logs ORDER BY created_at ASC LIMIT 1").get();
|
|
41
|
+
const firstLog = this.db.prepare("SELECT created_at FROM request_logs WHERE provider_id = ? ORDER BY created_at ASC LIMIT 1").get(providerId);
|
|
35
42
|
if (!firstLog)
|
|
36
43
|
return;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
insertWindow(this.db, {
|
|
40
|
-
id: randomUUID(),
|
|
41
|
-
router_key_id: null,
|
|
42
|
-
start_time: toSqliteDatetime(truncated),
|
|
43
|
-
end_time: toSqliteDatetime(new Date(truncated.getTime() + WINDOW_DURATION_MS)),
|
|
44
|
-
});
|
|
45
|
-
// 继续补齐后续窗口
|
|
46
|
-
this.backfillWindows(truncated);
|
|
44
|
+
const truncated = truncateToMinute(parseDate(firstLog.created_at));
|
|
45
|
+
this.backfillProviderWindows(providerId, truncated);
|
|
47
46
|
return;
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
this.backfillWindows(parseDate(latest.end_time));
|
|
48
|
+
this.backfillProviderWindows(providerId, parseDate(latest.end_time));
|
|
51
49
|
}
|
|
52
50
|
/** 从 baseTime 开始,每 5h 一个窗口,直到覆盖 lastLogTime */
|
|
53
|
-
|
|
54
|
-
const lastLog = this.db.prepare("SELECT created_at FROM request_logs ORDER BY created_at DESC LIMIT 1").get();
|
|
51
|
+
backfillProviderWindows(providerId, baseTime) {
|
|
52
|
+
const lastLog = this.db.prepare("SELECT created_at FROM request_logs WHERE provider_id = ? ORDER BY created_at DESC LIMIT 1").get(providerId);
|
|
55
53
|
if (!lastLog)
|
|
56
54
|
return;
|
|
57
55
|
const lastLogTime = parseDate(lastLog.created_at);
|
|
@@ -61,6 +59,7 @@ export class UsageWindowTracker {
|
|
|
61
59
|
insertWindow(this.db, {
|
|
62
60
|
id: randomUUID(),
|
|
63
61
|
router_key_id: null,
|
|
62
|
+
provider_id: providerId,
|
|
64
63
|
start_time: toSqliteDatetime(windowStart),
|
|
65
64
|
end_time: toSqliteDatetime(windowEnd),
|
|
66
65
|
});
|
|
@@ -4,6 +4,6 @@ export interface TimeRange {
|
|
|
4
4
|
startTime: string;
|
|
5
5
|
endTime: string;
|
|
6
6
|
}
|
|
7
|
-
export declare function resolveTimeRange(period: DashboardPeriod, db: Database.Database, routerKeyId?: string): TimeRange;
|
|
7
|
+
export declare function resolveTimeRange(period: DashboardPeriod, db: Database.Database, routerKeyId?: string, providerId?: string): TimeRange;
|
|
8
8
|
/** 从 date 对象中计算出当周的周一 */
|
|
9
9
|
export declare function getMonday(date: Date): Date;
|
package/dist/utils/time-range.js
CHANGED
|
@@ -5,28 +5,33 @@ const WINDOW_HOURS = 5;
|
|
|
5
5
|
const MS_PER_HOUR = 3600_000;
|
|
6
6
|
// 与 usage-windows 的默认窗口时长对齐
|
|
7
7
|
const WINDOW_DURATION_MS = WINDOW_HOURS * MS_PER_HOUR;
|
|
8
|
-
export function resolveTimeRange(period, db, routerKeyId) {
|
|
8
|
+
export function resolveTimeRange(period, db, routerKeyId, providerId) {
|
|
9
9
|
const now = new Date();
|
|
10
10
|
switch (period) {
|
|
11
11
|
case "window": {
|
|
12
|
-
const latest = getLatestWindow(db, routerKeyId);
|
|
12
|
+
const latest = getLatestWindow(db, routerKeyId, providerId);
|
|
13
13
|
if (!latest) {
|
|
14
|
-
return createAndReturnWindow(db, now, routerKeyId);
|
|
14
|
+
return createAndReturnWindow(db, now, routerKeyId, providerId);
|
|
15
15
|
}
|
|
16
16
|
// 最新窗口已过期(无请求触发新窗口创建),主动补齐
|
|
17
17
|
if (now > parseSqliteDatetime(latest.end_time)) {
|
|
18
|
-
return createAndReturnWindow(db, now, routerKeyId);
|
|
18
|
+
return createAndReturnWindow(db, now, routerKeyId, providerId);
|
|
19
19
|
}
|
|
20
20
|
return { startTime: latest.start_time, endTime: latest.end_time };
|
|
21
21
|
}
|
|
22
22
|
case "weekly": {
|
|
23
23
|
const monday = getMonday(now);
|
|
24
24
|
monday.setHours(0, 0, 0, 0);
|
|
25
|
-
|
|
25
|
+
const sunday = new Date(monday);
|
|
26
|
+
sunday.setDate(sunday.getDate() + 6);
|
|
27
|
+
sunday.setHours(23, 59, 59, 999);
|
|
28
|
+
return { startTime: toSqliteDatetime(monday), endTime: toSqliteDatetime(sunday) };
|
|
26
29
|
}
|
|
27
30
|
case "monthly": {
|
|
28
31
|
const first = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
29
|
-
|
|
32
|
+
const last = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
33
|
+
last.setHours(23, 59, 59, 999);
|
|
34
|
+
return { startTime: toSqliteDatetime(first), endTime: toSqliteDatetime(last) };
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
}
|
|
@@ -41,13 +46,14 @@ export function getMonday(date) {
|
|
|
41
46
|
d.setDate(diff);
|
|
42
47
|
return d;
|
|
43
48
|
}
|
|
44
|
-
function createAndReturnWindow(db, now, routerKeyId) {
|
|
49
|
+
function createAndReturnWindow(db, now, routerKeyId, providerId) {
|
|
45
50
|
const start = new Date(now);
|
|
46
51
|
start.setMinutes(0, 0, 0);
|
|
47
52
|
const end = new Date(start.getTime() + WINDOW_DURATION_MS);
|
|
48
53
|
insertWindow(db, {
|
|
49
54
|
id: randomUUID(),
|
|
50
55
|
router_key_id: routerKeyId ?? null,
|
|
56
|
+
provider_id: providerId ?? null,
|
|
51
57
|
start_time: toSqliteDatetime(start),
|
|
52
58
|
end_time: toSqliteDatetime(end),
|
|
53
59
|
});
|