llm-simple-router 0.6.7 → 0.7.1

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.
Files changed (182) hide show
  1. package/README.md +69 -0
  2. package/dist/admin/constants.d.ts +1 -1
  3. package/dist/admin/constants.js +2 -2
  4. package/dist/admin/logs.d.ts +2 -0
  5. package/dist/admin/logs.js +17 -1
  6. package/dist/admin/providers.d.ts +2 -2
  7. package/dist/admin/providers.js +29 -16
  8. package/dist/admin/proxy-enhancement.d.ts +2 -0
  9. package/dist/admin/proxy-enhancement.js +4 -10
  10. package/dist/admin/retry-rules.d.ts +2 -2
  11. package/dist/admin/retry-rules.js +4 -8
  12. package/dist/admin/routes.d.ts +5 -4
  13. package/dist/admin/routes.js +7 -7
  14. package/dist/admin/settings-import-export.d.ts +2 -4
  15. package/dist/admin/settings-import-export.js +9 -19
  16. package/dist/admin/settings.d.ts +1 -0
  17. package/dist/admin/settings.js +29 -1
  18. package/dist/admin/upgrade.d.ts +1 -0
  19. package/dist/admin/upgrade.js +37 -4
  20. package/dist/{constants.d.ts → core/constants.d.ts} +4 -1
  21. package/dist/{constants.js → core/constants.js} +21 -1
  22. package/dist/core/container.d.ts +31 -0
  23. package/dist/core/container.js +41 -0
  24. package/dist/core/errors.d.ts +26 -0
  25. package/dist/core/errors.js +42 -0
  26. package/dist/core/registry.d.ts +43 -0
  27. package/dist/core/registry.js +3 -0
  28. package/dist/core/types.d.ts +105 -0
  29. package/dist/core/types.js +3 -0
  30. package/dist/db/index.d.ts +1 -1
  31. package/dist/db/index.js +1 -1
  32. package/dist/db/logs.d.ts +11 -24
  33. package/dist/db/logs.js +37 -38
  34. package/dist/db/metrics.js +1 -1
  35. package/dist/db/migrations/033_add_pipeline_snapshot.sql +1 -0
  36. package/dist/db/migrations/034_drop_redundant_log_columns.sql +13 -0
  37. package/dist/db/settings.d.ts +2 -0
  38. package/dist/db/settings.js +9 -0
  39. package/dist/index.d.ts +10 -2
  40. package/dist/index.js +196 -108
  41. package/dist/metrics/metrics-extractor.d.ts +1 -24
  42. package/dist/metrics/metrics-extractor.js +1 -1
  43. package/dist/metrics/sse-metrics-transform.d.ts +1 -1
  44. package/dist/middleware/admin-auth.js +4 -0
  45. package/dist/middleware/auth.js +1 -2
  46. package/dist/monitor/request-tracker.d.ts +3 -4
  47. package/dist/monitor/request-tracker.js +6 -16
  48. package/dist/monitor/runtime-collector.js +1 -1
  49. package/dist/monitor/types.d.ts +8 -0
  50. package/dist/proxy/adaptive-controller.d.ts +4 -1
  51. package/dist/proxy/adaptive-controller.js +5 -0
  52. package/dist/proxy/enhancement/enhancement-handler.d.ts +19 -3
  53. package/dist/proxy/enhancement/enhancement-handler.js +80 -28
  54. package/dist/proxy/enhancement/index.d.ts +1 -0
  55. package/dist/proxy/handler/anthropic.d.ts +7 -0
  56. package/dist/proxy/{anthropic.js → handler/anthropic.js} +8 -7
  57. package/dist/proxy/handler/openai.d.ts +7 -0
  58. package/dist/proxy/{openai.js → handler/openai.js} +10 -9
  59. package/dist/proxy/handler/proxy-handler-utils.d.ts +9 -0
  60. package/dist/proxy/handler/proxy-handler-utils.js +63 -0
  61. package/dist/proxy/handler/proxy-handler.d.ts +13 -0
  62. package/dist/proxy/{proxy-handler.js → handler/proxy-handler.js} +104 -120
  63. package/dist/proxy/log-detail-policy.d.ts +12 -0
  64. package/dist/proxy/log-detail-policy.js +21 -0
  65. package/dist/proxy/log-helpers.d.ts +8 -0
  66. package/dist/proxy/log-helpers.js +16 -4
  67. package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
  68. package/dist/proxy/loop-prevention/tool-loop-guard.js +9 -12
  69. package/dist/proxy/{orchestrator.d.ts → orchestration/orchestrator.d.ts} +6 -4
  70. package/dist/proxy/{orchestrator.js → orchestration/orchestrator.js} +2 -1
  71. package/dist/proxy/{resilience.d.ts → orchestration/resilience.d.ts} +2 -14
  72. package/dist/proxy/{resilience.js → orchestration/resilience.js} +2 -2
  73. package/dist/proxy/{retry-rules.d.ts → orchestration/retry-rules.d.ts} +1 -1
  74. package/dist/proxy/{retry-rules.js → orchestration/retry-rules.js} +1 -1
  75. package/dist/proxy/{scope.d.ts → orchestration/scope.d.ts} +3 -3
  76. package/dist/proxy/{semaphore.d.ts → orchestration/semaphore.d.ts} +7 -15
  77. package/dist/proxy/{semaphore.js → orchestration/semaphore.js} +12 -26
  78. package/dist/proxy/patch/index.d.ts +8 -2
  79. package/dist/proxy/patch/index.js +5 -2
  80. package/dist/proxy/pipeline-snapshot.d.ts +37 -0
  81. package/dist/proxy/pipeline-snapshot.js +15 -0
  82. package/dist/proxy/proxy-core.d.ts +1 -1
  83. package/dist/proxy/proxy-core.js +1 -1
  84. package/dist/proxy/proxy-logging.d.ts +10 -2
  85. package/dist/proxy/proxy-logging.js +23 -9
  86. package/dist/proxy/response-transform.d.ts +7 -0
  87. package/dist/proxy/response-transform.js +15 -0
  88. package/dist/proxy/{enhancement-config.js → routing/enhancement-config.js} +1 -1
  89. package/dist/proxy/{mapping-resolver.d.ts → routing/mapping-resolver.d.ts} +1 -1
  90. package/dist/proxy/{mapping-resolver.js → routing/mapping-resolver.js} +1 -1
  91. package/dist/proxy/{model-state.js → routing/model-state.js} +1 -1
  92. package/dist/proxy/{overflow.d.ts → routing/overflow.d.ts} +1 -1
  93. package/dist/proxy/{overflow.js → routing/overflow.js} +3 -3
  94. package/dist/proxy/{usage-window-tracker.js → routing/usage-window-tracker.js} +3 -3
  95. package/dist/proxy/{transport.d.ts → transport/http.d.ts} +2 -2
  96. package/dist/proxy/{transport.js → transport/http.js} +3 -3
  97. package/dist/proxy/{stream-proxy.d.ts → transport/stream.d.ts} +4 -4
  98. package/dist/proxy/{stream-proxy.js → transport/stream.js} +25 -7
  99. package/dist/proxy/{transport-fn.d.ts → transport/transport-fn.d.ts} +5 -5
  100. package/dist/proxy/{transport-fn.js → transport/transport-fn.js} +11 -9
  101. package/dist/proxy/types.d.ts +3 -64
  102. package/dist/proxy/types.js +5 -34
  103. package/dist/storage/log-file-compressor.d.ts +15 -0
  104. package/dist/storage/log-file-compressor.js +83 -0
  105. package/dist/storage/log-file-writer.d.ts +21 -0
  106. package/dist/storage/log-file-writer.js +103 -0
  107. package/dist/storage/types.d.ts +16 -0
  108. package/dist/storage/types.js +5 -0
  109. package/dist/upgrade/deployment.d.ts +13 -0
  110. package/dist/upgrade/deployment.js +40 -0
  111. package/frontend-dist/assets/{CardContent-jQcfCC7J.js → CardContent-CxOF1feY.js} +1 -1
  112. package/frontend-dist/assets/{CardTitle-BrCTvULL.js → CardTitle-BSEFcEOM.js} +1 -1
  113. package/frontend-dist/assets/{CascadingModelSelect-BFh67j5d.js → CascadingModelSelect-DTwksDPZ.js} +1 -1
  114. package/frontend-dist/assets/{Checkbox-Bbt7JpdE.js → Checkbox-RfsERG07.js} +1 -1
  115. package/frontend-dist/assets/{CollapsibleTrigger-DMnEA0qC.js → CollapsibleTrigger-Dsjo7QlC.js} +1 -1
  116. package/frontend-dist/assets/{Collection-CVk3TPHc.js → Collection-rQ4eIYfa.js} +1 -1
  117. package/frontend-dist/assets/{Dashboard-Coftbg4B.js → Dashboard-YejfAPiB.js} +1 -1
  118. package/frontend-dist/assets/{DialogTitle-BbOAZzPQ.js → DialogTitle-DeFTnmgC.js} +1 -1
  119. package/frontend-dist/assets/{Input-DdHY9q0w.js → Input-CENz_g9t.js} +1 -1
  120. package/frontend-dist/assets/{Label-DRQv_Dr_.js → Label-BAciBrrd.js} +1 -1
  121. package/frontend-dist/assets/{Login-SV3ctFnJ.js → Login-DQkYFq7R.js} +1 -1
  122. package/frontend-dist/assets/{Logs-BG45kX6E.js → Logs-Dol8AX7z.js} +1 -1
  123. package/frontend-dist/assets/{ModelMappings-DEaBnRU3.js → ModelMappings-VEYW1TrW.js} +1 -1
  124. package/frontend-dist/assets/{Monitor-ZHOt11n-.js → Monitor-C0r9WefB.js} +1 -1
  125. package/frontend-dist/assets/{PopoverTrigger-z-Z3EjBk.js → PopoverTrigger-Cyqik5SE.js} +1 -1
  126. package/frontend-dist/assets/{PopperContent-DPC-6a3n.js → PopperContent-B7IuAHeq.js} +1 -1
  127. package/frontend-dist/assets/{Providers-DpY6pAcg.js → Providers-D8Z97edN.js} +1 -1
  128. package/frontend-dist/assets/{ProxyEnhancement-D6KBDXMp.js → ProxyEnhancement-Kn8r2SN6.js} +1 -1
  129. package/frontend-dist/assets/{RetryRules-DWI7_WLZ.js → RetryRules-F0295m4_.js} +1 -1
  130. package/frontend-dist/assets/{RouterKeys-CZ1657eX.js → RouterKeys-CFbPtUE_.js} +1 -1
  131. package/frontend-dist/assets/{RovingFocusItem-BREE2YEV.js → RovingFocusItem-D291Vjh8.js} +1 -1
  132. package/frontend-dist/assets/{Schedules-BVPsBRPi.js → Schedules-DWhF3uod.js} +1 -1
  133. package/frontend-dist/assets/{SelectValue-H8hwQwbk.js → SelectValue-BWlgUZa3.js} +1 -1
  134. package/frontend-dist/assets/Settings-BnIzEF_k.js +6 -0
  135. package/frontend-dist/assets/{Setup-yOYNKkOG.js → Setup-BglKyQKq.js} +1 -1
  136. package/frontend-dist/assets/{Switch-CojD3rTH.js → Switch-DyCR-CPu.js} +1 -1
  137. package/frontend-dist/assets/{TableHeader-awoHTsWN.js → TableHeader-DVUlBL35.js} +1 -1
  138. package/frontend-dist/assets/{TabsTrigger-DTKSFj85.js → TabsTrigger-BU1DY-C8.js} +1 -1
  139. package/frontend-dist/assets/{Teleport-DehYAXud.js → Teleport-BQgusr9g.js} +1 -1
  140. package/frontend-dist/assets/{TooltipTrigger-C2dl_dml.js → TooltipTrigger-Bv_QoBns.js} +1 -1
  141. package/frontend-dist/assets/{UnifiedRequestDialog-C8A-uSTR.js → UnifiedRequestDialog-f_evI835.js} +2 -2
  142. package/frontend-dist/assets/{VisuallyHidden-C8oaGi2S.js → VisuallyHidden-Con10z4F.js} +1 -1
  143. package/frontend-dist/assets/{VisuallyHiddenInput-BMc813t2.js → VisuallyHiddenInput-yrDtxucb.js} +1 -1
  144. package/frontend-dist/assets/{alert-dialog-C8TZQmU6.js → alert-dialog-2Db6Z7JQ.js} +1 -1
  145. package/frontend-dist/assets/arrow-down-WyouvE7T.js +1 -0
  146. package/frontend-dist/assets/{badge-BVh2WpA5.js → badge-DEhZfeI0.js} +1 -1
  147. package/frontend-dist/assets/button-Cnkbp_6J.js +12 -0
  148. package/frontend-dist/assets/check-BuqB5Nyb.js +1 -0
  149. package/frontend-dist/assets/{copy-DTOecxa9.js → copy-CwqZSuIG.js} +1 -1
  150. package/frontend-dist/assets/{dialog-kA7AUNoc.js → dialog-CVMKSdPr.js} +1 -1
  151. package/frontend-dist/assets/{file-text-DzZCFO7y.js → file-text-D0K8Hovo.js} +1 -1
  152. package/frontend-dist/assets/index-Ct718O93.js +1 -0
  153. package/frontend-dist/assets/{lib-ClDokUbt.js → lib-H3YI7EK4.js} +1 -1
  154. package/frontend-dist/assets/loader-circle-Be82FnVY.js +1 -0
  155. package/frontend-dist/assets/{useClipboard-DU1ne-Jw.js → useClipboard-Cd7k-5Yq.js} +1 -1
  156. package/frontend-dist/assets/{useFocusGuards-Btmdbg_F.js → useFocusGuards-luoLXnwV.js} +1 -1
  157. package/frontend-dist/assets/useFormControl-Da4ViGZF.js +1 -0
  158. package/frontend-dist/assets/{useLogRetention--EGNWXig.js → useLogRetention-DB4Iu6o_.js} +1 -1
  159. package/frontend-dist/assets/useNonce-DvAdQ48J.js +1 -0
  160. package/frontend-dist/assets/x-DB22csQl.js +1 -0
  161. package/frontend-dist/index.html +19 -19
  162. package/package.json +1 -1
  163. package/dist/proxy/anthropic.d.ts +0 -19
  164. package/dist/proxy/openai.d.ts +0 -19
  165. package/dist/proxy/proxy-handler.d.ts +0 -19
  166. package/dist/proxy/strategy/types.d.ts +0 -21
  167. package/dist/proxy/strategy/types.js +0 -1
  168. package/frontend-dist/assets/Settings-DHYaYRgU.js +0 -6
  169. package/frontend-dist/assets/arrow-down-D-cQXxau.js +0 -1
  170. package/frontend-dist/assets/button-N59D1BGa.js +0 -12
  171. package/frontend-dist/assets/check-dDgrw3T3.js +0 -1
  172. package/frontend-dist/assets/index-DVTeNVaa.js +0 -1
  173. package/frontend-dist/assets/loader-circle-DVHRL-38.js +0 -1
  174. package/frontend-dist/assets/useFormControl-C5Kjziuj.js +0 -1
  175. package/frontend-dist/assets/useNonce-Cp31yRzV.js +0 -1
  176. package/frontend-dist/assets/x-DMktsI_w.js +0 -1
  177. /package/dist/{config.d.ts → config/index.d.ts} +0 -0
  178. /package/dist/{config.js → config/index.js} +0 -0
  179. /package/dist/proxy/{scope.js → orchestration/scope.js} +0 -0
  180. /package/dist/proxy/{enhancement-config.d.ts → routing/enhancement-config.d.ts} +0 -0
  181. /package/dist/proxy/{model-state.d.ts → routing/model-state.d.ts} +0 -0
  182. /package/dist/proxy/{usage-window-tracker.d.ts → routing/usage-window-tracker.d.ts} +0 -0
