llm-simple-router 0.4.0 → 0.4.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/README.md +128 -91
- package/dist/admin/logs.js +7 -1
- package/dist/admin/metrics.js +7 -3
- package/dist/admin/recommended.d.ts +7 -0
- package/dist/admin/recommended.js +25 -0
- package/dist/admin/routes.js +6 -0
- package/dist/admin/settings.d.ts +7 -0
- package/dist/admin/settings.js +16 -0
- package/dist/admin/usage.d.ts +7 -0
- package/dist/admin/usage.js +66 -0
- package/dist/config/recommended.d.ts +24 -0
- package/dist/config/recommended.js +30 -0
- package/dist/db/index.d.ts +4 -2
- package/dist/db/index.js +21 -4
- package/dist/db/log-cleaner.d.ts +10 -0
- package/dist/db/log-cleaner.js +42 -0
- package/dist/db/logs.d.ts +33 -8
- package/dist/db/logs.js +52 -6
- package/dist/db/metrics.d.ts +3 -3
- package/dist/db/metrics.js +50 -42
- package/dist/db/migrations/019_create_usage_windows.sql +11 -0
- package/dist/db/migrations/020_drop_log_redundancy.sql +8 -0
- package/dist/db/migrations/021_merge_metrics_columns.sql +28 -0
- package/dist/db/retry-rules.d.ts +0 -5
- package/dist/db/retry-rules.js +0 -36
- package/dist/db/settings.d.ts +2 -0
- package/dist/db/settings.js +7 -0
- package/dist/db/usage-windows.d.ts +19 -0
- package/dist/db/usage-windows.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +20 -3
- package/dist/monitor/types.d.ts +3 -0
- package/dist/proxy/log-helpers.d.ts +0 -2
- package/dist/proxy/log-helpers.js +3 -5
- package/dist/proxy/orchestrator.d.ts +2 -0
- package/dist/proxy/orchestrator.js +1 -0
- package/dist/proxy/proxy-handler.js +29 -4
- package/dist/proxy/proxy-logging.d.ts +0 -1
- package/dist/proxy/proxy-logging.js +8 -10
- package/dist/proxy/usage-window-tracker.d.ts +11 -0
- package/dist/proxy/usage-window-tracker.js +75 -0
- package/dist/utils/datetime.d.ts +4 -0
- package/dist/utils/datetime.js +10 -0
- package/frontend-dist/assets/CardContent-3ytnac7B.js +1 -0
- package/frontend-dist/assets/CardTitle-BHZE8Rty.js +1 -0
- package/frontend-dist/assets/Checkbox-CMYgDuxw.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-DooxvEnx.js +1 -0
- package/frontend-dist/assets/Collection-GDvpW_uY.js +3 -0
- package/frontend-dist/assets/Dashboard-BJslVTg8.js +3 -0
- package/frontend-dist/assets/DialogTitle-lj6NAA5R.js +1 -0
- package/frontend-dist/assets/Input-JApdUstN.js +1 -0
- package/frontend-dist/assets/Label-IbQFgxLe.js +1 -0
- package/frontend-dist/assets/Login-BjuVvrPV.js +1 -0
- package/frontend-dist/assets/Logs-J08HyZWA.js +1 -0
- package/frontend-dist/assets/ModelMappings-DWVmxMy6.js +1 -0
- package/frontend-dist/assets/Monitor-BTEW0evp.js +1 -0
- package/frontend-dist/assets/PopperContent-ZhhkKJo0.js +1 -0
- package/frontend-dist/assets/Providers-BqLSKXuv.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-TAHOKnxW.js +5 -0
- package/frontend-dist/assets/RetryRules-Cn6KHzgB.js +1 -0
- package/frontend-dist/assets/RouterKeys-CBgWAJ6-.js +1 -0
- package/frontend-dist/assets/SelectValue-DS4Z8y0u.js +1 -0
- package/frontend-dist/assets/Setup-QKmeMDtB.js +1 -0
- package/frontend-dist/assets/Switch-BYebebrY.js +1 -0
- package/frontend-dist/assets/TableHeader-B2A48qgy.js +1 -0
- package/frontend-dist/assets/TabsContent-BcNBY5CB.js +1 -0
- package/frontend-dist/assets/TabsTrigger-8W_mNsGI.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-BmEamR1L.js +3 -0
- package/frontend-dist/assets/UnifiedRequestDialog-Dk3IIDDx.css +1 -0
- package/frontend-dist/assets/VisuallyHidden-DPKPka_x.js +1 -0
- package/frontend-dist/assets/VisuallyHiddenInput-Bnglr6yR.js +1 -0
- package/frontend-dist/assets/alert-dialog-BzyDZnoE.js +1 -0
- package/frontend-dist/assets/badge-BTjuxlp4.js +1 -0
- package/frontend-dist/assets/button-BKJB3nEQ.js +12 -0
- package/frontend-dist/assets/{createLucideIcon-DGZkBjcJ.js → createLucideIcon-igIAnu_Y.js} +1 -1
- package/frontend-dist/assets/dialog-C0B-Xn-S.js +1 -0
- package/frontend-dist/assets/file-text-Ci7Mgh3F.js +1 -0
- package/frontend-dist/assets/index-BrDOp_gc.js +1 -0
- package/frontend-dist/assets/index-DMdVJThL.css +1 -0
- package/frontend-dist/assets/lib-BGW4QyKP.js +1 -0
- package/frontend-dist/assets/{ohash.D__AXeF1-B64hB831.js → ohash.D__AXeF1-CsY_LBk-.js} +1 -1
- package/frontend-dist/assets/{useClipboard-CWc1cTDo.js → useClipboard-wnGQAe3I.js} +1 -1
- package/frontend-dist/assets/useForwardExpose-bqtcPo63.js +1 -0
- package/frontend-dist/assets/useNonce-DN0Hrw3l.js +1 -0
- package/frontend-dist/assets/x-Cy_v5hrA.js +1 -0
- package/frontend-dist/index.html +8 -7
- package/package.json +1 -1
- package/frontend-dist/assets/CardContent-CTnwqTdL.js +0 -1
- package/frontend-dist/assets/CardHeader-CfUeY7tk.js +0 -1
- package/frontend-dist/assets/CardTitle-CWiDwWqd.js +0 -1
- package/frontend-dist/assets/Checkbox-BxNz70R_.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-Uz1aGdtH.js +0 -1
- package/frontend-dist/assets/Collection-1EHC87X5.js +0 -3
- package/frontend-dist/assets/Dashboard-C3FL30UN.js +0 -3
- package/frontend-dist/assets/DialogTitle-CAOFxr83.js +0 -1
- package/frontend-dist/assets/Input-DRIid2C6.js +0 -1
- package/frontend-dist/assets/Label-UyNN2jyE.js +0 -1
- package/frontend-dist/assets/LogDetailDialog-8BT4vIlV.js +0 -3
- package/frontend-dist/assets/Login-CnzH6TdS.js +0 -1
- package/frontend-dist/assets/Logs-CbK8NB_X.js +0 -1
- package/frontend-dist/assets/ModelMappings-DeRFgsYG.js +0 -1
- package/frontend-dist/assets/Monitor-Dd80bdUn.js +0 -1
- package/frontend-dist/assets/PopperContent-B3fZao7v.js +0 -1
- package/frontend-dist/assets/Providers-B_DbV-_y.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-up1fnPzq.js +0 -5
- package/frontend-dist/assets/RetryRules-Dkuhjh0u.js +0 -1
- package/frontend-dist/assets/RouterKeys-CvMMAa4t.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-X0bfqWWS.js +0 -1
- package/frontend-dist/assets/SelectValue-zO8t-tx1.js +0 -1
- package/frontend-dist/assets/Setup-ByT2ThOQ.js +0 -1
- package/frontend-dist/assets/Switch-BEMjVugO.js +0 -1
- package/frontend-dist/assets/TableHeader-DpHWSnxK.js +0 -1
- package/frontend-dist/assets/TabsTrigger-Db6RqsZc.js +0 -1
- package/frontend-dist/assets/VisuallyHidden-hs8pj8OP.js +0 -1
- package/frontend-dist/assets/VisuallyHiddenInput-1m0nNADN.js +0 -1
- package/frontend-dist/assets/alert-dialog-PP91kaO8.js +0 -1
- package/frontend-dist/assets/button-Dcc0gF5i.js +0 -1
- package/frontend-dist/assets/client-DIIo9zPK.js +0 -12
- package/frontend-dist/assets/dialog-CxSyR-fN.js +0 -1
- package/frontend-dist/assets/index-BL-LAtac.css +0 -1
- package/frontend-dist/assets/index-CvT41fGL.js +0 -1
- package/frontend-dist/assets/lib-Bl0OuBjh.js +0 -1
- package/frontend-dist/assets/useForwardExpose-AkE0lq8y.js +0 -1
- package/frontend-dist/assets/useNonce-DGyPxdjq.js +0 -1
- package/frontend-dist/assets/x-BuUpx9Fr.js +0 -1
package/README.md
CHANGED
|
@@ -1,64 +1,133 @@
|
|
|
1
1
|
# LLM Simple Router
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
>
|
|
5
|
-
> 核心代理、模型映射、自动重试、多密钥管理、请求日志、性能指标已完成。
|
|
6
|
-
> 代码规范 githook 检查已集成。欢迎试用和反馈。
|
|
3
|
+
LLM API 代理路由器。接收 Claude Code / Cursor 等客户端请求,通过模型映射和路由策略转发到配置的后端 Provider,支持流式(SSE)和非流式代理。
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
**解决的核心问题**:国产模型限流频繁、多供应商切换麻烦、并发控制缺失。
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
## 适合谁
|
|
11
8
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- **并发队列等待** — 不同 Provider 可以配置并发数限制,超过限制的请求会进入队列等待。解决 Claude Code 多个subagent并行执行时经常触发限流失败的问题。不过需要配置 claude code 的 API_TIMEOUT_MS 为一个比较大的值。这个功能可以让使用 Claude Code 体验更好,但不能根本性解决限流问题。
|
|
16
|
-
- **实时请求监控** — 实时监控活跃请求、队列情况,可以实时查看流式请求的输出并结构化展示(目前仅支持 Claude Code )。
|
|
9
|
+
- 用 Claude Code 配合国产模型(智谱、Moonshot、Minimax 等)的开发者
|
|
10
|
+
- 希望自动重试限流错误、分时段切换模型、控制并发排队
|
|
11
|
+
- 想要一个开箱即用的方案,不折腾
|
|
17
12
|
|
|
18
|
-
##
|
|
13
|
+
## 功能一览
|
|
19
14
|
|
|
20
15
|
| 功能 | 说明 |
|
|
21
16
|
|------|------|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
17
|
+
| 自动重试 | 对 429/400/网络超时自动指数退避重试,默认针对智谱模型配置 |
|
|
18
|
+
| 多供应商支持 | 智谱、Moonshot、Minimax、火山引擎、阿里云、腾讯云等,Coding Plan 选择后自动填写 |
|
|
19
|
+
| 模型分时段映射 | 按时间段自动切换后端模型(如高峰期切到 Kimi,低谷期切回 GLM) |
|
|
20
|
+
| 并发队列等待 | 按 Provider 配置并发数上限,超限请求排队等待 |
|
|
21
|
+
| Failover 故障转移 | 多 Provider 互备,失败自动切换下一个 |
|
|
22
|
+
| 实时请求监控 | SSE 推送活跃请求、队列状态、流式输出实时查看 |
|
|
23
|
+
| 多密钥管理 | 独立密钥 + 模型白名单,支持多用户/多项目 |
|
|
24
|
+
| 请求日志 | 四阶段完整链路(客户端请求/上游请求/上游响应/客户端响应) |
|
|
25
|
+
| 性能指标 | TTFT、TPS、Token 用量、缓存命中率 |
|
|
26
26
|
|
|
27
27
|
> **API 兼容性:** 支持 Anthropic 兼容 API(已适配 Claude Code)。OpenAI 兼容 API(`/v1/chat/completions`)尚未充分测试。
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## 管理后台
|
|
30
30
|
|
|
31
|
-
| Provider
|
|
32
|
-
|
|
33
|
-
|  |  |
|
|
34
34
|
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
|  |
|
|
42
|
-
|
|
43
|
-
| 重试规则 |
|
|
44
|
-
|---------|
|
|
45
|
-
|  |
|
|
35
|
+
| 模型映射 | 重试规则 |
|
|
36
|
+
|---|---|
|
|
37
|
+
|  |  |
|
|
46
38
|
|
|
47
39
|
| Dashboard | 请求日志 |
|
|
48
|
-
|
|
40
|
+
|---|---|
|
|
49
41
|
|  |  |
|
|
50
42
|
|
|
51
43
|
| 代理增强 (实验性) |
|
|
52
44
|
|-----------------|
|
|
53
45
|
|  |
|
|
54
46
|
|
|
47
|
+
## 快速开始
|
|
48
|
+
|
|
49
|
+
### 1. 启动 Router
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx llm-simple-router
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
访问 http://localhost:9981/admin ,首次进入 Setup 页面设置管理员密码。数据存储在 `~/.llm-simple-router/`。
|
|
56
|
+
|
|
57
|
+
### 2. 配置 Provider
|
|
58
|
+
|
|
59
|
+
管理后台 > Provider 页面 > 添加 Provider。选择 Coding Plan 后会自动填写 Base URL,只需填入 API Key。
|
|
60
|
+
|
|
61
|
+
### 3. 配置模型映射
|
|
62
|
+
|
|
63
|
+
管理后台 > 模型映射页面。示例配置:
|
|
64
|
+
|
|
65
|
+
| 客户端模型 | 后端模型 | 供应商 | 时间窗口 |
|
|
66
|
+
|-----------|---------|--------|---------|
|
|
67
|
+
| sonnet | glm-5.1 | 智谱 | 全天 |
|
|
68
|
+
| sonnet | kimi-for-coding | Moonshot | 14:00-18:00 |
|
|
69
|
+
|
|
70
|
+
客户端模型是指 Claude Code 实际请求的模型名(由 `ANTHROPIC_MODEL` 等环境变量决定)。
|
|
71
|
+
|
|
72
|
+
### 4. 配置 Claude Code
|
|
73
|
+
|
|
74
|
+
在管理后台创建 Router API 密钥,然后选择一种方式配置:
|
|
75
|
+
|
|
76
|
+
**方式一:shell alias(推荐)**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
alias clode='\
|
|
80
|
+
export ANTHROPIC_AUTH_TOKEN="<your-router-key>" && \
|
|
81
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:9981" && \
|
|
82
|
+
export ANTHROPIC_MODEL="<your-default-model>" && \
|
|
83
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL="<your-opus-model>" && \
|
|
84
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL="<your-sonnet-model>" && \
|
|
85
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL="<your-haiku-model>" && \
|
|
86
|
+
export ANTHROPIC_SMALL_FAST_MODEL="<your-fast-model>" && \
|
|
87
|
+
claude'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**方式二:~/.claude/settings.json**
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"env": {
|
|
95
|
+
"ANTHROPIC_AUTH_TOKEN": "<your-router-key>",
|
|
96
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9981",
|
|
97
|
+
"ANTHROPIC_MODEL": "<your-default-model>",
|
|
98
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL": "<your-opus-model>",
|
|
99
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL": "<your-sonnet-model>",
|
|
100
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "<your-haiku-model>",
|
|
101
|
+
"ANTHROPIC_SMALL_FAST_MODEL": "<your-fast-model>"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 5. 使用
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# 方式一用户直接用 alias
|
|
110
|
+
clode
|
|
111
|
+
|
|
112
|
+
# 方式二用户正常启动 claude
|
|
113
|
+
claude
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Docker 部署
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
docker compose up -d
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
环境变量通过 Setup 页面设置,不需要 `.env` 文件。
|
|
123
|
+
|
|
55
124
|
## 工作原理
|
|
56
125
|
|
|
57
126
|
```
|
|
58
|
-
Claude Code
|
|
127
|
+
Claude Code → Router (模型映射 + 自动重试 + 并发控制) → 智谱 GLM / Kimi / 其他供应商
|
|
59
128
|
```
|
|
60
129
|
|
|
61
|
-
Router 根据模型映射找到后端供应商
|
|
130
|
+
Router 根据模型映射找到后端供应商 → 转发请求 → 自动重试失败请求 → 记录日志和性能指标 → 返回响应。
|
|
62
131
|
|
|
63
132
|
### 架构图
|
|
64
133
|
|
|
@@ -89,74 +158,42 @@ flowchart LR
|
|
|
89
158
|
|
|
90
159
|
E -.->|失败| C
|
|
91
160
|
```
|
|
161
|
+
|
|
92
162
|
Router 收到请求后:认证 → 按映射规则找到后端 Provider → 排队控制并发 → 转发到上游(失败自动重试,Failover 策略下会切换 Provider)→ 记录日志和指标 → 返回响应。
|
|
93
163
|
|
|
94
|
-
##
|
|
164
|
+
## 环境变量
|
|
95
165
|
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
# 一行命令启动
|
|
99
|
-
npx llm-simple-router
|
|
100
|
-
# 访问 http://localhost:9981/admin
|
|
101
|
-
# 首次访问会进入 Setup 页面设置管理员密码
|
|
102
|
-
```
|
|
166
|
+
所有密钥通过 Setup 页面设置,以下为可选配置:
|
|
103
167
|
|
|
104
|
-
|
|
168
|
+
| 变量 | 默认值 | 说明 |
|
|
169
|
+
|------|--------|------|
|
|
170
|
+
| `PORT` | `9981` | 服务端口 |
|
|
171
|
+
| `DB_PATH` | `~/.llm-simple-router/router.db` | SQLite 数据库路径 |
|
|
172
|
+
| `LOG_LEVEL` | `info` | 日志级别 |
|
|
173
|
+
| `TZ` | `Asia/Shanghai` | 时区设置 |
|
|
174
|
+
| `STREAM_TIMEOUT_MS` | `3000000` | 流式代理空闲超时(ms) |
|
|
175
|
+
| `RETRY_MAX_ATTEMPTS` | `3` | 最大重试次数 |
|
|
176
|
+
| `RETRY_BASE_DELAY_MS` | `1000` | 重试基础延迟(ms) |
|
|
105
177
|
|
|
106
|
-
|
|
178
|
+
## 开发
|
|
107
179
|
|
|
108
180
|
```bash
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
### 创建 Router API 密钥
|
|
112
|
-
|
|
113
|
-
在管理后台创建一个 API 密钥,原先 Claude Code 密钥替换为 Router 密钥。
|
|
181
|
+
# 后端(热重载)
|
|
182
|
+
npm run dev
|
|
114
183
|
|
|
115
|
-
|
|
184
|
+
# 前端(热重载,代理 API 到后端 :9980)
|
|
185
|
+
cd frontend && npm run dev
|
|
116
186
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```
|
|
187
|
+
# 构建
|
|
188
|
+
npm run build:full
|
|
120
189
|
|
|
121
|
-
|
|
190
|
+
# 测试
|
|
191
|
+
npm test
|
|
122
192
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"env": {
|
|
126
|
-
"ANTHROPIC_AUTH_TOKEN": "sk-router-change-me",
|
|
127
|
-
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9981",
|
|
128
|
-
"ANTHROPIC_MODEL": "some-model"
|
|
129
|
-
}
|
|
130
|
-
}
|
|
193
|
+
# Lint
|
|
194
|
+
npm run lint
|
|
131
195
|
```
|
|
132
196
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Provider 支持快速配置,目前支持智谱、Moonshot、Minimax、火山引擎、阿里云、腾讯云等,Coding Plan 选择后会自动填写,只需要配置 API Key。
|
|
136
|
-
|
|
137
|
-
### 管理后台配置模型映射
|
|
138
|
-
|
|
139
|
-
| 客户端模型 | 后端模型 | 供应商 | 时间窗口 |
|
|
140
|
-
|-----------|---------|--------|---------|
|
|
141
|
-
| opus | glm-5.1 | 智谱 | / |
|
|
142
|
-
| sonnet | glm-5.1 | 智谱 | / |
|
|
143
|
-
| sonnet | kimi-for-coding | Moonshot | 14:00-18:00 |
|
|
144
|
-
| sonnet | glm-5-turbo | 智谱 | / |
|
|
145
|
-
|
|
146
|
-
客户端模型是指 Claude Code 实际请求发出的模型名。
|
|
147
|
-
如果你配置了 ANTHROPIC_MODEL 等变量,那应该以该变量为准。
|
|
148
|
-
个人的配置:高峰期 GLM 3倍用量,且频繁超限时,将 sonnet 切到 Kimi;低谷期切回 GLM。
|
|
149
|
-
|
|
150
|
-
## 环境变量
|
|
197
|
+
## License
|
|
151
198
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
| 变量 | 必需 | 默认值 | 说明 |
|
|
155
|
-
|------|------|--------|------|
|
|
156
|
-
| `PORT` | No | `9981` | 服务端口 |
|
|
157
|
-
| `DB_PATH` | No | `~/.llm-simple-router/router.db` | SQLite 数据库路径 |
|
|
158
|
-
| `LOG_LEVEL` | No | `info` | 日志级别 |
|
|
159
|
-
| `TZ` | No | `Asia/Shanghai` | 时区设置 |
|
|
160
|
-
| `STREAM_TIMEOUT_MS` | No | `3000000` | 流式代理空闲超时(ms) |
|
|
161
|
-
| `RETRY_MAX_ATTEMPTS` | No | `3` | 最大重试次数 |
|
|
162
|
-
| `RETRY_BASE_DELAY_MS` | No | `1000` | 重试基础延迟(ms) |
|
|
199
|
+
MIT
|
package/dist/admin/logs.js
CHANGED
|
@@ -7,6 +7,9 @@ const LogQuerySchema = Type.Object({
|
|
|
7
7
|
api_type: Type.Optional(Type.String()),
|
|
8
8
|
model: Type.Optional(Type.String()),
|
|
9
9
|
router_key_id: Type.Optional(Type.String()),
|
|
10
|
+
provider_id: Type.Optional(Type.String()),
|
|
11
|
+
start_time: Type.Optional(Type.String()),
|
|
12
|
+
end_time: Type.Optional(Type.String()),
|
|
10
13
|
view: Type.Optional(Type.Literal("grouped")),
|
|
11
14
|
});
|
|
12
15
|
const DeleteLogsBeforeSchema = Type.Object({
|
|
@@ -26,6 +29,9 @@ export const adminLogRoutes = (app, options, done) => {
|
|
|
26
29
|
api_type: query.api_type || undefined,
|
|
27
30
|
model: query.model || undefined,
|
|
28
31
|
router_key_id: query.router_key_id || undefined,
|
|
32
|
+
provider_id: query.provider_id || undefined,
|
|
33
|
+
start_time: query.start_time || undefined,
|
|
34
|
+
end_time: query.end_time || undefined,
|
|
29
35
|
};
|
|
30
36
|
const result = view === "grouped"
|
|
31
37
|
? getRequestLogsGrouped(db, listOptions)
|
|
@@ -47,7 +53,7 @@ export const adminLogRoutes = (app, options, done) => {
|
|
|
47
53
|
return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
|
|
48
54
|
}
|
|
49
55
|
const rows = getRequestLogChildren(db, params.id);
|
|
50
|
-
return reply.send(
|
|
56
|
+
return reply.send(rows);
|
|
51
57
|
});
|
|
52
58
|
app.delete("/admin/api/logs/before", { schema: { body: DeleteLogsBeforeSchema } }, async (request, reply) => {
|
|
53
59
|
const body = request.body;
|
package/dist/admin/metrics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getMetricsSummary, getMetricsTimeseries } from "../db/index.js";
|
|
3
3
|
const PeriodEnum = Type.Union([
|
|
4
|
-
Type.Literal("1h"), Type.Literal("6h"), Type.Literal("24h"),
|
|
4
|
+
Type.Literal("1h"), Type.Literal("5h"), Type.Literal("6h"), Type.Literal("24h"),
|
|
5
5
|
Type.Literal("7d"), Type.Literal("30d"),
|
|
6
6
|
]);
|
|
7
7
|
const MetricEnum = Type.Union([
|
|
@@ -15,6 +15,8 @@ const SummaryQuerySchema = Type.Object({
|
|
|
15
15
|
provider_id: Type.Optional(Type.String()),
|
|
16
16
|
backend_model: Type.Optional(Type.String()),
|
|
17
17
|
router_key_id: Type.Optional(Type.String()),
|
|
18
|
+
start_time: Type.Optional(Type.String()),
|
|
19
|
+
end_time: Type.Optional(Type.String()),
|
|
18
20
|
});
|
|
19
21
|
const TimeseriesQuerySchema = Type.Object({
|
|
20
22
|
period: Type.Optional(PeriodEnum),
|
|
@@ -22,19 +24,21 @@ const TimeseriesQuerySchema = Type.Object({
|
|
|
22
24
|
provider_id: Type.Optional(Type.String()),
|
|
23
25
|
backend_model: Type.Optional(Type.String()),
|
|
24
26
|
router_key_id: Type.Optional(Type.String()),
|
|
27
|
+
start_time: Type.Optional(Type.String()),
|
|
28
|
+
end_time: Type.Optional(Type.String()),
|
|
25
29
|
});
|
|
26
30
|
export const adminMetricsRoutes = (app, options, done) => {
|
|
27
31
|
app.get("/admin/api/metrics/summary", { schema: { querystring: SummaryQuerySchema } }, async (request, reply) => {
|
|
28
32
|
const query = request.query;
|
|
29
33
|
const period = (query.period ?? "24h");
|
|
30
|
-
const summary = getMetricsSummary(options.db, period, query.provider_id, query.backend_model, query.router_key_id);
|
|
34
|
+
const summary = getMetricsSummary(options.db, period, query.provider_id, query.backend_model, query.router_key_id, query.start_time, query.end_time);
|
|
31
35
|
return reply.send(summary);
|
|
32
36
|
});
|
|
33
37
|
app.get("/admin/api/metrics/timeseries", { schema: { querystring: TimeseriesQuerySchema } }, async (request, reply) => {
|
|
34
38
|
const query = request.query;
|
|
35
39
|
const period = (query.period ?? "24h");
|
|
36
40
|
const metric = query.metric;
|
|
37
|
-
const timeseries = getMetricsTimeseries(options.db, period, metric, query.provider_id, query.backend_model, query.router_key_id);
|
|
41
|
+
const timeseries = getMetricsTimeseries(options.db, period, metric, query.provider_id, query.backend_model, query.router_key_id, query.start_time, query.end_time);
|
|
38
42
|
return reply.send(timeseries);
|
|
39
43
|
});
|
|
40
44
|
done();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getRecommendedProviders, getRecommendedRetryRules, reloadConfig } from "../config/recommended.js";
|
|
2
|
+
export const adminRecommendedRoutes = (app, options, done) => {
|
|
3
|
+
const { db } = options;
|
|
4
|
+
app.get("/admin/api/recommended/providers", async (_req, reply) => {
|
|
5
|
+
const groups = getRecommendedProviders();
|
|
6
|
+
const existing = new Set(db.prepare("SELECT name FROM providers").all().map((r) => r.name));
|
|
7
|
+
const filtered = groups
|
|
8
|
+
.map((g) => ({
|
|
9
|
+
...g,
|
|
10
|
+
presets: g.presets.filter((p) => !existing.has(p.presetName)),
|
|
11
|
+
}))
|
|
12
|
+
.filter((g) => g.presets.length > 0);
|
|
13
|
+
return reply.send(filtered);
|
|
14
|
+
});
|
|
15
|
+
app.get("/admin/api/recommended/retry-rules", async (_req, reply) => {
|
|
16
|
+
const rules = getRecommendedRetryRules();
|
|
17
|
+
const existing = new Set(db.prepare("SELECT name FROM retry_rules").all().map((r) => r.name));
|
|
18
|
+
return reply.send(rules.filter((r) => !existing.has(r.name)));
|
|
19
|
+
});
|
|
20
|
+
app.post("/admin/api/recommended/reload", async (_req, reply) => {
|
|
21
|
+
reloadConfig();
|
|
22
|
+
return reply.send({ ok: true });
|
|
23
|
+
});
|
|
24
|
+
done();
|
|
25
|
+
};
|
package/dist/admin/routes.js
CHANGED
|
@@ -10,6 +10,9 @@ import { adminProxyEnhancementRoutes } from "./proxy-enhancement.js";
|
|
|
10
10
|
import { adminRouterKeyRoutes } from "./router-keys.js";
|
|
11
11
|
import { adminSetupRoutes } from "./setup.js";
|
|
12
12
|
import { adminMonitorRoutes } from "./monitor.js";
|
|
13
|
+
import { adminSettingsRoutes } from "./settings.js";
|
|
14
|
+
import { adminRecommendedRoutes } from "./recommended.js";
|
|
15
|
+
import { adminUsageRoutes } from "./usage.js";
|
|
13
16
|
export const adminRoutes = (app, options, done) => {
|
|
14
17
|
// Setup 路由不需要 auth
|
|
15
18
|
app.register(adminSetupRoutes, { db: options.db });
|
|
@@ -25,5 +28,8 @@ export const adminRoutes = (app, options, done) => {
|
|
|
25
28
|
app.register(adminMetricsRoutes, { db: options.db });
|
|
26
29
|
app.register(adminProxyEnhancementRoutes, { db: options.db });
|
|
27
30
|
app.register(adminMonitorRoutes, { tracker: options.tracker });
|
|
31
|
+
app.register(adminSettingsRoutes, { db: options.db });
|
|
32
|
+
app.register(adminRecommendedRoutes, { db: options.db });
|
|
33
|
+
app.register(adminUsageRoutes, { db: options.db });
|
|
28
34
|
done();
|
|
29
35
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getLogRetentionDays, setLogRetentionDays } from "../db/settings.js";
|
|
2
|
+
export const adminSettingsRoutes = (app, options, done) => {
|
|
3
|
+
const { db } = options;
|
|
4
|
+
app.get("/admin/api/settings/log-retention", async () => {
|
|
5
|
+
return { days: getLogRetentionDays(db) };
|
|
6
|
+
});
|
|
7
|
+
app.put("/admin/api/settings/log-retention", async (request) => {
|
|
8
|
+
const { days } = request.body;
|
|
9
|
+
if (!Number.isInteger(days) || days < 0 || days > 90) {
|
|
10
|
+
throw { statusCode: 400, message: "days must be integer 0-90" };
|
|
11
|
+
}
|
|
12
|
+
setLogRetentionDays(db, days);
|
|
13
|
+
return { days };
|
|
14
|
+
});
|
|
15
|
+
done();
|
|
16
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
|
|
3
|
+
import { toSqliteDatetime } from "../utils/datetime.js";
|
|
4
|
+
const UsageQuerySchema = Type.Object({
|
|
5
|
+
router_key_id: Type.Optional(Type.String()),
|
|
6
|
+
});
|
|
7
|
+
function getMonday(date) {
|
|
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) {
|
|
16
|
+
const routerKeyFilter = routerKeyId
|
|
17
|
+
? " AND rl.router_key_id = ?"
|
|
18
|
+
: "";
|
|
19
|
+
const params = routerKeyId
|
|
20
|
+
? [toSqliteDatetime(start), toSqliteDatetime(end), routerKeyId]
|
|
21
|
+
: [toSqliteDatetime(start), toSqliteDatetime(end)];
|
|
22
|
+
return db.prepare(`
|
|
23
|
+
SELECT
|
|
24
|
+
date(rm.created_at) AS date,
|
|
25
|
+
COUNT(*) AS request_count,
|
|
26
|
+
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
|
|
27
|
+
COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
|
|
28
|
+
FROM request_metrics rm
|
|
29
|
+
JOIN request_logs rl ON rl.id = rm.request_log_id
|
|
30
|
+
WHERE rm.is_complete = 1
|
|
31
|
+
AND rm.created_at >= datetime(?)
|
|
32
|
+
AND rm.created_at < datetime(?)
|
|
33
|
+
${routerKeyFilter}
|
|
34
|
+
GROUP BY date(rm.created_at)
|
|
35
|
+
ORDER BY date ASC
|
|
36
|
+
`).all(...params);
|
|
37
|
+
}
|
|
38
|
+
export const adminUsageRoutes = (app, options, done) => {
|
|
39
|
+
const { db } = options;
|
|
40
|
+
app.get("/admin/api/usage/windows", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
41
|
+
const query = request.query;
|
|
42
|
+
const today = new Date();
|
|
43
|
+
today.setHours(0, 0, 0, 0);
|
|
44
|
+
const tomorrow = new Date(today);
|
|
45
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
46
|
+
const windows = getWindowsInRange(db, toSqliteDatetime(today), toSqliteDatetime(tomorrow), query.router_key_id);
|
|
47
|
+
return windows.map(w => ({
|
|
48
|
+
window: w,
|
|
49
|
+
usage: getWindowUsage(db, w.start_time, w.end_time, query.router_key_id),
|
|
50
|
+
}));
|
|
51
|
+
});
|
|
52
|
+
app.get("/admin/api/usage/weekly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
53
|
+
const query = request.query;
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const monday = getMonday(now);
|
|
56
|
+
monday.setHours(0, 0, 0, 0);
|
|
57
|
+
return getDailyUsage(db, monday, now, query.router_key_id);
|
|
58
|
+
});
|
|
59
|
+
app.get("/admin/api/usage/monthly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
|
|
60
|
+
const query = request.query;
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
63
|
+
return getDailyUsage(db, firstOfMonth, now, query.router_key_id);
|
|
64
|
+
});
|
|
65
|
+
done();
|
|
66
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ProviderPreset {
|
|
2
|
+
plan: string;
|
|
3
|
+
presetName: string;
|
|
4
|
+
apiType: 'openai' | 'anthropic';
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
models: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ProviderGroup {
|
|
9
|
+
group: string;
|
|
10
|
+
presets: ProviderPreset[];
|
|
11
|
+
}
|
|
12
|
+
export interface RecommendedRetryRule {
|
|
13
|
+
name: string;
|
|
14
|
+
status_code: number;
|
|
15
|
+
body_pattern: string;
|
|
16
|
+
retry_strategy: 'fixed' | 'exponential';
|
|
17
|
+
retry_delay_ms: number;
|
|
18
|
+
max_retries: number;
|
|
19
|
+
max_delay_ms: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function loadRecommendedConfig(dir?: string): void;
|
|
22
|
+
export declare function getRecommendedProviders(): ProviderGroup[];
|
|
23
|
+
export declare function getRecommendedRetryRules(): RecommendedRetryRule[];
|
|
24
|
+
export declare function reloadConfig(): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
let cachedProviders = [];
|
|
4
|
+
let cachedRetryRules = [];
|
|
5
|
+
let configDir = '';
|
|
6
|
+
export function loadRecommendedConfig(dir) {
|
|
7
|
+
configDir = dir ?? path.resolve(process.cwd(), 'config');
|
|
8
|
+
cachedProviders = loadJson('recommended-providers.json');
|
|
9
|
+
cachedRetryRules = loadJson('recommended-retry-rules.json');
|
|
10
|
+
}
|
|
11
|
+
export function getRecommendedProviders() {
|
|
12
|
+
return cachedProviders;
|
|
13
|
+
}
|
|
14
|
+
export function getRecommendedRetryRules() {
|
|
15
|
+
return cachedRetryRules;
|
|
16
|
+
}
|
|
17
|
+
export function reloadConfig() {
|
|
18
|
+
loadRecommendedConfig(configDir);
|
|
19
|
+
}
|
|
20
|
+
function loadJson(filename) {
|
|
21
|
+
const filePath = path.join(configDir, filename);
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(filePath))
|
|
24
|
+
return [];
|
|
25
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export { getActiveProviders, getAllProviders, getProviderById, createProvider, u
|
|
|
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,
|
|
7
|
+
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
8
8
|
export type { RetryRule } from "./retry-rules.js";
|
|
9
|
-
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
|
|
9
|
+
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, } 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";
|
|
@@ -17,3 +17,5 @@ export type { Stats, StatsPeriod } from "./stats.js";
|
|
|
17
17
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
18
18
|
export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
|
|
19
19
|
export type { SessionModelState, SessionModelHistory, UpsertSessionStateInput, InsertSessionHistoryInput } from "./session-states.js";
|
|
20
|
+
export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
|
21
|
+
export type { UsageWindow, WindowUsage } from "./usage-windows.js";
|
package/dist/db/index.js
CHANGED
|
@@ -5,11 +5,17 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
7
|
const MIGRATIONS_DIR = join(__dirname, "migrations");
|
|
8
|
+
const MIGRATION_RENAMES = {
|
|
9
|
+
"019_drop_log_redundancy.sql": "020_drop_log_redundancy.sql",
|
|
10
|
+
"020_merge_metrics_columns.sql": "021_merge_metrics_columns.sql",
|
|
11
|
+
};
|
|
8
12
|
export function initDatabase(dbPath) {
|
|
9
13
|
if (dbPath !== ":memory:") {
|
|
10
14
|
mkdirSync(dirname(dbPath), { recursive: true });
|
|
11
15
|
}
|
|
12
16
|
const db = new Database(dbPath);
|
|
17
|
+
db.pragma("journal_mode = WAL");
|
|
18
|
+
db.pragma("foreign_keys = ON");
|
|
13
19
|
db.exec(`
|
|
14
20
|
CREATE TABLE IF NOT EXISTS migrations (
|
|
15
21
|
name TEXT PRIMARY KEY,
|
|
@@ -17,6 +23,14 @@ export function initDatabase(dbPath) {
|
|
|
17
23
|
);
|
|
18
24
|
`);
|
|
19
25
|
const applied = new Set(db.prepare("SELECT name FROM migrations").all().map((r) => r.name));
|
|
26
|
+
// 将已应用的旧文件名更新为新文件名,避免重命名后重复执行
|
|
27
|
+
for (const [oldName, newName] of Object.entries(MIGRATION_RENAMES)) {
|
|
28
|
+
if (applied.has(oldName) && !applied.has(newName)) {
|
|
29
|
+
db.prepare("UPDATE migrations SET name = ? WHERE name = ?").run(newName, oldName);
|
|
30
|
+
applied.delete(oldName);
|
|
31
|
+
applied.add(newName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
20
34
|
const files = readdirSync(MIGRATIONS_DIR)
|
|
21
35
|
.filter((f) => f.endsWith(".sql"))
|
|
22
36
|
.sort();
|
|
@@ -25,23 +39,26 @@ export function initDatabase(dbPath) {
|
|
|
25
39
|
continue;
|
|
26
40
|
try {
|
|
27
41
|
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");
|
|
28
|
-
db.
|
|
42
|
+
db.transaction(() => {
|
|
43
|
+
db.exec(sql);
|
|
44
|
+
db.prepare("INSERT INTO migrations (name, applied_at) VALUES (?, ?)").run(file, new Date().toISOString());
|
|
45
|
+
})();
|
|
29
46
|
}
|
|
30
47
|
catch (err) {
|
|
31
48
|
console.error(`Failed to apply migration ${file}:`, err);
|
|
32
49
|
throw err;
|
|
33
50
|
}
|
|
34
|
-
db.prepare("INSERT INTO migrations (name, applied_at) VALUES (?, ?)").run(file, new Date().toISOString());
|
|
35
51
|
}
|
|
36
52
|
return db;
|
|
37
53
|
}
|
|
38
54
|
// --- Re-export from per-table modules ---
|
|
39
55
|
export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
40
56
|
export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
|
41
|
-
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule,
|
|
42
|
-
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
|
|
57
|
+
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
58
|
+
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, } from "./logs.js";
|
|
43
59
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
44
60
|
export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
|
|
45
61
|
export { getStats } from "./stats.js";
|
|
46
62
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
47
63
|
export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
|
|
64
|
+
export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface LogCleanupHandle {
|
|
3
|
+
stop: () => void;
|
|
4
|
+
}
|
|
5
|
+
/** 运行一次清理,返回删除条数 */
|
|
6
|
+
export declare function runLogCleanup(db: Database.Database): number;
|
|
7
|
+
/** 启动定时清理,返回 handle 用于停止 */
|
|
8
|
+
export declare function scheduleLogCleanup(db: Database.Database, log: {
|
|
9
|
+
info: (msg: string) => void;
|
|
10
|
+
}): LogCleanupHandle;
|