webaiapi 0.0.0-stage → 0.1.0

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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -2
  3. package/bin/webapi.mjs +45 -0
  4. package/dist/browser/auth-capture.d.ts +110 -0
  5. package/dist/browser/auth-capture.d.ts.map +1 -0
  6. package/dist/browser/auth-capture.js +487 -0
  7. package/dist/browser/auth-capture.js.map +1 -0
  8. package/dist/browser/credentials.d.ts +102 -0
  9. package/dist/browser/credentials.d.ts.map +1 -0
  10. package/dist/browser/credentials.js +209 -0
  11. package/dist/browser/credentials.js.map +1 -0
  12. package/dist/browser/js-eval.d.ts +44 -0
  13. package/dist/browser/js-eval.d.ts.map +1 -0
  14. package/dist/browser/js-eval.js +20 -0
  15. package/dist/browser/js-eval.js.map +1 -0
  16. package/dist/browser/login-flow.d.ts +171 -0
  17. package/dist/browser/login-flow.d.ts.map +1 -0
  18. package/dist/browser/login-flow.js +606 -0
  19. package/dist/browser/login-flow.js.map +1 -0
  20. package/dist/browser/manager.d.ts +144 -0
  21. package/dist/browser/manager.d.ts.map +1 -0
  22. package/dist/browser/manager.js +586 -0
  23. package/dist/browser/manager.js.map +1 -0
  24. package/dist/browser/platforms.d.ts +96 -0
  25. package/dist/browser/platforms.d.ts.map +1 -0
  26. package/dist/browser/platforms.js +288 -0
  27. package/dist/browser/platforms.js.map +1 -0
  28. package/dist/browser/sse-capture.d.ts +23 -0
  29. package/dist/browser/sse-capture.d.ts.map +1 -0
  30. package/dist/browser/sse-capture.js +199 -0
  31. package/dist/browser/sse-capture.js.map +1 -0
  32. package/dist/browser/stealth.d.ts +12 -0
  33. package/dist/browser/stealth.d.ts.map +1 -0
  34. package/dist/browser/stealth.js +12 -0
  35. package/dist/browser/stealth.js.map +1 -0
  36. package/dist/cli.d.ts +12 -0
  37. package/dist/cli.d.ts.map +1 -0
  38. package/dist/cli.js +441 -0
  39. package/dist/cli.js.map +1 -0
  40. package/dist/config.d.ts +90 -0
  41. package/dist/config.d.ts.map +1 -0
  42. package/dist/config.js +246 -0
  43. package/dist/config.js.map +1 -0
  44. package/dist/controllers/history-db.d.ts +56 -0
  45. package/dist/controllers/history-db.d.ts.map +1 -0
  46. package/dist/controllers/history-db.js +260 -0
  47. package/dist/controllers/history-db.js.map +1 -0
  48. package/dist/controllers/request-pool.d.ts +78 -0
  49. package/dist/controllers/request-pool.d.ts.map +1 -0
  50. package/dist/controllers/request-pool.js +222 -0
  51. package/dist/controllers/request-pool.js.map +1 -0
  52. package/dist/controllers/router.d.ts +172 -0
  53. package/dist/controllers/router.d.ts.map +1 -0
  54. package/dist/controllers/router.js +569 -0
  55. package/dist/controllers/router.js.map +1 -0
  56. package/dist/index.d.ts +72 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +61 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/log/api-logger.d.ts +55 -0
  61. package/dist/log/api-logger.d.ts.map +1 -0
  62. package/dist/log/api-logger.js +264 -0
  63. package/dist/log/api-logger.js.map +1 -0
  64. package/dist/log/debug-logger.d.ts +49 -0
  65. package/dist/log/debug-logger.d.ts.map +1 -0
  66. package/dist/log/debug-logger.js +285 -0
  67. package/dist/log/debug-logger.js.map +1 -0
  68. package/dist/log/logger.d.ts +16 -0
  69. package/dist/log/logger.d.ts.map +1 -0
  70. package/dist/log/logger.js +35 -0
  71. package/dist/log/logger.js.map +1 -0
  72. package/dist/postinstall.d.ts +3 -0
  73. package/dist/postinstall.d.ts.map +1 -0
  74. package/dist/postinstall.js +90 -0
  75. package/dist/postinstall.js.map +1 -0
  76. package/dist/providers/base.d.ts +83 -0
  77. package/dist/providers/base.d.ts.map +1 -0
  78. package/dist/providers/base.js +51 -0
  79. package/dist/providers/base.js.map +1 -0
  80. package/dist/providers/chatglm-dom-extract.d.ts +24 -0
  81. package/dist/providers/chatglm-dom-extract.d.ts.map +1 -0
  82. package/dist/providers/chatglm-dom-extract.js +175 -0
  83. package/dist/providers/chatglm-dom-extract.js.map +1 -0
  84. package/dist/providers/chatglm-dom.d.ts +110 -0
  85. package/dist/providers/chatglm-dom.d.ts.map +1 -0
  86. package/dist/providers/chatglm-dom.js +384 -0
  87. package/dist/providers/chatglm-dom.js.map +1 -0
  88. package/dist/providers/chatglm-sse.d.ts +119 -0
  89. package/dist/providers/chatglm-sse.d.ts.map +1 -0
  90. package/dist/providers/chatglm-sse.js +241 -0
  91. package/dist/providers/chatglm-sse.js.map +1 -0
  92. package/dist/providers/chatglm.d.ts +288 -0
  93. package/dist/providers/chatglm.d.ts.map +1 -0
  94. package/dist/providers/chatglm.js +1631 -0
  95. package/dist/providers/chatglm.js.map +1 -0
  96. package/dist/routes/chat.d.ts +117 -0
  97. package/dist/routes/chat.d.ts.map +1 -0
  98. package/dist/routes/chat.js +1346 -0
  99. package/dist/routes/chat.js.map +1 -0
  100. package/dist/routes/compress.d.ts +142 -0
  101. package/dist/routes/compress.d.ts.map +1 -0
  102. package/dist/routes/compress.js +551 -0
  103. package/dist/routes/compress.js.map +1 -0
  104. package/dist/routes/failover.d.ts +34 -0
  105. package/dist/routes/failover.d.ts.map +1 -0
  106. package/dist/routes/failover.js +221 -0
  107. package/dist/routes/failover.js.map +1 -0
  108. package/dist/routes/helpers.d.ts +174 -0
  109. package/dist/routes/helpers.d.ts.map +1 -0
  110. package/dist/routes/helpers.js +525 -0
  111. package/dist/routes/helpers.js.map +1 -0
  112. package/dist/routes/meta.d.ts +31 -0
  113. package/dist/routes/meta.d.ts.map +1 -0
  114. package/dist/routes/meta.js +184 -0
  115. package/dist/routes/meta.js.map +1 -0
  116. package/dist/routes/sse.d.ts +82 -0
  117. package/dist/routes/sse.d.ts.map +1 -0
  118. package/dist/routes/sse.js +77 -0
  119. package/dist/routes/sse.js.map +1 -0
  120. package/dist/server/app.d.ts +24 -0
  121. package/dist/server/app.d.ts.map +1 -0
  122. package/dist/server/app.js +292 -0
  123. package/dist/server/app.js.map +1 -0
  124. package/dist/server/middleware/api-key.d.ts +61 -0
  125. package/dist/server/middleware/api-key.d.ts.map +1 -0
  126. package/dist/server/middleware/api-key.js +95 -0
  127. package/dist/server/middleware/api-key.js.map +1 -0
  128. package/dist/server/middleware/rate-limit.d.ts +67 -0
  129. package/dist/server/middleware/rate-limit.d.ts.map +1 -0
  130. package/dist/server/middleware/rate-limit.js +134 -0
  131. package/dist/server/middleware/rate-limit.js.map +1 -0
  132. package/dist/server/middleware/timeout.d.ts +34 -0
  133. package/dist/server/middleware/timeout.d.ts.map +1 -0
  134. package/dist/server/middleware/timeout.js +64 -0
  135. package/dist/server/middleware/timeout.js.map +1 -0
  136. package/dist/server/types.d.ts +20 -0
  137. package/dist/server/types.d.ts.map +1 -0
  138. package/dist/server/types.js +2 -0
  139. package/dist/server/types.js.map +1 -0
  140. package/dist/tools/build.d.ts +14 -0
  141. package/dist/tools/build.d.ts.map +1 -0
  142. package/dist/tools/build.js +105 -0
  143. package/dist/tools/build.js.map +1 -0
  144. package/dist/tools/clean.d.ts +32 -0
  145. package/dist/tools/clean.d.ts.map +1 -0
  146. package/dist/tools/clean.js +65 -0
  147. package/dist/tools/clean.js.map +1 -0
  148. package/dist/tools/convert.d.ts +50 -0
  149. package/dist/tools/convert.d.ts.map +1 -0
  150. package/dist/tools/convert.js +289 -0
  151. package/dist/tools/convert.js.map +1 -0
  152. package/dist/tools/names.d.ts +138 -0
  153. package/dist/tools/names.d.ts.map +1 -0
  154. package/dist/tools/names.js +578 -0
  155. package/dist/tools/names.js.map +1 -0
  156. package/dist/tools/parse.d.ts +31 -0
  157. package/dist/tools/parse.d.ts.map +1 -0
  158. package/dist/tools/parse.js +273 -0
  159. package/dist/tools/parse.js.map +1 -0
  160. package/dist/tools/placeholder.d.ts +21 -0
  161. package/dist/tools/placeholder.d.ts.map +1 -0
  162. package/dist/tools/placeholder.js +135 -0
  163. package/dist/tools/placeholder.js.map +1 -0
  164. package/dist/tools/prompts.d.ts +42 -0
  165. package/dist/tools/prompts.d.ts.map +1 -0
  166. package/dist/tools/prompts.js +69 -0
  167. package/dist/tools/prompts.js.map +1 -0
  168. package/dist/tools/protocol.d.ts +58 -0
  169. package/dist/tools/protocol.d.ts.map +1 -0
  170. package/dist/tools/protocol.js +287 -0
  171. package/dist/tools/protocol.js.map +1 -0
  172. package/dist/tools/repair.d.ts +83 -0
  173. package/dist/tools/repair.d.ts.map +1 -0
  174. package/dist/tools/repair.js +510 -0
  175. package/dist/tools/repair.js.map +1 -0
  176. package/dist/tools/workspace.d.ts +40 -0
  177. package/dist/tools/workspace.d.ts.map +1 -0
  178. package/dist/tools/workspace.js +224 -0
  179. package/dist/tools/workspace.js.map +1 -0
  180. package/dist/types/message.d.ts +87 -0
  181. package/dist/types/message.d.ts.map +1 -0
  182. package/dist/types/message.js +85 -0
  183. package/dist/types/message.js.map +1 -0
  184. package/dist/types/openai.d.ts +100 -0
  185. package/dist/types/openai.d.ts.map +1 -0
  186. package/dist/types/openai.js +66 -0
  187. package/dist/types/openai.js.map +1 -0
  188. package/dist/utils/errors.d.ts +82 -0
  189. package/dist/utils/errors.d.ts.map +1 -0
  190. package/dist/utils/errors.js +115 -0
  191. package/dist/utils/errors.js.map +1 -0
  192. package/dist/utils/mutex.d.ts +31 -0
  193. package/dist/utils/mutex.d.ts.map +1 -0
  194. package/dist/utils/mutex.js +61 -0
  195. package/dist/utils/mutex.js.map +1 -0
  196. package/dist/utils/prompt.d.ts +150 -0
  197. package/dist/utils/prompt.d.ts.map +1 -0
  198. package/dist/utils/prompt.js +514 -0
  199. package/dist/utils/prompt.js.map +1 -0
  200. package/dist/utils/text.d.ts +30 -0
  201. package/dist/utils/text.d.ts.map +1 -0
  202. package/dist/utils/text.js +70 -0
  203. package/dist/utils/text.js.map +1 -0
  204. package/package.json +67 -4
  205. package/postinstall.mjs +34 -0
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Fastify 应用组装 —— `legacy-python/src/main.py` 的移植。
3
+ *
4
+ * ## ⚠️ 一处**有意修复**的偏差(必须知道)
5
+ * Python 的 `verify_api_key` 是个 FastAPI **依赖**,但**从来没有被任何路由
6
+ * `Depends()` 引用**(`grep -rn "Depends(" src` 只有它自己的签名)。
7
+ * 也就是说:Python 版里 API Key 认证**从来没生效过** —— `main.py` 往管理器里塞了密钥、
8
+ * 设了 `required`,但没有任何地方去查。
9
+ *
10
+ * TS 版把它**接上了**(`onRequest` 钩子),但保持完全相同的放行语义:
11
+ * - `API_KEYS` 为空 → 强制为「不校验」(与 Python 的日志「API Key 认证已禁用」一致);
12
+ * - `API_KEY_REQUIRED=false`(默认)→ 不带密钥也放行,**现有用户零感知**;
13
+ * - 只有显式 `API_KEY_REQUIRED=true` 时才真正拦 401。
14
+ *
15
+ * ## 其他差异
16
+ * - CORS 用**手写钩子**而不是 `@fastify/cors`:少一个运行时依赖,
17
+ * 且能正确实现「`allow_credentials` + 通配来源」这个 FastAPI 写法其实不合法、
18
+ * 浏览器会拒绝的组合(这里回显请求方 Origin)。
19
+ */
20
+ import { execFileSync } from 'node:child_process';
21
+ import Fastify, {} from 'fastify';
22
+ import { getApiKeys, settings } from '../config.js';
23
+ import { getHistoryDb } from '../controllers/history-db.js';
24
+ import { getModelRouter } from '../controllers/router.js';
25
+ import { resetApiLog } from '../log/api-logger.js';
26
+ import { logger } from '../log/logger.js';
27
+ import { disposeAllChannels, disposeSharedBrowser } from '../browser/manager.js';
28
+ import { registerChatRoutes } from '../routes/chat.js';
29
+ import { registerMetaRoutes, runStartupTasks, startPeriodicHistoryCleanup } from '../routes/meta.js';
30
+ import { waitForBackgroundTasks } from '../routes/compress.js';
31
+ import { errorMessage } from '../utils/errors.js';
32
+ import { getApiKeyManager, makeApiKeyHook, verifyApiKey } from './middleware/api-key.js';
33
+ import { makeRateLimitHook } from './middleware/rate-limit.js';
34
+ import { makeTimeoutHook } from './middleware/timeout.js';
35
+ /** 关服时挂起的清理句柄。 */
36
+ let periodicCleanup = null;
37
+ /**
38
+ * 初始化 API Key 管理器。
39
+ *
40
+ * 与 Python `main()` 的语义一致:**没有配置任何密钥时视为禁用认证**
41
+ * (即使 `API_KEY_REQUIRED=true` 也不拦 —— 否则等于把自己锁在门外)。
42
+ */
43
+ export function initApiKeys() {
44
+ const manager = getApiKeyManager();
45
+ const keys = getApiKeys();
46
+ if (keys.size === 0) {
47
+ manager.required = false;
48
+ logger.info('API Key 认证已禁用(未配置密钥)');
49
+ return;
50
+ }
51
+ for (const key of keys)
52
+ manager.addKey(key);
53
+ manager.required = settings.apiKeyRequired;
54
+ logger.info(`API Key 认证已配置: ${keys.size} 个密钥` +
55
+ `(${settings.apiKeyRequired ? '强制校验' : '仅记录,不拦截'})`);
56
+ }
57
+ /**
58
+ * 创建 Fastify 应用(不监听端口)。
59
+ *
60
+ * 中间件顺序与原实现一致:CORS → 超时 → 限流 → 认证 → 路由。
61
+ */
62
+ export async function createApp() {
63
+ const app = Fastify({
64
+ // ⚠️ 不用 Fastify 内建 logger:项目统一走 pino 单例(`src/log/logger.ts`),
65
+ // 避免同一条日志被两套格式化器各打一遍。
66
+ logger: false,
67
+ trustProxy: true, // 让 `request.ip` 取到反向代理后的真实 IP(限流按 IP 时很关键)
68
+ bodyLimit: 64 * 1024 * 1024, // 上下文可能很大(几十万字符的历史)
69
+ });
70
+ initApiKeys();
71
+ const prefix = settings.apiPrefix || '';
72
+ /* ===== CORS(手写,见文件头说明)===== */
73
+ app.addHook('onRequest', async (request, reply) => {
74
+ const origin = request.headers['origin'];
75
+ if (typeof origin === 'string' && origin) {
76
+ reply.header('access-control-allow-origin', origin);
77
+ reply.header('access-control-allow-credentials', 'true');
78
+ reply.header('vary', 'Origin');
79
+ }
80
+ else {
81
+ reply.header('access-control-allow-origin', '*');
82
+ }
83
+ reply.header('access-control-allow-methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
84
+ reply.header('access-control-allow-headers', request.headers['access-control-request-headers'] || '*');
85
+ // 预检直接结束,不进业务逻辑
86
+ if (request.method === 'OPTIONS') {
87
+ void reply.code(204).send();
88
+ }
89
+ });
90
+ /* ===== 超时(必须在业务路由之前,才能把 deadline 传给下游)===== */
91
+ const skipPaths = new Set([
92
+ `${prefix}/health`,
93
+ `${prefix}/status`,
94
+ '/health',
95
+ '/status',
96
+ '/',
97
+ ]);
98
+ app.addHook('onRequest', makeTimeoutHook({ timeoutSeconds: settings.requestTimeout, skipPaths }));
99
+ /* ===== 限流 ===== */
100
+ app.addHook('onRequest', makeRateLimitHook({
101
+ requestsPerMinute: settings.rateLimitPerMinute,
102
+ enabled: settings.rateLimitEnabled,
103
+ skipPaths,
104
+ }));
105
+ /* ===== API Key 认证 =====
106
+ * 只在「配了密钥**且**要求强制」时才真的校验(见文件头说明)。 */
107
+ const apiKeyManager = getApiKeyManager();
108
+ if (apiKeyManager.required) {
109
+ app.addHook('onRequest', makeApiKeyHook(skipPaths));
110
+ }
111
+ else {
112
+ // 未强制时也解析一次密钥:便于限流按 key 分组(但不拦截)
113
+ app.addHook('onRequest', async (request) => {
114
+ verifyApiKey(request);
115
+ });
116
+ }
117
+ /* ===== 路由 ===== */
118
+ await app.register(async (instance) => {
119
+ registerMetaRoutes(instance);
120
+ registerChatRoutes(instance);
121
+ }, prefix ? { prefix } : {});
122
+ // 根路径(便于确认服务活着)
123
+ app.get('/', async () => ({
124
+ name: settings.appName,
125
+ version: settings.appVersion,
126
+ docs: `${prefix}/models`,
127
+ }));
128
+ /* ===== 404 ===== */
129
+ app.setNotFoundHandler((request, reply) => {
130
+ void reply.code(404).send({
131
+ error: {
132
+ message: `Not Found: ${request.method} ${request.url}`,
133
+ type: 'invalid_request_error',
134
+ code: 'not_found',
135
+ },
136
+ });
137
+ });
138
+ /* ===== 全局异常处理 ===== */
139
+ app.setErrorHandler((error, _request, reply) => {
140
+ // Fastify 的 body 解析错误(JSON 语法错等)自带 4xx 状态码
141
+ const err = error;
142
+ const statusCode = typeof err.statusCode === 'number' ? err.statusCode : 500;
143
+ const message = err.message ?? '内部服务器错误';
144
+ if (statusCode >= 500) {
145
+ logger.error(`未处理的异常: ${message}`);
146
+ }
147
+ const payload = statusCode >= 500
148
+ ? { message: '内部服务器错误', type: 'server_error', code: 'internal_error' }
149
+ : { message, type: 'invalid_request_error', code: err.code ?? null };
150
+ void reply.code(statusCode >= 500 ? 500 : statusCode).send({ error: payload });
151
+ });
152
+ return app;
153
+ }
154
+ /**
155
+ * 启动服务(含启动任务与优雅关闭)。
156
+ *
157
+ * 返回的 Promise 在服务关闭后 resolve(与 `uvicorn.run` 的阻塞语义一致)。
158
+ */
159
+ /* ==================================================================== */
160
+ /* 端口占用诊断 */
161
+ /* ==================================================================== */
162
+ /** 判断错误是不是「端口被占用」。 */
163
+ function isAddrInUse(error) {
164
+ const code = error?.['code'];
165
+ if (typeof code === 'string' && code === 'EADDRINUSE')
166
+ return true;
167
+ return String(error).includes('EADDRINUSE');
168
+ }
169
+ /**
170
+ * 尽力找出占用指定端口的进程 PID(找不到返回 `null`)。
171
+ *
172
+ * ⚠️ 这里的输出**只用于给用户一句提示**,所以任何失败都必须静默放弃 ——
173
+ * 绝不能因为「`ss`/`lsof` 不存在、输出格式变了、没权限看别人的进程」
174
+ * 而把启动流程搞挂。同用户的进程才能看到 pid,而我们提示的恰恰是
175
+ * 「另一个同用户实例」这种最常见的情况,所以够用。
176
+ */
177
+ function pidListeningOn(port) {
178
+ const tryRead = (cmd, args, pattern) => {
179
+ try {
180
+ const out = execFileSync(cmd, args, { encoding: 'utf8', timeout: 3000 });
181
+ const match = pattern.exec(out);
182
+ return match ? Number(match[1]) : null;
183
+ }
184
+ catch {
185
+ return null;
186
+ }
187
+ };
188
+ // `ss -ltnp`:LISTEN 0 511 0.0.0.0:8631 ... users:(("MainThread",pid=78002,fd=24))
189
+ const bySs = tryRead('ss', ['-ltnp'], new RegExp(`:${port}\\b.*pid=(\\d+)`));
190
+ if (bySs !== null)
191
+ return bySs;
192
+ // `lsof -ti :PORT`:每行一个 pid
193
+ return tryRead('lsof', ['-ti', `:${port}`], /^(\d+)$/m);
194
+ }
195
+ /**
196
+ * 启动服务并阻塞直到进程收到退出信号。
197
+ *
198
+ * @returns 进程退出码:`0` 正常 / `1` 启动失败(如端口被占用)。
199
+ * ⚠️ 返回退出码而不是在函数里设 `process.exitCode` —— CLI 侧的
200
+ * `process.exitCode = await runCli()` 会覆盖它(实测踩过:报错信息是对的,
201
+ * 退出码却是 0,脚本会误判成功)。
202
+ */
203
+ export async function startServer() {
204
+ const app = await createApp();
205
+ // ===== 启动任务 =====
206
+ resetApiLog();
207
+ logger.info('API 请求日志已重置');
208
+ await runStartupTasks();
209
+ periodicCleanup = startPeriodicHistoryCleanup();
210
+ // 预热模型路由器(会打印账号分配与辅助通道状态,是排查「多账号有没有生效」的第一现场)
211
+ getModelRouter();
212
+ /* ===== 浏览器二进制预检(**非致命**,只警告)=====
213
+ * Playwright 不随包分发浏览器(Firefox 约 80MB),这是 npm 安装后最常见的
214
+ * 「装好了但跑不起来」。浏览器是**首次请求时才懒加载**的,若不在这里提前告警,
215
+ * 用户会在第一次对话失败时才发现 —— 那时错误信息埋在日志里,很难定位。
216
+ * 所以启动时查一次并给出可复制的修复命令,但**不阻断启动**
217
+ * (健康检查、/v1/models 等不需要浏览器)。 */
218
+ try {
219
+ const { firefox } = await import('playwright');
220
+ const { existsSync } = await import('node:fs');
221
+ const exe = firefox.executablePath();
222
+ if (!existsSync(exe)) {
223
+ logger.warn('⚠️ 未找到 Playwright 的 Firefox 二进制,真实对话会失败。' +
224
+ '请运行 `npx playwright install firefox` 下载(约 80MB,只需一次)。');
225
+ }
226
+ }
227
+ catch (error) {
228
+ logger.warn(`浏览器预检失败: ${errorMessage(error)}`);
229
+ }
230
+ const url = `http://${settings.apiHost}:${settings.apiPort}${settings.apiPrefix}`;
231
+ try {
232
+ await app.listen({ host: settings.apiHost, port: settings.apiPort });
233
+ }
234
+ catch (error) {
235
+ // ⚠️ 必须拦下来翻译成人话:`listen` 失败时 Node 抛的是一整段堆栈
236
+ // (`UVExceptionWithHostPort ... code: 'EADDRINUSE'`),用户看不出
237
+ // 「其实是有另一个实例在跑」—— 实测就有这样的困惑。
238
+ // 而且此时启动已经做了不少事(注册 Provider、启用辅助通道、
239
+ // 打开浏览器),堆栈会把真正的原因淹没在里面。
240
+ if (isAddrInUse(error)) {
241
+ const pid = pidListeningOn(settings.apiPort);
242
+ logger.error(`端口 ${settings.apiPort} 已被占用(${pid ? `进程 ${pid}` : '可能有另一个实例在跑'})。` +
243
+ '\n 处理方式任选:' +
244
+ '\n 1. 直接用已在跑的那个实例(`webapi status` 可查看);' +
245
+ `\n 2. 停掉它再启动:kill ${pid ?? '<pid>'};` +
246
+ `\n 3. 换个端口:API_PORT=${settings.apiPort + 1} webapi start`);
247
+ // ⚠️ 返回退出码而不是设 `process.exitCode`:CLI 侧会执行
248
+ // `process.exitCode = await runCli()`,把这里设的值**覆盖回 0**
249
+ // (实测确实发生了:报错信息对了,退出码却是 0,脚本会误判成功)。
250
+ return 1;
251
+ }
252
+ throw error;
253
+ }
254
+ logger.info(`服务已启动: ${url}`);
255
+ // ===== 优雅关闭 =====
256
+ const shutdown = async (signal) => {
257
+ logger.info(`收到 ${signal},正在关闭...`);
258
+ try {
259
+ await app.close();
260
+ }
261
+ catch (error) {
262
+ logger.warn(`关闭 HTTP 服务失败: ${errorMessage(error)}`);
263
+ }
264
+ periodicCleanup?.stop();
265
+ await waitForBackgroundTasks();
266
+ try {
267
+ await disposeAllChannels();
268
+ await disposeSharedBrowser();
269
+ logger.info('浏览器资源已清理');
270
+ }
271
+ catch (error) {
272
+ logger.warn(`清理浏览器失败: ${errorMessage(error)}`);
273
+ }
274
+ try {
275
+ getHistoryDb().close();
276
+ }
277
+ catch {
278
+ // 忽略
279
+ }
280
+ logger.info('应用已关闭');
281
+ };
282
+ for (const signal of ['SIGINT', 'SIGTERM']) {
283
+ process.once(signal, () => {
284
+ void shutdown(signal).then(() => process.exit(0));
285
+ });
286
+ }
287
+ await new Promise((resolve) => {
288
+ app.server.once('close', () => resolve());
289
+ });
290
+ return 0;
291
+ }
292
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * API Key 认证 —— `legacy-python/src/middleware/auth.py` 的移植(OpenAI 兼容)。
3
+ *
4
+ * 认证方式(优先级与原实现一致):
5
+ * 1. `Authorization: Bearer {key}`
6
+ * 2. `x-api-key: {key}`
7
+ *
8
+ * 配置:`API_KEYS=key1,key2` / `API_KEY_REQUIRED=true`(默认 false,兼容无认证模式)。
9
+ *
10
+ * 与原实现的差异:
11
+ * - FastAPI 的 `Depends(security)` → 显式调用 {@link verifyApiKey}(由路由自行调用或
12
+ * 交给 `onRequest` 钩子)。
13
+ * - Python 的 `import hmac` 从未被使用(`key in self.keys` 是普通集合查找)。
14
+ * TS 侧同样用 Set,但**保留一个说明**:这不是恒定时间比较,若将来把 API Key
15
+ * 当成真正的密钥用,应改为 `timingSafeEqual`。
16
+ */
17
+ import type { FastifyReply, FastifyRequest } from 'fastify';
18
+ /** 从请求头里取 API Key(`Authorization: Bearer` 优先,其次 `x-api-key`)。 */
19
+ export declare function extractApiKey(request: FastifyRequest): string | null;
20
+ /** API Key 管理器。 */
21
+ export declare class ApiKeyManager {
22
+ private keys;
23
+ /** 是否强制认证(未配置密钥时自动为 false)。 */
24
+ required: boolean;
25
+ /**
26
+ * 校验密钥。
27
+ *
28
+ * ## 语义(三条,顺序有意义)
29
+ * 1. **一个密钥都没配(`keys.size === 0`)且不强制 → 认证功能整体关闭,
30
+ * 忽略请求里带的任何 Key**。
31
+ * ⚠️ 旧实现在这里会 401(理由是「传了错密钥说明客户端有意图,静默放行
32
+ * 会掩盖配置错误」)。但 OpenAI 兼容客户端**几乎总会**带一个
33
+ * `Authorization` 头(不少还强制要求填非空值),于是出现两边各说各话:
34
+ * 服务端日志说「认证已禁用」,客户端却收到 401「API Key 无效或已过期」
35
+ * —— 实测 xzh 就是这么死的。**「禁用」必须真的禁用。**
36
+ * 2. 配了密钥但没传 → 按 `required` 决定(不强制则放行)。
37
+ * 3. 配了密钥且传了 → 必须在集合里(真认证,传错一定 401)。
38
+ */
39
+ isValid(key: string | null | undefined): boolean;
40
+ /** 添加密钥。 */
41
+ addKey(key: string): void;
42
+ /** 移除密钥(返回是否真的移除了)。 */
43
+ removeKey(key: string): boolean;
44
+ /** 已配置的密钥数量。 */
45
+ get count(): number;
46
+ }
47
+ /** 获取 API Key 管理器(单例)。 */
48
+ export declare function getApiKeyManager(): ApiKeyManager;
49
+ /**
50
+ * 校验请求的 API Key,返回使用的密钥(未传且不强制时返回 null)。
51
+ *
52
+ * 失败时抛 {@link AuthenticationError}(401,`invalid_api_key`)。
53
+ */
54
+ export declare function verifyApiKey(request: FastifyRequest): string | null;
55
+ /**
56
+ * Fastify 钩子:认证所有请求(除豁免路径)。
57
+ *
58
+ * 豁免名单与原实现一致:健康检查 / 状态 / 根路径。
59
+ */
60
+ export declare function makeApiKeyHook(skipPaths: ReadonlySet<string>): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
61
+ //# sourceMappingURL=api-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/api-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAK5D,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CASpE;AAED,mBAAmB;AACnB,qBAAa,aAAa;IACxB,OAAO,CAAC,IAAI,CAAqB;IAEjC,+BAA+B;IAC/B,QAAQ,UAAS;IAEjB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI/C;IAED,YAAY;IACZ,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CASxB;IAED,uBAAuB;IACvB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9B;IAED,gBAAgB;IAChB,IAAI,KAAK,IAAI,MAAM,CAElB;CACF;AAID,0BAA0B;AAC1B,wBAAgB,gBAAgB,IAAI,aAAa,CAGhD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAenE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,GAC7B,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAKjE"}
@@ -0,0 +1,95 @@
1
+ import { logger } from '../../log/logger.js';
2
+ import { AuthenticationError } from '../../utils/errors.js';
3
+ /** 从请求头里取 API Key(`Authorization: Bearer` 优先,其次 `x-api-key`)。 */
4
+ export function extractApiKey(request) {
5
+ const auth = request.headers['authorization'];
6
+ if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
7
+ const key = auth.slice(7).trim();
8
+ if (key)
9
+ return key;
10
+ }
11
+ const xApiKey = request.headers['x-api-key'];
12
+ if (typeof xApiKey === 'string' && xApiKey)
13
+ return xApiKey;
14
+ return null;
15
+ }
16
+ /** API Key 管理器。 */
17
+ export class ApiKeyManager {
18
+ keys = new Set();
19
+ /** 是否强制认证(未配置密钥时自动为 false)。 */
20
+ required = false;
21
+ /**
22
+ * 校验密钥。
23
+ *
24
+ * ## 语义(三条,顺序有意义)
25
+ * 1. **一个密钥都没配(`keys.size === 0`)且不强制 → 认证功能整体关闭,
26
+ * 忽略请求里带的任何 Key**。
27
+ * ⚠️ 旧实现在这里会 401(理由是「传了错密钥说明客户端有意图,静默放行
28
+ * 会掩盖配置错误」)。但 OpenAI 兼容客户端**几乎总会**带一个
29
+ * `Authorization` 头(不少还强制要求填非空值),于是出现两边各说各话:
30
+ * 服务端日志说「认证已禁用」,客户端却收到 401「API Key 无效或已过期」
31
+ * —— 实测 xzh 就是这么死的。**「禁用」必须真的禁用。**
32
+ * 2. 配了密钥但没传 → 按 `required` 决定(不强制则放行)。
33
+ * 3. 配了密钥且传了 → 必须在集合里(真认证,传错一定 401)。
34
+ */
35
+ isValid(key) {
36
+ if (this.keys.size === 0 && !this.required)
37
+ return true;
38
+ if (!key)
39
+ return !this.required;
40
+ return this.keys.has(key);
41
+ }
42
+ /** 添加密钥。 */
43
+ addKey(key) {
44
+ const trimmed = key.trim();
45
+ if (!trimmed)
46
+ return;
47
+ this.keys.add(trimmed);
48
+ logger.info(trimmed.length > 12
49
+ ? `API Key 已添加: ${trimmed.slice(0, 8)}...${trimmed.slice(-4)}`
50
+ : `API Key 已添加: ${trimmed}`);
51
+ }
52
+ /** 移除密钥(返回是否真的移除了)。 */
53
+ removeKey(key) {
54
+ return this.keys.delete(key);
55
+ }
56
+ /** 已配置的密钥数量。 */
57
+ get count() {
58
+ return this.keys.size;
59
+ }
60
+ }
61
+ let manager = null;
62
+ /** 获取 API Key 管理器(单例)。 */
63
+ export function getApiKeyManager() {
64
+ manager ??= new ApiKeyManager();
65
+ return manager;
66
+ }
67
+ /**
68
+ * 校验请求的 API Key,返回使用的密钥(未传且不强制时返回 null)。
69
+ *
70
+ * 失败时抛 {@link AuthenticationError}(401,`invalid_api_key`)。
71
+ */
72
+ export function verifyApiKey(request) {
73
+ const mgr = getApiKeyManager();
74
+ const apiKey = extractApiKey(request);
75
+ if (!apiKey && mgr.required) {
76
+ throw new AuthenticationError('缺少 API Key。请设置 Authorization: Bearer {key} 或 x-api-key: {key}');
77
+ }
78
+ if (apiKey && !mgr.isValid(apiKey)) {
79
+ throw new AuthenticationError('API Key 无效');
80
+ }
81
+ return apiKey;
82
+ }
83
+ /**
84
+ * Fastify 钩子:认证所有请求(除豁免路径)。
85
+ *
86
+ * 豁免名单与原实现一致:健康检查 / 状态 / 根路径。
87
+ */
88
+ export function makeApiKeyHook(skipPaths) {
89
+ return async (request, _reply) => {
90
+ if (skipPaths.has(request.url.split('?')[0] ?? ''))
91
+ return;
92
+ verifyApiKey(request);
93
+ };
94
+ }
95
+ //# sourceMappingURL=api-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key.js","sourceRoot":"","sources":["../../../src/server/middleware/api-key.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 速率限制中间件 —— `legacy-python/src/middleware/rate_limit.py` 的移植。
3
+ *
4
+ * 令牌桶算法,按客户端标识(API Key 优先,否则 IP)分别限流。
5
+ *
6
+ * 配置:`RATE_LIMIT_PER_MINUTE=60` / `RATE_LIMIT_ENABLED=true`
7
+ *
8
+ * 与原实现的差异:
9
+ * - `BaseHTTPMiddleware.dispatch` → Fastify `onRequest` 钩子的工厂函数。
10
+ * - Python 用 `time.monotonic()`(不受系统时间调整影响);TS 用
11
+ * `performance.now()` 达到同样效果(`Date.now()` 会被 NTP 调时影响)。
12
+ * - `asyncio.Lock` → {@link Mutex}:JS 是单线程,`consume()` 本身不需要锁,
13
+ * 但清理与取桶的「读改写」序列仍需要原子化,保留锁以免将来加入 await 时出错。
14
+ */
15
+ import type { FastifyReply, FastifyRequest } from 'fastify';
16
+ /** 令牌桶。 */
17
+ export declare class TokenBucket {
18
+ /** 令牌生成速率(每秒)。 */
19
+ private readonly rate;
20
+ /** 桶容量。 */
21
+ private readonly capacity;
22
+ private tokens;
23
+ private lastTime;
24
+ constructor(
25
+ /** 令牌生成速率(每秒)。 */
26
+ rate: number,
27
+ /** 桶容量。 */
28
+ capacity: number, now?: () => number);
29
+ /** 尝试消费 1 个令牌。 */
30
+ consume(now?: () => number): boolean;
31
+ /** 最后使用时间(清理过期桶用)。 */
32
+ get lastUsed(): number;
33
+ }
34
+ /** 速率限制选项。 */
35
+ export interface RateLimitOptions {
36
+ requestsPerMinute: number;
37
+ enabled: boolean;
38
+ /**
39
+ * 不参与限流的路径。默认与 Python 版一致:
40
+ * `/health`、`/status`、`/v1/health`、`/v1/status`、`/`。
41
+ */
42
+ skipPaths?: ReadonlySet<string>;
43
+ }
44
+ /** 限流器(可在测试中直接实例化,不依赖 Fastify)。 */
45
+ export declare class RateLimiter {
46
+ private readonly requestsPerMinute;
47
+ private readonly buckets;
48
+ private readonly lock;
49
+ private lastCleanup;
50
+ /** 清理间隔(秒)。 */
51
+ private readonly cleanupInterval;
52
+ /** 判定是否放行(纯逻辑,便于单测)。 */
53
+ allow(clientKey: string): Promise<boolean>;
54
+ /** 清理超过 5 分钟未使用的桶。 */
55
+ private cleanup;
56
+ constructor(requestsPerMinute: number);
57
+ }
58
+ /**
59
+ * 取客户端标识:API Key 优先,否则回退到 IP。
60
+ *
61
+ * 与服务端反向代理的配合:Fastify 默认不信任 `X-Forwarded-For`,
62
+ * 需要时在 `app.ts` 里设置 `trustProxy`,届时 `request.ip` 会自动取真实 IP。
63
+ */
64
+ export declare function clientKeyOf(request: FastifyRequest): string;
65
+ /** 构造 Fastify `onRequest` 钩子。 */
66
+ export declare function makeRateLimitHook(options: RateLimitOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
67
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAU5D,WAAW;AACX,qBAAa,WAAW;IAMpB,kBAAkB;IAClB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,WAAW;IACX,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR3B,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,QAAQ,CAAS;IAEzB;IACE,kBAAkB;IACD,IAAI,EAAE,MAAM;IAC7B,WAAW;IACM,QAAQ,EAAE,MAAM,EACjC,GAAG,GAAE,MAAM,MAAoB,EAIhC;IAED,kBAAkB;IAClB,OAAO,CAAC,GAAG,GAAE,MAAM,MAAoB,GAAG,OAAO,CAUhD;IAED,sBAAsB;IACtB,IAAI,QAAQ,IAAI,MAAM,CAErB;CACF;AAED,cAAc;AACd,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACjC;AAID,mCAAmC;AACnC,qBAAa,WAAW;IA2CV,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IA1C9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IAEpC,OAAO,CAAC,WAAW,CAAiB;IAEpC,eAAe;IACf,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAO;IAEvC,wBAAwB;IAClB,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa/C;IAED,sBAAsB;IACtB,OAAO,CAAC,OAAO;IAgBf,YAA6B,iBAAiB,EAAE,MAAM,EAAI;CAC3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAU3D;AAED,iCAAiC;AACjC,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,gBAAgB,GACxB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CA4BjE"}