package/README.md CHANGED
@@ -170,6 +170,75 @@ docker compose up -d
170
170
 
171
171
  环境变量通过 Setup 页面设置,不需要 `.env` 文件。
172
172
 
173
+ ## 进程管理
174
+
175
+ 通过 Web UI 一键升级后,服务需要重启才能生效。推荐使用以下方式部署,确保进程崩溃或升级重启后自动恢复。
176
+
177
+ ### PM2(推荐)
178
+
179
+ ```bash
180
+ # 安装 PM2
181
+ npm install -g pm2
182
+
183
+ # 全局安装 Router
184
+ npm install -g llm-simple-router
185
+
186
+ # 启动(PM2 自动重启崩溃的进程)
187
+ pm2 start llm-simple-router --name llm-router
188
+
189
+ # 查看日志
190
+ pm2 logs llm-router
191
+
192
+ # 设置开机自启
193
+ pm2 startup
194
+ pm2 save
195
+ ```
196
+
197
+ 升级流程:Web UI 一键升级 → 点击重启 → PM2 自动拉起新进程(< 1s 中断)。
198
+
199
+ ### systemd(Linux 服务器)
200
+
201
+ 创建服务文件 `/etc/systemd/system/llm-simple-router.service`:
202
+
203
+ ```ini
204
+ [Unit]
205
+ Description=LLM Simple Router
206
+ After=network.target
207
+
208
+ [Service]
209
+ Type=simple
210
+ ExecStart=/usr/local/bin/llm-simple-router
211
+ Restart=always
212
+ RestartSec=3
213
+ Environment=PORT=9981
214
+ Environment=LOG_LEVEL=info
215
+ # 按需配置其他环境变量
216
+ # Environment=DB_PATH=/var/lib/llm-simple-router/router.db
217
+
218
+ [Install]
219
+ WantedBy=multi-user.target
220
+ ```
221
+
222
+ > **注意**:`ExecStart` 路径取决于 Node.js 安装方式。用 `which llm-simple-router` 确认实际路径。
223
+
224
+ ```bash
225
+ # 启用并启动
226
+ sudo systemctl enable llm-simple-router
227
+ sudo systemctl start llm-simple-router
228
+
229
+ # 查看状态和日志
230
+ sudo systemctl status llm-simple-router
231
+ journalctl -u llm-simple-router -f
232
+ ```
233
+
234
+ 升级流程:Web UI 一键升级 → 点击重启 → systemd 自动重启(< 1s 中断)。
235
+
236
+ ### npx / 手动启动
237
+
238
+ 无需额外配置。Web UI 升级并点击重启后,Router 会自动 spawn 新进程并退出旧进程。短暂中断约 1-2 秒。
239
+
240
+ > **注意**:如果直接 `Ctrl+C` 或终端关闭,服务不会自动恢复。建议生产环境使用 PM2 或 systemd。
241
+
173
242
  ## 工作原理
174
243
 
175
244
  ```
@@ -1 +1 @@
1
- export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../constants.js";
1
+ export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../core/constants.js";
@@ -1,2 +1,2 @@
1
- // HTTP 状态码统一从 src/constants.ts 导入,避免重复定义
2
- export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../constants.js";
1
+ // HTTP 状态码统一从 core/constants.ts 导入,避免重复定义
2
+ export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../core/constants.js";
@@ -1,7 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
+ import type { LogFileWriter } from "../storage/log-file-writer.js";
3
4
  interface LogRoutesOptions {
4
5
  db: Database.Database;
6
+ logFileWriter?: LogFileWriter | null;
5
7
  }
6
8
  export declare const adminLogRoutes: FastifyPluginCallback<LogRoutesOptions>;
7
9
  export {};
@@ -19,7 +19,7 @@ const DeleteLogsBeforeSchema = Type.Object({
19
19
  });
20
20
  const DEFAULT_LOG_VIEW = "flat";
21
21
  export const adminLogRoutes = (app, options, done) => {
22
- const { db } = options;
22
+ const { db, logFileWriter } = options;
23
23
  app.get("/admin/api/logs", { schema: { querystring: LogQuerySchema } }, async (request, reply) => {
24
24
  const query = request.query;
25
25
  const page = parseInt(query.page || "1", 10);
@@ -47,6 +47,22 @@ export const adminLogRoutes = (app, options, done) => {
47
47
  if (!log) {
48
48
  return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Log not found"));
49
49
  }
50
+ // DB 字段为 null 时,从 JSONL 文件回填详情
51
+ const needsBackfill = log.client_request === null || log.upstream_request === null || log.upstream_response === null;
52
+ if (needsBackfill && logFileWriter && logFileWriter.isEnabled && log.created_at) {
53
+ const fileEntry = logFileWriter.read(log.id, log.created_at);
54
+ if (fileEntry) {
55
+ if (log.client_request === null && fileEntry.client_request !== null) {
56
+ log.client_request = fileEntry.client_request;
57
+ }
58
+ if (log.upstream_request === null && fileEntry.upstream_request !== null) {
59
+ log.upstream_request = fileEntry.upstream_request;
60
+ }
61
+ if (log.upstream_response === null && fileEntry.upstream_response !== null) {
62
+ log.upstream_response = fileEntry.upstream_response;
63
+ }
64
+ }
65
+ }
50
66
  return reply.send(log);
51
67
  });
52
68
  app.get("/admin/api/logs/:id/children", async (request, reply) => {
@@ -1,11 +1,11 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
3
+ import type { StateRegistry } from "../core/registry.js";
4
4
  import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
5
5
  import type { RequestTracker } from "../monitor/request-tracker.js";
6
6
  interface ProviderRoutesOptions {
7
7
  db: Database.Database;
8
- semaphoreManager?: ProviderSemaphoreManager;
8
+ stateRegistry?: StateRegistry;
9
9
  tracker?: RequestTracker;
10
10
  adaptiveController?: AdaptiveConcurrencyController;
11
11
  }
@@ -95,7 +95,7 @@ const UpdateProviderSchema = Type.Object({
95
95
  adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
96
96
  });
97
97
  export const adminProviderRoutes = (app, options, done) => {
98
- const { db, semaphoreManager, tracker, adaptiveController } = options;
98
+ const { db, stateRegistry, tracker, adaptiveController } = options;
99
99
  app.get("/admin/api/providers", async (_request, reply) => {
100
100
  const encryptionKey = getSetting(db, "encryption_key");
101
101
  const providers = getAllProviders(db);
@@ -114,7 +114,7 @@ export const adminProviderRoutes = (app, options, done) => {
114
114
  queue_timeout_ms: s.queue_timeout_ms,
115
115
  max_queue_size: s.max_queue_size,
116
116
  adaptive_enabled: s.adaptive_enabled,
117
- concurrency_status: semaphoreManager?.getStatus(s.id) ?? { active: 0, queued: 0 },
117
+ concurrency_status: stateRegistry?.getProviderStatus(s.id) ?? { active: 0, queued: 0 },
118
118
  created_at: s.created_at,
119
119
  updated_at: s.updated_at,
120
120
  };
@@ -148,11 +148,14 @@ export const adminProviderRoutes = (app, options, done) => {
148
148
  if (contextOverrides.length > 0) {
149
149
  setModelInfoForProvider(db, id, contextOverrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
150
150
  }
151
- semaphoreManager?.updateConfig(id, {
152
- maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
153
- queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
154
- maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
155
- });
151
+ // 当 adaptive 启用时,由 syncProvider 全权管理信号量(避免重复调用 updateConfig
152
+ if (!isAdaptiveEnabled) {
153
+ stateRegistry?.updateProviderConcurrency(id, {
154
+ maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
155
+ queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
156
+ maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
157
+ });
158
+ }
156
159
  adaptiveController?.syncProvider(id, {
157
160
  adaptive_enabled: isAdaptiveEnabled,
158
161
  max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
@@ -213,21 +216,31 @@ export const adminProviderRoutes = (app, options, done) => {
213
216
  let cascade;
214
217
  if (existing.is_active === 1 && body.is_active === 0) {
215
218
  cascade = cascadeProviderDisable(db, id);
219
+ // 禁用时清理信号量和自适应并发,避免排队请求悬挂
220
+ stateRegistry?.removeProvider(id);
221
+ adaptiveController?.remove(id);
216
222
  }
217
- if (body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined) {
218
- semaphoreManager?.updateConfig(id, {
219
- maxConcurrency: updated.max_concurrency,
220
- queueTimeoutMs: updated.queue_timeout_ms,
221
- maxQueueSize: updated.max_queue_size,
222
- });
223
- }
224
- if (body.adaptive_enabled !== undefined || body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined) {
223
+ // 重新启用时重建信号量和自适应并发
224
+ const concurrencyChanged = body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined;
225
+ const adaptiveChanged = body.adaptive_enabled !== undefined;
226
+ const reenabled = existing.is_active === 0 && body.is_active === 1;
227
+ const needsSync = concurrencyChanged || adaptiveChanged || reenabled;
228
+ if (needsSync) {
229
+ // adaptive 同步:syncProvider 内部根据 adaptive_enabled 决定是 init+syncToSemaphore 还是 remove+updateConfig
225
230
  adaptiveController?.syncProvider(id, {
226
231
  adaptive_enabled: updated.adaptive_enabled,
227
232
  max_concurrency: updated.max_concurrency,
228
233
  queue_timeout_ms: updated.queue_timeout_ms,
229
234
  max_queue_size: updated.max_queue_size,
230
235
  });
236
+ // 非 adaptive 模式下手动同步信号量(adaptive 启用时由 syncProvider 内部管理)
237
+ if (!updated.adaptive_enabled) {
238
+ stateRegistry?.updateProviderConcurrency(id, {
239
+ maxConcurrency: updated.max_concurrency,
240
+ queueTimeoutMs: updated.queue_timeout_ms,
241
+ maxQueueSize: updated.max_queue_size,
242
+ });
243
+ }
231
244
  }
232
245
  tracker?.updateProviderConfig(id, {
233
246
  name: body.name ?? existing.name,
@@ -292,7 +305,7 @@ export const adminProviderRoutes = (app, options, done) => {
292
305
  }
293
306
  }
294
307
  deleteProvider(db, id);
295
- semaphoreManager?.remove(id);
308
+ stateRegistry?.removeProvider(id);
296
309
  adaptiveController?.remove(id);
297
310
  tracker?.removeProviderConfig(id);
298
311
  return reply.send({ success: true });
@@ -1,7 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
+ import type { StateRegistry } from "../core/registry.js";
3
4
  interface ProxyEnhancementOptions {
4
5
  db: Database.Database;
6
+ stateRegistry?: StateRegistry;
5
7
  }
6
8
  export declare const adminProxyEnhancementRoutes: FastifyPluginCallback<ProxyEnhancementOptions>;
7
9
  export {};
@@ -1,6 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { setSetting } from "../db/settings.js";
3
- import { loadEnhancementConfig } from "../proxy/enhancement-config.js";
4
3
  const UpdateProxyEnhancementSchema = Type.Object({
5
4
  claude_code_enabled: Type.Boolean(),
6
5
  tool_call_loop_enabled: Type.Boolean(),
@@ -11,16 +10,11 @@ const SessionParamsSchema = Type.Object({
11
10
  sessionId: Type.String(),
12
11
  });
13
12
  import { getSessionStates, getSessionHistory, } from "../db/session-states.js";
14
- import { modelState } from "../proxy/model-state.js";
15
13
  export const adminProxyEnhancementRoutes = (app, options, done) => {
16
- const { db } = options;
14
+ const { db, stateRegistry } = options;
17
15
  app.get("/admin/api/proxy-enhancement", async (_request, reply) => {
18
- const config = loadEnhancementConfig(db);
19
- return reply.send({
20
- claude_code_enabled: config.claude_code_enabled,
21
- tool_call_loop_enabled: config.tool_call_loop_enabled,
22
- stream_loop_enabled: config.stream_loop_enabled,
23
- });
16
+ const config = stateRegistry?.getEnhancementConfig() ?? { claude_code_enabled: false, tool_call_loop_enabled: false, stream_loop_enabled: false };
17
+ return reply.send(config);
24
18
  });
25
19
  app.put("/admin/api/proxy-enhancement", { schema: { body: UpdateProxyEnhancementSchema } }, async (request, reply) => {
26
20
  const body = request.body;
@@ -43,7 +37,7 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
43
37
  });
44
38
  app.delete("/admin/api/session-states/:keyId/:sessionId", { schema: { params: SessionParamsSchema } }, async (req, reply) => {
45
39
  const { keyId, sessionId } = req.params;
46
- modelState.delete(keyId, sessionId);
40
+ stateRegistry?.deleteModelState(keyId, sessionId);
47
41
  return reply.send({ success: true });
48
42
  });
49
43
  done();
@@ -1,9 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { RetryRuleMatcher } from "../proxy/retry-rules.js";
3
+ import type { StateRegistry } from "../core/registry.js";
4
4
  interface RetryRuleRoutesOptions {
5
5
  db: Database.Database;
6
- matcher: RetryRuleMatcher | null;
6
+ stateRegistry?: StateRegistry;
7
7
  }
8
8
  export declare const adminRetryRuleRoutes: FastifyPluginCallback<RetryRuleRoutesOptions>;
9
9
  export {};
@@ -34,12 +34,8 @@ function validateBodyPattern(pattern) {
34
34
  return "Invalid body_pattern regex";
35
35
  }
36
36
  }
37
- function refreshMatcher(matcher, db) {
38
- if (matcher)
39
- matcher.load(db);
40
- }
41
37
  export const adminRetryRuleRoutes = (app, options, done) => {
42
- const { db, matcher } = options;
38
+ const { db, stateRegistry } = options;
43
39
  app.get("/admin/api/retry-rules", async (_request, reply) => {
44
40
  const rules = getAllRetryRules(db);
45
41
  return reply.send(rules);
@@ -60,7 +56,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
60
56
  max_retries: body.max_retries ?? DEFAULT_MAX_RETRIES,
61
57
  max_delay_ms: body.max_delay_ms ?? DEFAULT_MAX_DELAY_MS,
62
58
  });
63
- refreshMatcher(matcher, db);
59
+ stateRegistry?.refreshRetryRules();
64
60
  return reply.code(HTTP_CREATED).send({ id });
65
61
  });
66
62
  app.put("/admin/api/retry-rules/:id", { schema: { body: UpdateRetryRuleSchema } }, async (request, reply) => {
@@ -89,7 +85,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
89
85
  if (body.max_delay_ms !== undefined)
90
86
  fields.max_delay_ms = body.max_delay_ms;
91
87
  updateRetryRule(db, id, fields);
92
- refreshMatcher(matcher, db);
88
+ stateRegistry?.refreshRetryRules();
93
89
  return reply.send({ success: true });
94
90
  });
95
91
  app.delete("/admin/api/retry-rules/:id", async (request, reply) => {
@@ -98,7 +94,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
98
94
  if (!existing)
99
95
  return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Retry rule not found"));
100
96
  deleteRetryRule(db, id);
101
- refreshMatcher(matcher, db);
97
+ stateRegistry?.refreshRetryRules();
102
98
  return reply.send({ success: true });
103
99
  });
104
100
  done();
@@ -1,15 +1,16 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { RetryRuleMatcher } from "../proxy/retry-rules.js";
3
+ import type { StateRegistry } from "../core/registry.js";
4
4
  import type { RequestTracker } from "../monitor/request-tracker.js";
5
- import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
6
5
  import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
7
6
  interface AdminRoutesOptions {
8
7
  db: Database.Database;
9
- matcher: RetryRuleMatcher | null;
8
+ stateRegistry: StateRegistry;
10
9
  tracker?: RequestTracker;
11
- semaphoreManager?: ProviderSemaphoreManager;
12
10
  adaptiveController?: AdaptiveConcurrencyController;
11
+ logFileWriter?: import("../storage/log-file-writer.js").LogFileWriter | null;
12
+ logsDir?: string;
13
+ closeFn?: () => Promise<void>;
13
14
  }
14
15
  export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
15
16
  export {};
@@ -21,21 +21,21 @@ export const adminRoutes = (app, options, done) => {
21
21
  app.register(adminSetupRoutes, { db: options.db });
22
22
  app.register(adminAuthPlugin, { db: options.db });
23
23
  app.register(adminLoginRoutes, { db: options.db });
24
- app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker, adaptiveController: options.adaptiveController });
24
+ app.register(adminProviderRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
25
25
  app.register(adminMappingRoutes, { db: options.db });
26
26
  app.register(adminGroupRoutes, { db: options.db });
27
27
  app.register(adminScheduleRoutes, { db: options.db });
28
- app.register(adminRetryRuleRoutes, { db: options.db, matcher: options.matcher });
29
- app.register(adminLogRoutes, { db: options.db });
28
+ app.register(adminRetryRuleRoutes, { db: options.db, stateRegistry: options.stateRegistry });
29
+ app.register(adminLogRoutes, { db: options.db, logFileWriter: options.logFileWriter });
30
30
  app.register(adminRouterKeyRoutes, { db: options.db });
31
31
  app.register(adminStatsRoutes, { db: options.db });
32
32
  app.register(adminMetricsRoutes, { db: options.db });
33
- app.register(adminProxyEnhancementRoutes, { db: options.db });
33
+ app.register(adminProxyEnhancementRoutes, { db: options.db, stateRegistry: options.stateRegistry });
34
34
  app.register(adminMonitorRoutes, { tracker: options.tracker });
35
- app.register(adminSettingsRoutes, { db: options.db });
36
- app.register(adminImportExportRoutes, { db: options.db, matcher: options.matcher, semaphoreManager: options.semaphoreManager });
35
+ app.register(adminSettingsRoutes, { db: options.db, logsDir: options.logsDir });
36
+ app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry });
37
37
  app.register(adminRecommendedRoutes, { db: options.db });
38
38
  app.register(adminUsageRoutes, { db: options.db });
39
- app.register(adminUpgradeRoutes, { db: options.db });
39
+ app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
40
40
  done();
41
41
  };
@@ -1,11 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { RetryRuleMatcher } from "../proxy/retry-rules.js";
4
- import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
3
+ import type { StateRegistry } from "../core/registry.js";
5
4
  interface ImportExportOptions {
6
5
  db: Database.Database;
7
- matcher: RetryRuleMatcher | null;
8
- semaphoreManager?: ProviderSemaphoreManager;
6
+ stateRegistry: StateRegistry;
9
7
  }
10
8
  export declare const adminImportExportRoutes: FastifyPluginCallback<ImportExportOptions>;
11
9
  export {};
@@ -1,8 +1,6 @@
1
1
  import { createHash } from "crypto";
2
- import { getAllProviders, PROVIDER_CONCURRENCY_DEFAULTS } from "../db/index.js";
3
2
  import { encrypt, decrypt } from "../utils/crypto.js";
4
3
  import { getSetting } from "../db/settings.js";
5
- import { modelState } from "../proxy/model-state.js";
6
4
  import { API_CODE, apiError } from "./api-response.js";
7
5
  const CONFIG_TABLES = [
8
6
  "providers",
@@ -11,6 +9,9 @@ const CONFIG_TABLES = [
11
9
  "router_keys",
12
10
  "settings",
13
11
  "session_model_states",
12
+ "schedules",
13
+ "provider_model_info",
14
+ "session_model_history",
14
15
  ];
15
16
  // settings 表按 key 列的值过滤,不覆盖本地安全敏感配置
16
17
  const PROTECTED_SETTING_KEYS = new Set(["admin_password_hash", "jwt_secret", "encryption_key"]);
@@ -19,7 +20,7 @@ const ISO_DATE_LENGTH = 10;
19
20
  const BAD_REQUEST = 400;
20
21
  const KEY_PREFIX_LENGTH = 8;
21
22
  export const adminImportExportRoutes = (app, options, done) => {
22
- const { db, matcher, semaphoreManager } = options;
23
+ const { db, stateRegistry } = options;
23
24
  app.get("/admin/api/settings/export", async (_request, reply) => {
24
25
  const encryptionKey = getSetting(db, "encryption_key");
25
26
  const data = {};
@@ -123,22 +124,11 @@ export const adminImportExportRoutes = (app, options, done) => {
123
124
  db.pragma(`foreign_keys = ${prevFk}`);
124
125
  })();
125
126
  // 导入成功后刷新内存缓存
126
- if (matcher)
127
- matcher.load(db);
128
- if (semaphoreManager) {
129
- // 清除旧的 semaphore 配置,按导入后的 providers 表重建
130
- semaphoreManager.removeAll();
131
- const providers = getAllProviders(db);
132
- for (const p of providers) {
133
- semaphoreManager.updateConfig(p.id, {
134
- maxConcurrency: p.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
135
- queueTimeoutMs: p.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
136
- maxQueueSize: p.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
137
- });
138
- }
139
- }
140
- // session_model_states 已通过 DB 导入,内存缓存会在读取时自然回填
141
- modelState.clearAll();
127
+ stateRegistry.refreshRetryRules();
128
+ // 清除旧的 semaphore/adaptive/tracker 配置,按导入后的 DB 数据全量重建
129
+ stateRegistry.removeAllProviders();
130
+ stateRegistry.clearModelState();
131
+ stateRegistry.reinitializeProviders();
142
132
  return reply.send(counts);
143
133
  });
144
134
  done();
@@ -2,6 +2,7 @@ import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
3
  interface SettingsOptions {
4
4
  db: Database.Database;
5
+ logsDir?: string;
5
6
  }
6
7
  export declare const adminSettingsRoutes: FastifyPluginCallback<SettingsOptions>;
7
8
  export {};
@@ -1,8 +1,10 @@
1
+ import { statSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, } from "../db/settings.js";
2
4
  import { HTTP_BAD_REQUEST } from "./constants.js";
3
5
  import { API_CODE, apiError } from "./api-response.js";
4
6
  export const adminSettingsRoutes = (app, options, done) => {
5
- const { db } = options;
7
+ const { db, logsDir } = options;
6
8
  app.get("/admin/api/settings/log-retention", async () => {
7
9
  return { days: getLogRetentionDays(db) };
8
10
  });
@@ -25,8 +27,17 @@ export const adminSettingsRoutes = (app, options, done) => {
25
27
  }
26
28
  catch { /* eslint-disable-line taste/no-silent-catch -- 损坏的缓存值,回退默认 */ }
27
29
  }
30
+ // 计算日志文件目录大小
31
+ let logFileBytes = 0;
32
+ if (logsDir) {
33
+ try {
34
+ logFileBytes = calcDirSize(logsDir);
35
+ }
36
+ catch { /* eslint-disable-line taste/no-silent-catch -- 目录可能不存在 */ }
37
+ }
28
38
  return {
29
39
  ...sizeInfo,
40
+ logFileBytes,
30
41
  thresholds: {
31
42
  dbMaxSizeMb: getDbMaxSizeMb(db),
32
43
  logTableMaxSizeMb: getLogTableMaxSizeMb(db),
@@ -54,3 +65,20 @@ export const adminSettingsRoutes = (app, options, done) => {
54
65
  });
55
66
  done();
56
67
  };
68
+ /** 递归计算目录下所有文件的总大小(字节) */
69
+ function calcDirSize(dirPath) {
70
+ let total = 0;
71
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
72
+ const fullPath = join(dirPath, entry.name);
73
+ if (entry.isDirectory()) {
74
+ total += calcDirSize(fullPath);
75
+ }
76
+ else if (entry.isFile()) {
77
+ try {
78
+ total += statSync(fullPath).size;
79
+ }
80
+ catch { /* 文件可能刚被删除 */ }
81
+ }
82
+ }
83
+ return total;
84
+ }
@@ -3,6 +3,7 @@ import Database from 'better-sqlite3';
3
3
  import { CheckerOptions } from '../upgrade/checker.js';
4
4
  interface UpgradeRoutesOptions {
5
5
  db: Database.Database;
6
+ closeFn: () => Promise<void>;
6
7
  }
7
8
  export declare function startUpgradeChecker(opts?: CheckerOptions): {
8
9
  check: (sourceOverride?: string) => Promise<void>;
@@ -1,11 +1,11 @@
1
1
  import { getConfigSyncSource, setConfigSyncSource } from '../db/settings.js';
2
- import { detectDeployment } from '../upgrade/deployment.js';
2
+ import { detectDeployment, hasProcessManager, resolveRestartBinPath, getRestartMethod } from '../upgrade/deployment.js';
3
3
  import { createUpgradeChecker, fetchJson } from '../upgrade/checker.js';
4
4
  import { reloadConfig } from '../config/recommended.js';
5
- import { execSync } from 'node:child_process';
5
+ import { execSync, spawn } from 'node:child_process';
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
- import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../constants.js';
8
+ import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../core/constants.js';
9
9
  import { API_CODE, apiError } from './api-response.js';
10
10
  const GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-simple-router/main/config';
11
11
  const GITEE_CONFIG_BASE = 'https://gitee.com/zzzzswszzzz/llm-simple-router/raw/main/config';
@@ -38,7 +38,8 @@ export const adminUpgradeRoutes = (app, options, done) => {
38
38
  const c = checker ?? createUpgradeChecker();
39
39
  const deployment = detectDeployment();
40
40
  const syncSource = getConfigSyncSource(db);
41
- return reply.send({ ...c.getStatus(), deployment, syncSource });
41
+ const restartMethod = getRestartMethod();
42
+ return reply.send({ ...c.getStatus(), deployment, syncSource, restartMethod });
42
43
  });
43
44
  app.post('/admin/api/upgrade/check', async (_req, reply) => {
44
45
  const c = checker ?? createUpgradeChecker();
@@ -78,6 +79,38 @@ export const adminUpgradeRoutes = (app, options, done) => {
78
79
  return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `升级失败: ${msg}`));
79
80
  }
80
81
  });
82
+ app.post('/admin/api/upgrade/restart', async (req, reply) => {
83
+ const managed = hasProcessManager();
84
+ const method = getRestartMethod();
85
+ // 先回复客户端,再执行重启(否则客户端收不到响应)
86
+ reply.send({ ok: true, method });
87
+ // 给响应发送窗口
88
+ await new Promise((resolve) => setTimeout(resolve, 300)); // eslint-disable-line no-magic-numbers
89
+ try {
90
+ req.log.info({ method, managed }, 'Restarting server...');
91
+ // 优雅关闭(释放端口、等待活跃请求完成)
92
+ await options.closeFn();
93
+ if (!managed) {
94
+ // 无进程管理器(npx / 手动 node):自 spawn 新进程
95
+ const binPath = resolveRestartBinPath();
96
+ const args = process.argv.slice(2); // eslint-disable-line no-magic-numbers
97
+ req.log.info({ binPath, args }, 'Spawning new process before exit');
98
+ const child = spawn(binPath, args, {
99
+ detached: true,
100
+ stdio: 'ignore',
101
+ env: { ...process.env },
102
+ });
103
+ child.unref();
104
+ }
105
+ req.log.info('Exiting current process');
106
+ process.exit(0);
107
+ }
108
+ catch (err) {
109
+ // 重启失败时记录错误,保持服务运行
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ req.log.error({ err }, `Restart failed: ${msg}`);
112
+ }
113
+ });
81
114
  app.post('/admin/api/upgrade/sync-config', async (req, reply) => {
82
115
  const { source } = req.body;
83
116
  if (source !== 'github' && source !== 'gitee') {
@@ -3,10 +3,13 @@ export declare const HTTP_CREATED = 201;
3
3
  export declare const HTTP_FORBIDDEN = 403;
4
4
  export declare const HTTP_NOT_FOUND = 404;
5
5
  export declare const HTTP_CONFLICT = 409;
6
+ export declare const HTTP_UNPROCESSABLE_ENTITY = 422;
6
7
  export declare const HTTP_INTERNAL_ERROR = 500;
7
8
  export declare const HTTP_BAD_GATEWAY = 502;
8
- export declare const HTTP_UNPROCESSABLE_ENTITY = 422;
9
9
  export declare const HTTP_SERVICE_UNAVAILABLE = 503;
10
10
  export declare const PROXY_API_TYPES: Record<string, string>;
11
11
  export declare function getProxyApiType(url: string): string | null;
12
12
  export declare const MS_PER_SECOND = 1000;
13
+ export declare const UPSTREAM_SUCCESS = 200;
14
+ /** 过滤掉不应转发给下游的 hop-by-hop headers */
15
+ export declare function filterHeaders(raw: import("./types.js").RawHeaders): Record<string, string>;
@@ -1,12 +1,13 @@
1
+ // src/core/constants.ts
1
2
  // HTTP 状态码常量 — 全局唯一来源
2
3
  export const HTTP_BAD_REQUEST = 400;
3
4
  export const HTTP_CREATED = 201;
4
5
  export const HTTP_FORBIDDEN = 403;
5
6
  export const HTTP_NOT_FOUND = 404;
6
7
  export const HTTP_CONFLICT = 409;
8
+ export const HTTP_UNPROCESSABLE_ENTITY = 422;
7
9
  export const HTTP_INTERNAL_ERROR = 500;
8
10
  export const HTTP_BAD_GATEWAY = 502;
9
- export const HTTP_UNPROCESSABLE_ENTITY = 422;
10
11
  export const HTTP_SERVICE_UNAVAILABLE = 503;
11
12
  // api_type 路由映射:proxy path → api type,用于全局 hook/errorHandler 中识别代理请求
12
13
  export const PROXY_API_TYPES = {
@@ -19,3 +20,22 @@ export function getProxyApiType(url) {
19
20
  return PROXY_API_TYPES[path] ?? null;
20
21
  }
21
22
  export const MS_PER_SECOND = 1000;
23
+ // 上游成功状态码
24
+ export const UPSTREAM_SUCCESS = 200;
25
+ /** 过滤掉不应转发给下游的 hop-by-hop headers */
26
+ const SKIP_DOWNSTREAM = new Set([
27
+ "content-length",
28
+ "transfer-encoding",
29
+ "connection",
30
+ "keep-alive",
31
+ ]);
32
+ /** 过滤掉不应转发给下游的 hop-by-hop headers */
33
+ export function filterHeaders(raw) {
34
+ const out = {};
35
+ for (const [key, value] of Object.entries(raw)) {
36
+ if (value == null || SKIP_DOWNSTREAM.has(key.toLowerCase()))
37
+ continue;
38
+ out[key] = Array.isArray(value) ? value.join(", ") : value;
39
+ }
40
+ return out;
41
+ }