fogact 1.1.3

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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -0
  3. package/README.zh-CN.md +244 -0
  4. package/bin/cli.js +9 -0
  5. package/bin/web-server.js +1434 -0
  6. package/config/upstream.example.json +14 -0
  7. package/frontend/activate.html +249 -0
  8. package/frontend/admin/admin-panel-v2.js +1899 -0
  9. package/frontend/admin/index.html +705 -0
  10. package/frontend/assets/market-ui.css +1876 -0
  11. package/frontend/color-test.html +136 -0
  12. package/frontend/index.html +191 -0
  13. package/frontend/user/assets/AnnouncementDetail-Dvxmwz0A.js +12 -0
  14. package/frontend/user/assets/Announcements-CS1tF2mx.js +11 -0
  15. package/frontend/user/assets/CardBind-CsCxihhP.js +21 -0
  16. package/frontend/user/assets/CardContent.vue_vue_type_script_setup_true_lang-D2L-uqSl.js +1 -0
  17. package/frontend/user/assets/CardDescription.vue_vue_type_script_setup_true_lang-D-v5Pl7F.js +1 -0
  18. package/frontend/user/assets/CardTitle.vue_vue_type_script_setup_true_lang-a0CCN6D5.js +1 -0
  19. package/frontend/user/assets/Dashboard-rPsmltm5.js +51 -0
  20. package/frontend/user/assets/DashboardLayout-BUCWGlXC.css +1 -0
  21. package/frontend/user/assets/DashboardLayout-DDkxHYFj.js +80 -0
  22. package/frontend/user/assets/Input.vue_vue_type_script_setup_true_lang-B0SyPmYb.js +6 -0
  23. package/frontend/user/assets/Label.vue_vue_type_script_setup_true_lang-CxYORSgN.js +1 -0
  24. package/frontend/user/assets/Progress.vue_vue_type_script_setup_true_lang-2_QbPsEQ.js +1 -0
  25. package/frontend/user/assets/QuotaPack-B_tJ7Psm.js +6 -0
  26. package/frontend/user/assets/Renewal-BSDhDmwv.js +6 -0
  27. package/frontend/user/assets/ScrollArea.vue_vue_type_script_setup_true_lang-DMYwcfpz.js +1 -0
  28. package/frontend/user/assets/Separator.vue_vue_type_script_setup_true_lang-Ckg8EXj_.js +1 -0
  29. package/frontend/user/assets/Settings-CBdAa3lw.js +11 -0
  30. package/frontend/user/assets/TooltipTrigger.vue_vue_type_script_setup_true_lang-DtSBjzGo.js +16 -0
  31. package/frontend/user/assets/Welcome-7IfzEli4.css +1 -0
  32. package/frontend/user/assets/Welcome-Dtfp6oER.js +1 -0
  33. package/frontend/user/assets/_plugin-vue_export-helper-5cjT4u0R.js +16 -0
  34. package/frontend/user/assets/activity-wYWtyqTJ.js +6 -0
  35. package/frontend/user/assets/announcement-35mOnjRL.js +16 -0
  36. package/frontend/user/assets/calendar-BFNuCata.js +6 -0
  37. package/frontend/user/assets/chart-vendor-CULJE59K.js +37 -0
  38. package/frontend/user/assets/chevron-down-kDbuU1Py.js +6 -0
  39. package/frontend/user/assets/chevron-right-BayASIm0.js +6 -0
  40. package/frontend/user/assets/eye-CY62vip0.js +6 -0
  41. package/frontend/user/assets/gauge-C5NQ-mV8.js +6 -0
  42. package/frontend/user/assets/index-B8QSyYhS.css +1 -0
  43. package/frontend/user/assets/index-Da98HOxL.js +91 -0
  44. package/frontend/user/assets/link-2-DT5R5nGO.js +6 -0
  45. package/frontend/user/assets/package-rUbExUEn.js +6 -0
  46. package/frontend/user/assets/plus-CQc6C8wG.js +11 -0
  47. package/frontend/user/assets/refresh-cw-Y9hCloPL.js +6 -0
  48. package/frontend/user/assets/useUserPageRefresh-BYZvpNR9.js +1 -0
  49. package/frontend/user/assets/zap-l5zbZqrM.js +11 -0
  50. package/frontend/user/index.html +67 -0
  51. package/install.sh +402 -0
  52. package/lib/commands/activate.js +144 -0
  53. package/lib/commands/restore.js +102 -0
  54. package/lib/commands/test.js +40 -0
  55. package/lib/config/claude.js +81 -0
  56. package/lib/config/codex.js +164 -0
  57. package/lib/config/upstream.js +79 -0
  58. package/lib/index.js +164 -0
  59. package/lib/platforms/claude-code.js +35 -0
  60. package/lib/platforms/codex-cli.js +35 -0
  61. package/lib/platforms/editor-codex.js +138 -0
  62. package/lib/platforms/index.js +32 -0
  63. package/lib/platforms/openclaw.js +118 -0
  64. package/lib/platforms/opencode.js +89 -0
  65. package/lib/services/activation-orchestrator.js +666 -0
  66. package/lib/services/backup-service.js +162 -0
  67. package/lib/services/cliproxy-api.js +174 -0
  68. package/lib/services/database.js +461 -0
  69. package/lib/services/newapi.js +97 -0
  70. package/lib/services/node-service.js +49 -0
  71. package/lib/utils/json-file.js +33 -0
  72. package/package.json +53 -0
@@ -0,0 +1,1434 @@
1
+ #!/usr/bin/env node
2
+
3
+ const http = require("http");
4
+ const https = require("https");
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+ const { userDb, codeDb, initializeSampleData } = require("../lib/services/database");
9
+ const { DEFAULT_CONFIG_PATH, loadUpstreamConfig } = require("../lib/config/upstream");
10
+ const { readJsonFile, writeJsonFile } = require("../lib/utils/json-file");
11
+ const { maskKey, verifyNewApiKey } = require("../lib/services/newapi");
12
+
13
+ const PORT = process.env.PORT || 34020;
14
+ const FRONTEND_DIR = path.join(__dirname, "..", "frontend");
15
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "admin123";
16
+ const SERVER_TIMEZONE = process.env.SERVER_TIMEZONE || "Asia/Shanghai";
17
+
18
+ const USER_STATUS_MAP = {
19
+ active: "活跃",
20
+ "活跃": "活跃",
21
+ inactive: "待激活",
22
+ pending: "待激活",
23
+ "待激活": "待激活",
24
+ disabled: "已禁用",
25
+ blocked: "已禁用",
26
+ banned: "已禁用",
27
+ "已禁用": "已禁用",
28
+ };
29
+
30
+ const USER_STATUS_KEY_MAP = {
31
+ "活跃": "active",
32
+ "待激活": "inactive",
33
+ "已禁用": "disabled",
34
+ };
35
+
36
+ const CODE_STATUS_MAP = {
37
+ unused: "unused",
38
+ "未使用": "unused",
39
+ used: "used",
40
+ "已使用": "used",
41
+ expired: "expired",
42
+ "已过期": "expired",
43
+ };
44
+
45
+ const CODE_STATUS_LABEL_MAP = {
46
+ unused: "未使用",
47
+ used: "已使用",
48
+ expired: "已过期",
49
+ };
50
+
51
+ // 初始化示例数据
52
+ initializeSampleData();
53
+
54
+ // Simple session storage (in-memory)
55
+ const sessions = new Map();
56
+
57
+ function generateSessionId() {
58
+ return Math.random().toString(36).substring(2) + Date.now().toString(36);
59
+ }
60
+
61
+ function isAuthenticated(req) {
62
+ const cookies = parseCookies(req.headers.cookie || '');
63
+ const sessionId = cookies.session_id;
64
+
65
+ if (!sessionId) return false;
66
+
67
+ const session = sessions.get(sessionId);
68
+ if (!session) return false;
69
+
70
+ // Check if session is expired (24 hours)
71
+ if (Date.now() - session.createdAt > 24 * 60 * 60 * 1000) {
72
+ sessions.delete(sessionId);
73
+ return false;
74
+ }
75
+
76
+ return session.authenticated;
77
+ }
78
+
79
+ function parseCookies(cookieHeader) {
80
+ const cookies = {};
81
+ if (!cookieHeader) return cookies;
82
+
83
+ cookieHeader.split(';').forEach(cookie => {
84
+ const parts = cookie.trim().split('=');
85
+ if (parts.length === 2) {
86
+ cookies[parts[0]] = parts[1];
87
+ }
88
+ });
89
+
90
+ return cookies;
91
+ }
92
+
93
+ function parseRequestBody(req) {
94
+ return new Promise((resolve, reject) => {
95
+ let body = "";
96
+ req.on("data", (chunk) => {
97
+ body += chunk.toString();
98
+ });
99
+ req.on("end", () => {
100
+ if (!body) {
101
+ resolve({});
102
+ return;
103
+ }
104
+
105
+ try {
106
+ resolve(JSON.parse(body));
107
+ } catch (error) {
108
+ reject(error);
109
+ }
110
+ });
111
+ });
112
+ }
113
+
114
+ function normalizeUserStatus(status) {
115
+ if (status === undefined || status === null || status === "") {
116
+ return "待激活";
117
+ }
118
+
119
+ const normalized = USER_STATUS_MAP[String(status).trim().toLowerCase()];
120
+ return normalized || String(status).trim();
121
+ }
122
+
123
+ function getUserStatusKey(status) {
124
+ return USER_STATUS_KEY_MAP[normalizeUserStatus(status)] || "inactive";
125
+ }
126
+
127
+ function normalizeService(service, fallback = "Claude Code") {
128
+ if (!service) return fallback;
129
+
130
+ const value = String(service).trim().toLowerCase();
131
+ const serviceMap = {
132
+ claude: "Claude Code",
133
+ claudecode: "Claude Code",
134
+ "claude-code": "Claude Code",
135
+ "claude code": "Claude Code",
136
+ codex: "Codex",
137
+ gpt: "GPT",
138
+ openai: "OpenAI",
139
+ gemini: "Gemini",
140
+ other: "其他",
141
+ };
142
+
143
+ return serviceMap[value] || String(service).trim();
144
+ }
145
+
146
+ function getServiceKey(service, fallback = "claude") {
147
+ const normalized = normalizeService(service, fallback === "codex" ? "Codex" : "Claude Code");
148
+ const value = String(normalized || "").trim().toLowerCase();
149
+ if (value.includes("codex")) return "codex";
150
+ if (value.includes("claude")) return "claude";
151
+ return String(service || fallback || "").trim().toLowerCase().replace(/\s+/g, "-") || fallback;
152
+ }
153
+
154
+ function serviceMatches(item, service) {
155
+ if (!service || service === "all") return true;
156
+ return getServiceKey(item.service) === getServiceKey(service);
157
+ }
158
+
159
+ function normalizeCodeStatus(codeOrStatus, expiresAt) {
160
+ const rawStatus =
161
+ typeof codeOrStatus === "object" && codeOrStatus !== null
162
+ ? codeOrStatus.status
163
+ : codeOrStatus;
164
+ const rawExpiresAt =
165
+ typeof codeOrStatus === "object" && codeOrStatus !== null
166
+ ? codeOrStatus.expiresAt
167
+ : expiresAt;
168
+
169
+ if (rawExpiresAt) {
170
+ const expiry = new Date(rawExpiresAt);
171
+ if (!Number.isNaN(expiry.getTime()) && expiry < new Date()) {
172
+ return "expired";
173
+ }
174
+ }
175
+
176
+ if (rawStatus === undefined || rawStatus === null || rawStatus === "") {
177
+ return "unused";
178
+ }
179
+
180
+ return CODE_STATUS_MAP[String(rawStatus).trim().toLowerCase()] || "unused";
181
+ }
182
+
183
+ function getCodeStatusLabel(status) {
184
+ return CODE_STATUS_LABEL_MAP[normalizeCodeStatus(status)] || "未使用";
185
+ }
186
+
187
+ function serializeUser(user) {
188
+ const status = normalizeUserStatus(user.status);
189
+ const service = normalizeService(user.service);
190
+ return {
191
+ ...user,
192
+ service,
193
+ serviceKey: getServiceKey(service),
194
+ serviceLabel: service,
195
+ status,
196
+ statusLabel: status,
197
+ statusKey: getUserStatusKey(status),
198
+ };
199
+ }
200
+
201
+ function serializeCode(code) {
202
+ const status = normalizeCodeStatus(code);
203
+ const service = normalizeService(code.service);
204
+ return {
205
+ ...code,
206
+ service,
207
+ serviceKey: getServiceKey(service),
208
+ serviceLabel: service,
209
+ allowedModels: code.allowedModels || (getServiceKey(service) === "codex" ? ["codex"] : ["claude"]),
210
+ status,
211
+ statusLabel: getCodeStatusLabel(status),
212
+ isExpired: status === "expired",
213
+ };
214
+ }
215
+
216
+ function trimTrailingSlash(value) {
217
+ return String(value || "").trim().replace(/\/+$/, "");
218
+ }
219
+
220
+ function getUpstreamConfigPath() {
221
+ return process.env.CLIPROXY_UPSTREAM_CONFIG || DEFAULT_CONFIG_PATH;
222
+ }
223
+
224
+ function readRawUpstreamConfig() {
225
+ return readJsonFile(getUpstreamConfigPath(), {});
226
+ }
227
+
228
+ function serializeUpstreamConfig(config) {
229
+ const services = config.services || {};
230
+ const claude = services.claude || {};
231
+ const codex = services.codex || {};
232
+ return {
233
+ provider: config.provider || "newapi",
234
+ baseUrl: config.baseUrl || "",
235
+ apiKey: "",
236
+ apiKeyConfigured: Boolean(config.apiKey),
237
+ apiKeyMasked: maskKey(config.apiKey || ""),
238
+ timeoutMs: config.timeoutMs || 10000,
239
+ claudeBaseUrl: claude.baseUrl || "",
240
+ codexBaseUrl: codex.baseUrl || "",
241
+ configPath: config.configPath || getUpstreamConfigPath(),
242
+ configured: Boolean(config.baseUrl && config.apiKey),
243
+ };
244
+ }
245
+
246
+ function buildUpstreamConfigFromPayload(payload = {}) {
247
+ const currentRaw = readRawUpstreamConfig();
248
+ const current = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
249
+ const services = { ...(currentRaw.services || {}) };
250
+
251
+ if (!services.claude) services.claude = {};
252
+ if (!services.codex) services.codex = {};
253
+
254
+ if (Object.prototype.hasOwnProperty.call(payload, "claudeBaseUrl")) {
255
+ services.claude.baseUrl = trimTrailingSlash(payload.claudeBaseUrl);
256
+ }
257
+ if (Object.prototype.hasOwnProperty.call(payload, "codexBaseUrl")) {
258
+ services.codex.baseUrl = trimTrailingSlash(payload.codexBaseUrl);
259
+ }
260
+
261
+ const nextApiKey = typeof payload.apiKey === "string" && payload.apiKey.trim()
262
+ ? payload.apiKey.trim()
263
+ : (currentRaw.apiKey || current.apiKey || "");
264
+
265
+ const timeoutMs = parseInt(payload.timeoutMs || currentRaw.timeoutMs || current.timeoutMs || "10000", 10) || 10000;
266
+
267
+ return {
268
+ ...currentRaw,
269
+ provider: String(payload.provider || currentRaw.provider || "newapi").trim() || "newapi",
270
+ baseUrl: trimTrailingSlash(payload.baseUrl ?? currentRaw.baseUrl ?? current.baseUrl),
271
+ apiKey: nextApiKey,
272
+ services,
273
+ timeoutMs,
274
+ updatedAt: new Date().toISOString(),
275
+ };
276
+ }
277
+
278
+ function paginate(items, page, limit) {
279
+ const safePage = Math.max(parseInt(page || "1", 10) || 1, 1);
280
+ const safeLimit = Math.max(parseInt(limit || "50", 10) || 50, 1);
281
+ const start = (safePage - 1) * safeLimit;
282
+ return {
283
+ items: items.slice(start, start + safeLimit),
284
+ page: safePage,
285
+ limit: safeLimit,
286
+ total: items.length,
287
+ };
288
+ }
289
+
290
+ // Get all network interfaces
291
+ function getNetworkAddresses() {
292
+ const interfaces = os.networkInterfaces();
293
+ const addresses = [];
294
+
295
+ for (const name of Object.keys(interfaces)) {
296
+ for (const iface of interfaces[name]) {
297
+ if (iface.family === 'IPv4' && !iface.internal) {
298
+ addresses.push(iface.address);
299
+ }
300
+ }
301
+ }
302
+
303
+ return addresses;
304
+ }
305
+
306
+ // MIME type mapping
307
+ const mimeTypes = {
308
+ '.html': 'text/html',
309
+ '.js': 'application/javascript',
310
+ '.css': 'text/css',
311
+ '.json': 'application/json',
312
+ '.png': 'image/png',
313
+ '.jpg': 'image/jpeg',
314
+ '.gif': 'image/gif',
315
+ '.svg': 'image/svg+xml',
316
+ '.ico': 'image/x-icon',
317
+ '.woff': 'font/woff',
318
+ '.woff2': 'font/woff2',
319
+ '.ttf': 'font/ttf',
320
+ '.eot': 'application/vnd.ms-fontobject'
321
+ };
322
+
323
+ function serveStaticFile(filePath, res) {
324
+ fs.readFile(filePath, (err, data) => {
325
+ if (err) {
326
+ res.writeHead(404, { "Content-Type": "text/plain" });
327
+ res.end("404 Not Found");
328
+ return;
329
+ }
330
+
331
+ const ext = path.extname(filePath).toLowerCase();
332
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
333
+
334
+ // For admin panel files, always use no-cache to prevent stale content
335
+ const isAdminFile = filePath.includes('admin-panel');
336
+
337
+ res.writeHead(200, {
338
+ "Content-Type": contentType,
339
+ "Cache-Control": (ext === '.html' || isAdminFile)
340
+ ? 'no-cache, no-store, must-revalidate'
341
+ : 'public, max-age=31536000',
342
+ "Pragma": "no-cache",
343
+ "Expires": "0"
344
+ });
345
+ res.end(data);
346
+ });
347
+ }
348
+
349
+ const server = http.createServer((req, res) => {
350
+ // Add CORS headers for external access
351
+ res.setHeader('Access-Control-Allow-Origin', '*');
352
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
353
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
354
+
355
+ if (req.method === 'OPTIONS') {
356
+ res.writeHead(200);
357
+ res.end();
358
+ return;
359
+ }
360
+
361
+ // Parse URL and remove query string
362
+ let urlPath = req.url.split('?')[0];
363
+
364
+ // Handle login API
365
+ if (urlPath === "/api/login" && req.method === "POST") {
366
+ let body = '';
367
+ req.on('data', chunk => {
368
+ body += chunk.toString();
369
+ });
370
+ req.on('end', () => {
371
+ try {
372
+ const data = JSON.parse(body);
373
+ if (data.password === ADMIN_PASSWORD) {
374
+ const sessionId = generateSessionId();
375
+ sessions.set(sessionId, {
376
+ authenticated: true,
377
+ createdAt: Date.now()
378
+ });
379
+
380
+ res.writeHead(200, {
381
+ 'Content-Type': 'application/json',
382
+ 'Set-Cookie': `session_id=${sessionId}; HttpOnly; Path=/; Max-Age=86400`
383
+ });
384
+ res.end(JSON.stringify({ success: true, message: '登录成功' }));
385
+ } else {
386
+ res.writeHead(401, { 'Content-Type': 'application/json' });
387
+ res.end(JSON.stringify({ success: false, message: '密码错误' }));
388
+ }
389
+ } catch (e) {
390
+ res.writeHead(400, { 'Content-Type': 'application/json' });
391
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
392
+ }
393
+ });
394
+ return;
395
+ }
396
+
397
+ // Handle logout API
398
+ if (urlPath === "/api/logout" && req.method === "POST") {
399
+ const cookies = parseCookies(req.headers.cookie || '');
400
+ const sessionId = cookies.session_id;
401
+ if (sessionId) {
402
+ sessions.delete(sessionId);
403
+ }
404
+ res.writeHead(200, {
405
+ 'Content-Type': 'application/json',
406
+ 'Set-Cookie': 'session_id=; HttpOnly; Path=/; Max-Age=0'
407
+ });
408
+ res.end(JSON.stringify({ success: true, message: '已退出登录' }));
409
+ return;
410
+ }
411
+
412
+ // Handle check auth API
413
+ if (urlPath === "/api/check-auth" && req.method === "GET") {
414
+ const authenticated = isAuthenticated(req);
415
+ res.writeHead(200, { 'Content-Type': 'application/json' });
416
+ res.end(JSON.stringify({ authenticated }));
417
+ return;
418
+ }
419
+
420
+ // Handle server-side settings API
421
+ if (urlPath === "/api/settings" && req.method === "GET") {
422
+ if (!isAuthenticated(req)) {
423
+ res.writeHead(401, { 'Content-Type': 'application/json' });
424
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
425
+ return;
426
+ }
427
+
428
+ const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
429
+ res.writeHead(200, { 'Content-Type': 'application/json' });
430
+ res.end(JSON.stringify({
431
+ success: true,
432
+ data: {
433
+ upstream: serializeUpstreamConfig(upstream),
434
+ },
435
+ }));
436
+ return;
437
+ }
438
+
439
+ if (urlPath === "/api/settings" && req.method === "PUT") {
440
+ if (!isAuthenticated(req)) {
441
+ res.writeHead(401, { 'Content-Type': 'application/json' });
442
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
443
+ return;
444
+ }
445
+
446
+ parseRequestBody(req).then((payload) => {
447
+ const upstreamPayload = payload.upstream || {};
448
+ const nextConfig = buildUpstreamConfigFromPayload(upstreamPayload);
449
+
450
+ if (!nextConfig.baseUrl) {
451
+ res.writeHead(400, { 'Content-Type': 'application/json' });
452
+ res.end(JSON.stringify({ success: false, message: '请填写上游 API 地址' }));
453
+ return;
454
+ }
455
+
456
+ if (!nextConfig.apiKey) {
457
+ res.writeHead(400, { 'Content-Type': 'application/json' });
458
+ res.end(JSON.stringify({ success: false, message: '请填写上游 API Key' }));
459
+ return;
460
+ }
461
+
462
+ writeJsonFile(getUpstreamConfigPath(), nextConfig);
463
+ const saved = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
464
+ res.writeHead(200, { 'Content-Type': 'application/json' });
465
+ res.end(JSON.stringify({
466
+ success: true,
467
+ message: '系统设置已保存',
468
+ data: {
469
+ upstream: serializeUpstreamConfig(saved),
470
+ },
471
+ }));
472
+ }).catch((error) => {
473
+ console.error('Save settings error:', error);
474
+ res.writeHead(400, { 'Content-Type': 'application/json' });
475
+ res.end(JSON.stringify({ success: false, message: '设置保存失败' }));
476
+ });
477
+ return;
478
+ }
479
+
480
+ if (urlPath === "/api/settings/upstream/test" && req.method === "POST") {
481
+ if (!isAuthenticated(req)) {
482
+ res.writeHead(401, { 'Content-Type': 'application/json' });
483
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
484
+ return;
485
+ }
486
+
487
+ parseRequestBody(req).then(async (payload) => {
488
+ const upstreamPayload = payload.upstream || payload || {};
489
+ const testConfig = buildUpstreamConfigFromPayload(upstreamPayload);
490
+ const baseUrl = trimTrailingSlash(upstreamPayload.baseUrl || testConfig.baseUrl);
491
+ const apiKey = String(upstreamPayload.apiKey || testConfig.apiKey || "").trim();
492
+
493
+ if (!baseUrl || !apiKey) {
494
+ res.writeHead(400, { 'Content-Type': 'application/json' });
495
+ res.end(JSON.stringify({ success: false, message: '请填写上游 API 地址和 API Key' }));
496
+ return;
497
+ }
498
+
499
+ try {
500
+ const result = await verifyNewApiKey({ ...testConfig, baseUrl }, apiKey);
501
+ res.writeHead(200, { 'Content-Type': 'application/json' });
502
+ res.end(JSON.stringify({
503
+ success: result.valid,
504
+ message: result.valid ? '上游连接成功' : `上游验证失败:${result.error || result.status}`,
505
+ data: {
506
+ valid: result.valid,
507
+ status: result.status,
508
+ modelsUrl: result.modelsUrl,
509
+ modelCount: Array.isArray(result.models) ? result.models.length : 0,
510
+ apiKeyMasked: maskKey(apiKey),
511
+ },
512
+ }));
513
+ } catch (error) {
514
+ res.writeHead(200, { 'Content-Type': 'application/json' });
515
+ res.end(JSON.stringify({
516
+ success: false,
517
+ message: `上游连接失败:${error.message}`,
518
+ data: { valid: false },
519
+ }));
520
+ }
521
+ }).catch((error) => {
522
+ console.error('Test upstream error:', error);
523
+ res.writeHead(400, { 'Content-Type': 'application/json' });
524
+ res.end(JSON.stringify({ success: false, message: '上游测试失败' }));
525
+ });
526
+ return;
527
+ }
528
+
529
+ // Handle users API
530
+ if (urlPath === "/api/users" && req.method === "GET") {
531
+ if (!isAuthenticated(req)) {
532
+ res.writeHead(401, { 'Content-Type': 'application/json' });
533
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
534
+ return;
535
+ }
536
+
537
+ const url = new URL(req.url, `http://${req.headers.host}`);
538
+ const query = (url.searchParams.get("q") || url.searchParams.get("search") || "").trim();
539
+ const status = url.searchParams.get("status");
540
+ const service = url.searchParams.get("service");
541
+ const { items, page, limit, total } = paginate(
542
+ userDb
543
+ .getAll()
544
+ .map(serializeUser)
545
+ .filter((user) => {
546
+ if (status && status !== "all" && getUserStatusKey(user.status) !== getUserStatusKey(status) && user.status !== normalizeUserStatus(status)) {
547
+ return false;
548
+ }
549
+
550
+ if (!serviceMatches(user, service)) {
551
+ return false;
552
+ }
553
+
554
+ if (!query) return true;
555
+ const keyword = query.toLowerCase();
556
+ return (
557
+ String(user.username || "").toLowerCase().includes(keyword) ||
558
+ String(user.email || "").toLowerCase().includes(keyword) ||
559
+ String(user.id || "").toLowerCase().includes(keyword)
560
+ );
561
+ }),
562
+ url.searchParams.get("page"),
563
+ url.searchParams.get("limit")
564
+ );
565
+
566
+ res.writeHead(200, { 'Content-Type': 'application/json' });
567
+ res.end(JSON.stringify({ success: true, data: items, page, limit, total }));
568
+ return;
569
+ }
570
+
571
+ if (urlPath === "/api/users" && req.method === "POST") {
572
+ if (!isAuthenticated(req)) {
573
+ res.writeHead(401, { 'Content-Type': 'application/json' });
574
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
575
+ return;
576
+ }
577
+
578
+ parseRequestBody(req).then((userData) => {
579
+ if (!userData.username || !userData.email) {
580
+ res.writeHead(400, { 'Content-Type': 'application/json' });
581
+ res.end(JSON.stringify({ success: false, message: '用户名和邮箱为必填项' }));
582
+ return;
583
+ }
584
+
585
+ if (userDb.getByUsername(userData.username)) {
586
+ res.writeHead(400, { 'Content-Type': 'application/json' });
587
+ res.end(JSON.stringify({ success: false, message: '用户名已存在' }));
588
+ return;
589
+ }
590
+
591
+ const service = normalizeService(userData.service);
592
+ const newUser = userDb.create({
593
+ ...userData,
594
+ service,
595
+ serviceKey: getServiceKey(service),
596
+ status: normalizeUserStatus(userData.status),
597
+ });
598
+
599
+ res.writeHead(201, { 'Content-Type': 'application/json' });
600
+ res.end(JSON.stringify({ success: true, data: serializeUser(newUser) }));
601
+ }).catch(() => {
602
+ res.writeHead(400, { 'Content-Type': 'application/json' });
603
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
604
+ });
605
+ return;
606
+ }
607
+
608
+ if (urlPath.startsWith("/api/users/") && req.method === "PUT") {
609
+ if (!isAuthenticated(req)) {
610
+ res.writeHead(401, { 'Content-Type': 'application/json' });
611
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
612
+ return;
613
+ }
614
+
615
+ const userId = urlPath.split('/')[3];
616
+ parseRequestBody(req).then((updates) => {
617
+ const payload = { ...updates };
618
+ if (payload.status !== undefined) {
619
+ payload.status = normalizeUserStatus(payload.status);
620
+ }
621
+ if (payload.service !== undefined) {
622
+ payload.service = normalizeService(payload.service);
623
+ payload.serviceKey = getServiceKey(payload.service);
624
+ }
625
+
626
+ const updatedUser = userDb.update(userId, payload);
627
+
628
+ if (!updatedUser) {
629
+ res.writeHead(404, { 'Content-Type': 'application/json' });
630
+ res.end(JSON.stringify({ success: false, message: '用户不存在' }));
631
+ return;
632
+ }
633
+
634
+ res.writeHead(200, { 'Content-Type': 'application/json' });
635
+ res.end(JSON.stringify({ success: true, data: serializeUser(updatedUser) }));
636
+ }).catch(() => {
637
+ res.writeHead(400, { 'Content-Type': 'application/json' });
638
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
639
+ });
640
+ return;
641
+ }
642
+
643
+ if (urlPath.startsWith("/api/users/") && req.method === "DELETE") {
644
+ if (!isAuthenticated(req)) {
645
+ res.writeHead(401, { 'Content-Type': 'application/json' });
646
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
647
+ return;
648
+ }
649
+
650
+ const userId = urlPath.split('/')[3];
651
+ const deleted = userDb.delete(userId);
652
+
653
+ if (!deleted) {
654
+ res.writeHead(404, { 'Content-Type': 'application/json' });
655
+ res.end(JSON.stringify({ success: false, message: '用户不存在' }));
656
+ return;
657
+ }
658
+
659
+ res.writeHead(200, { 'Content-Type': 'application/json' });
660
+ res.end(JSON.stringify({ success: true, message: '删除成功' }));
661
+ return;
662
+ }
663
+
664
+ // Handle codes API
665
+ if (urlPath === "/api/codes" && req.method === "GET") {
666
+ if (!isAuthenticated(req)) {
667
+ res.writeHead(401, { 'Content-Type': 'application/json' });
668
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
669
+ return;
670
+ }
671
+
672
+ const url = new URL(req.url, `http://${req.headers.host}`);
673
+ const query = (url.searchParams.get("q") || url.searchParams.get("search") || "").trim();
674
+ const status = url.searchParams.get("status");
675
+ const service = url.searchParams.get("service");
676
+ const { items, page, limit, total } = paginate(
677
+ codeDb
678
+ .getAll()
679
+ .map(serializeCode)
680
+ .filter((code) => {
681
+ if (status && status !== "all" && code.status !== normalizeCodeStatus(status)) {
682
+ return false;
683
+ }
684
+
685
+ if (!serviceMatches(code, service)) {
686
+ return false;
687
+ }
688
+
689
+ if (!query) return true;
690
+ const keyword = query.toLowerCase();
691
+ return (
692
+ String(code.code || "").toLowerCase().includes(keyword) ||
693
+ String(code.usedBy || "").toLowerCase().includes(keyword) ||
694
+ String(code.id || "").toLowerCase().includes(keyword)
695
+ );
696
+ }),
697
+ url.searchParams.get("page"),
698
+ url.searchParams.get("limit")
699
+ );
700
+
701
+ res.writeHead(200, { 'Content-Type': 'application/json' });
702
+ res.end(JSON.stringify({ success: true, data: items, page, limit, total }));
703
+ return;
704
+ }
705
+
706
+ if (urlPath === "/api/codes" && req.method === "POST") {
707
+ if (!isAuthenticated(req)) {
708
+ res.writeHead(401, { 'Content-Type': 'application/json' });
709
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
710
+ return;
711
+ }
712
+
713
+ parseRequestBody(req).then((codeData) => {
714
+ const count = Math.max(parseInt(codeData.count || "1", 10) || 1, 1);
715
+ const requestedServices = Array.isArray(codeData.services) && codeData.services.length
716
+ ? codeData.services
717
+ : [codeData.service || codeData.platform || "Claude Code"];
718
+ const services = requestedServices
719
+ .map((service) => normalizeService(service))
720
+ .filter((service, index, list) => service && list.indexOf(service) === index);
721
+ const createdCodes = [];
722
+
723
+ if (!services.length) {
724
+ res.writeHead(400, { 'Content-Type': 'application/json' });
725
+ res.end(JSON.stringify({ success: false, message: '请至少选择一个服务渠道' }));
726
+ return;
727
+ }
728
+
729
+ let expiresAt = codeData.expiresAt;
730
+ if (codeData.duration) {
731
+ const expiry = new Date();
732
+ expiry.setDate(expiry.getDate() + parseInt(codeData.duration, 10));
733
+ expiresAt = expiry.toISOString();
734
+ }
735
+
736
+ for (const service of services) {
737
+ for (let i = 0; i < count; i++) {
738
+ const serviceKey = getServiceKey(service);
739
+ const newCode = codeDb.create({
740
+ ...(codeData.code && services.length === 1 && count === 1 ? { code: codeData.code } : {}),
741
+ service,
742
+ serviceKey,
743
+ allowedModels: [serviceKey],
744
+ quota: codeData.quota || { total: 1000000, used: 0 },
745
+ expiresAt,
746
+ notes: codeData.notes || codeData.note,
747
+ status: "unused",
748
+ });
749
+ createdCodes.push(serializeCode(newCode));
750
+ }
751
+ }
752
+
753
+ res.writeHead(201, { 'Content-Type': 'application/json' });
754
+ res.end(JSON.stringify({
755
+ success: true,
756
+ data: count === 1 ? createdCodes[0] : createdCodes,
757
+ message: count > 1 ? `成功生成 ${count} 个激活码` : '激活码创建成功'
758
+ }));
759
+ }).catch((error) => {
760
+ console.error('Create code error:', error);
761
+ res.writeHead(400, { 'Content-Type': 'application/json' });
762
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
763
+ });
764
+ return;
765
+ }
766
+
767
+ if (urlPath.startsWith("/api/codes/") && req.method === "PUT") {
768
+ if (!isAuthenticated(req)) {
769
+ res.writeHead(401, { 'Content-Type': 'application/json' });
770
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
771
+ return;
772
+ }
773
+
774
+ const codeId = urlPath.split('/')[3];
775
+ parseRequestBody(req).then((updates) => {
776
+ const payload = { ...updates };
777
+ if (payload.service !== undefined) {
778
+ payload.service = normalizeService(payload.service);
779
+ payload.serviceKey = getServiceKey(payload.service);
780
+ payload.allowedModels = [payload.serviceKey];
781
+ }
782
+ if (payload.status !== undefined) {
783
+ payload.status = normalizeCodeStatus(payload.status);
784
+ }
785
+
786
+ const updatedCode = codeDb.update(codeId, payload);
787
+ if (!updatedCode) {
788
+ res.writeHead(404, { 'Content-Type': 'application/json' });
789
+ res.end(JSON.stringify({ success: false, message: '激活码不存在' }));
790
+ return;
791
+ }
792
+
793
+ res.writeHead(200, { 'Content-Type': 'application/json' });
794
+ res.end(JSON.stringify({ success: true, data: serializeCode(updatedCode) }));
795
+ }).catch(() => {
796
+ res.writeHead(400, { 'Content-Type': 'application/json' });
797
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
798
+ });
799
+ return;
800
+ }
801
+
802
+ if (urlPath.startsWith("/api/codes/") && req.method === "DELETE") {
803
+ if (!isAuthenticated(req)) {
804
+ res.writeHead(401, { 'Content-Type': 'application/json' });
805
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
806
+ return;
807
+ }
808
+
809
+ const codeId = urlPath.split('/')[3];
810
+ const deleted = codeDb.delete(codeId);
811
+
812
+ if (!deleted) {
813
+ res.writeHead(404, { 'Content-Type': 'application/json' });
814
+ res.end(JSON.stringify({ success: false, message: '激活码不存在' }));
815
+ return;
816
+ }
817
+
818
+ res.writeHead(200, { 'Content-Type': 'application/json' });
819
+ res.end(JSON.stringify({ success: true, message: '删除成功' }));
820
+ return;
821
+ }
822
+
823
+ // CDK Activation API - User端激活激活码
824
+ if (urlPath === "/api/activate" && req.method === "POST") {
825
+ parseRequestBody(req).then((data) => {
826
+ const { code: activationCode, userId, username, email } = data;
827
+ const requestedService = data.service || data.platform || data.product || "";
828
+
829
+ if (!activationCode || !activationCode.trim()) {
830
+ res.writeHead(400, { 'Content-Type': 'application/json' });
831
+ res.end(JSON.stringify({ success: false, message: '激活码不能为空' }));
832
+ return;
833
+ }
834
+
835
+ // 查找激活码
836
+ const code = codeDb.getAll().find(c => c.code === activationCode.trim());
837
+
838
+ if (!code) {
839
+ res.writeHead(404, { 'Content-Type': 'application/json' });
840
+ res.end(JSON.stringify({ success: false, message: '激活码不存在' }));
841
+ return;
842
+ }
843
+
844
+ const serializedCode = serializeCode(code);
845
+ if (requestedService && !serviceMatches(serializedCode, requestedService)) {
846
+ res.writeHead(400, { 'Content-Type': 'application/json' });
847
+ res.end(JSON.stringify({
848
+ success: false,
849
+ message: `此激活码属于 ${serializedCode.serviceLabel},不能用于 ${normalizeService(requestedService)}`
850
+ }));
851
+ return;
852
+ }
853
+
854
+ // 检查激活码状态
855
+ if (normalizeCodeStatus(code) === 'used') {
856
+ res.writeHead(400, { 'Content-Type': 'application/json' });
857
+ res.end(JSON.stringify({ success: false, message: '激活码已被使用' }));
858
+ return;
859
+ }
860
+
861
+ // 检查是否过期
862
+ if (code.expiresAt && new Date(code.expiresAt) < new Date()) {
863
+ res.writeHead(400, { 'Content-Type': 'application/json' });
864
+ res.end(JSON.stringify({ success: false, message: '激活码已过期' }));
865
+ return;
866
+ }
867
+
868
+ // 更新激活码状态
869
+ const updatedCode = codeDb.update(code.id, {
870
+ status: 'used',
871
+ usedBy: userId || username || email || 'unknown',
872
+ lastUsedAt: new Date().toISOString(),
873
+ activatedService: serializedCode.service,
874
+ activatedServiceKey: serializedCode.serviceKey
875
+ });
876
+
877
+ if (!updatedCode) {
878
+ res.writeHead(500, { 'Content-Type': 'application/json' });
879
+ res.end(JSON.stringify({ success: false, message: '激活失败' }));
880
+ return;
881
+ }
882
+
883
+ // 返回激活成功信息
884
+ res.writeHead(200, { 'Content-Type': 'application/json' });
885
+ res.end(JSON.stringify({
886
+ success: true,
887
+ message: '激活成功',
888
+ data: {
889
+ code: updatedCode.code,
890
+ service: serializeCode(updatedCode).service,
891
+ serviceKey: serializeCode(updatedCode).serviceKey,
892
+ allowedModels: serializeCode(updatedCode).allowedModels,
893
+ quota: updatedCode.quota,
894
+ expiresAt: updatedCode.expiresAt,
895
+ activatedAt: updatedCode.lastUsedAt
896
+ }
897
+ }));
898
+ }).catch((error) => {
899
+ console.error('Activation error:', error);
900
+ res.writeHead(400, { 'Content-Type': 'application/json' });
901
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
902
+ });
903
+ return;
904
+ }
905
+
906
+ // CDK Quota Refresh API - 每日额度刷新
907
+ if (urlPath === "/api/codes/refresh-quota" && req.method === "POST") {
908
+ if (!isAuthenticated(req)) {
909
+ res.writeHead(401, { 'Content-Type': 'application/json' });
910
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
911
+ return;
912
+ }
913
+
914
+ parseRequestBody(req).then((data) => {
915
+ const { codeId } = data;
916
+
917
+ if (!codeId) {
918
+ res.writeHead(400, { 'Content-Type': 'application/json' });
919
+ res.end(JSON.stringify({ success: false, message: '激活码ID不能为空' }));
920
+ return;
921
+ }
922
+
923
+ const code = codeDb.getAll().find(c => c.id === codeId);
924
+
925
+ if (!code) {
926
+ res.writeHead(404, { 'Content-Type': 'application/json' });
927
+ res.end(JSON.stringify({ success: false, message: '激活码不存在' }));
928
+ return;
929
+ }
930
+
931
+ // 检查是否需要刷新(每日刷新逻辑)
932
+ const now = new Date();
933
+ const lastRefresh = code.lastQuotaRefresh ? new Date(code.lastQuotaRefresh) : null;
934
+
935
+ // 如果今天还没刷新过,则刷新
936
+ const needsRefresh = !lastRefresh ||
937
+ lastRefresh.toDateString() !== now.toDateString();
938
+
939
+ if (!needsRefresh) {
940
+ res.writeHead(200, { 'Content-Type': 'application/json' });
941
+ res.end(JSON.stringify({
942
+ success: true,
943
+ message: '今日额度已刷新',
944
+ data: serializeCode(code)
945
+ }));
946
+ return;
947
+ }
948
+
949
+ // 刷新额度
950
+ const dailyQuota = code.quota?.daily || 100000;
951
+ const updatedCode = codeDb.update(codeId, {
952
+ quota: {
953
+ ...code.quota,
954
+ used: 0,
955
+ daily: dailyQuota
956
+ },
957
+ lastQuotaRefresh: now.toISOString()
958
+ });
959
+
960
+ res.writeHead(200, { 'Content-Type': 'application/json' });
961
+ res.end(JSON.stringify({
962
+ success: true,
963
+ message: '额度刷新成功',
964
+ data: serializeCode(updatedCode)
965
+ }));
966
+ }).catch((error) => {
967
+ console.error('Quota refresh error:', error);
968
+ res.writeHead(400, { 'Content-Type': 'application/json' });
969
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
970
+ });
971
+ return;
972
+ }
973
+
974
+ // Handle stats API
975
+ if (urlPath === "/api/stats" && req.method === "GET") {
976
+ if (!isAuthenticated(req)) {
977
+ res.writeHead(401, { 'Content-Type': 'application/json' });
978
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
979
+ return;
980
+ }
981
+
982
+ const url = new URL(req.url, `http://${req.headers.host}`);
983
+ const service = url.searchParams.get("service");
984
+ const users = userDb.getAll().map(serializeUser).filter((user) => serviceMatches(user, service));
985
+ const codes = codeDb.getAll().map(serializeCode).filter((code) => serviceMatches(code, service));
986
+
987
+ const totalUsers = users.length;
988
+ const activeUsers = users.filter(u => u.statusKey === 'active').length;
989
+ const totalCodes = codes.length;
990
+ const usedCodes = codes.filter(c => c.status === 'used').length;
991
+ const unusedCodes = codes.filter(c => c.status === 'unused').length;
992
+ const expiredCodes = codes.filter(c => c.status === 'expired').length;
993
+
994
+ const stats = {
995
+ totalUsers,
996
+ activeUsers,
997
+ totalCodes,
998
+ usedCodes,
999
+ unusedCodes,
1000
+ expiredCodes,
1001
+ systemStatus: '运行正常',
1002
+ uptime: Math.floor(process.uptime() / 86400) + ' 天'
1003
+ };
1004
+
1005
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1006
+ res.end(JSON.stringify({ success: true, data: stats }));
1007
+ return;
1008
+ }
1009
+
1010
+ // Mock API for user frontend - usage data
1011
+ if (urlPath === "/api/user/usage" && req.method === "GET") {
1012
+ const mockUsageData = {
1013
+ success: true,
1014
+ data: {
1015
+ total_requests: 15234,
1016
+ total_tokens: 2456789,
1017
+ today_requests: 342,
1018
+ today_tokens: 45678,
1019
+ quota: {
1020
+ total: 10000000,
1021
+ used: 2456789,
1022
+ remaining: 7543211
1023
+ },
1024
+ daily_stats: [
1025
+ { date: '2026-03-30', requests: 234, tokens: 34567 },
1026
+ { date: '2026-03-31', requests: 289, tokens: 42345 },
1027
+ { date: '2026-04-01', requests: 312, tokens: 45678 },
1028
+ { date: '2026-04-02', requests: 298, tokens: 43210 },
1029
+ { date: '2026-04-03', requests: 276, tokens: 39876 },
1030
+ { date: '2026-04-04', requests: 301, tokens: 44321 },
1031
+ { date: '2026-04-05', requests: 342, tokens: 45678 }
1032
+ ]
1033
+ }
1034
+ };
1035
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1036
+ res.end(JSON.stringify(mockUsageData));
1037
+ return;
1038
+ }
1039
+
1040
+ // Mock API for user frontend - user info
1041
+ if (urlPath === "/api/user/info" && req.method === "GET") {
1042
+ const mockUserInfo = {
1043
+ success: true,
1044
+ data: {
1045
+ username: 'test_user',
1046
+ email: 'test@example.com',
1047
+ service: 'Claude Code',
1048
+ status: 'active',
1049
+ created_at: '2026-03-15T00:00:00.000Z',
1050
+ api_key: 'sk-test-' + Math.random().toString(36).substring(2, 15)
1051
+ }
1052
+ };
1053
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1054
+ res.end(JSON.stringify(mockUserInfo));
1055
+ return;
1056
+ }
1057
+
1058
+ // Activity API
1059
+ if (urlPath === "/api/activity" && req.method === "GET") {
1060
+ if (!isAuthenticated(req)) {
1061
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1062
+ res.end(JSON.stringify({ success: false, message: '未授权' }));
1063
+ return;
1064
+ }
1065
+
1066
+ // Mock activity data
1067
+ const activities = [
1068
+ { timestamp: new Date().toISOString(), user: 'admin', action: '登录系统', status: 'success' },
1069
+ { timestamp: new Date(Date.now() - 300000).toISOString(), user: 'test_user', action: '创建激活码', status: 'success' },
1070
+ { timestamp: new Date(Date.now() - 600000).toISOString(), user: 'admin', action: '更新用户信息', status: 'success' },
1071
+ { timestamp: new Date(Date.now() - 900000).toISOString(), user: 'test_user', action: '查看统计数据', status: 'success' },
1072
+ { timestamp: new Date(Date.now() - 1200000).toISOString(), user: 'admin', action: '删除激活码', status: 'success' }
1073
+ ];
1074
+
1075
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1076
+ res.end(JSON.stringify({ success: true, data: activities }));
1077
+ return;
1078
+ }
1079
+
1080
+ // Verify API key or inspect activation code capability
1081
+ if (urlPath === "/api/verify" && req.method === "POST") {
1082
+ parseRequestBody(req).then((data) => {
1083
+ if (data.code) {
1084
+ const code = codeDb.getByCode(String(data.code).trim());
1085
+ if (!code) {
1086
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1087
+ res.end(JSON.stringify({ success: false, valid: false, message: '激活码不存在' }));
1088
+ return;
1089
+ }
1090
+
1091
+ const serializedCode = serializeCode(code);
1092
+ if (serializedCode.status !== 'unused') {
1093
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1094
+ res.end(JSON.stringify({ success: false, valid: false, message: serializedCode.status === 'expired' ? '激活码已过期' : '激活码已被使用' }));
1095
+ return;
1096
+ }
1097
+
1098
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1099
+ res.end(JSON.stringify({
1100
+ success: true,
1101
+ valid: true,
1102
+ message: '验证成功',
1103
+ data: {
1104
+ code: serializedCode.code,
1105
+ service: serializedCode.service,
1106
+ services: [serializedCode.serviceKey],
1107
+ platforms: serializedCode.serviceKey === 'codex' ? ['codex-cli'] : ['claude-code'],
1108
+ allowedModels: serializedCode.allowedModels,
1109
+ quota: serializedCode.quota,
1110
+ expiresAt: serializedCode.expiresAt
1111
+ }
1112
+ }));
1113
+ return;
1114
+ }
1115
+
1116
+ if (data.api_key && data.api_key.startsWith('sk-test-')) {
1117
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1118
+ res.end(JSON.stringify({
1119
+ success: true,
1120
+ valid: true,
1121
+ message: '验证成功',
1122
+ data: {
1123
+ username: 'test_user',
1124
+ email: 'test@example.com',
1125
+ service: 'Claude Code'
1126
+ }
1127
+ }));
1128
+ return;
1129
+ }
1130
+
1131
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1132
+ res.end(JSON.stringify({
1133
+ success: false,
1134
+ valid: false,
1135
+ message: '验证失败,请检查 API Key 是否正确'
1136
+ }));
1137
+ }).catch(() => {
1138
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1139
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
1140
+ });
1141
+ return;
1142
+ }
1143
+
1144
+ // User frontend API endpoints - /user/api/v1/*
1145
+ if (urlPath.startsWith("/user/api/v1/")) {
1146
+ const apiPath = urlPath.replace("/user/api/v1", "");
1147
+
1148
+ // GET /user/api/v1/me - Get current user info
1149
+ if (apiPath === "/me" && req.method === "GET") {
1150
+ // 从 session 或 query 获取用户信息
1151
+ const username = req.headers['x-user-id'] || 'demo_user';
1152
+ const user = userDb.getByUsername(username) || userDb.getAll()[0];
1153
+
1154
+ if (!user) {
1155
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1156
+ res.end(JSON.stringify({ success: false, message: 'User not found' }));
1157
+ return;
1158
+ }
1159
+
1160
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1161
+ res.end(JSON.stringify({
1162
+ success: true,
1163
+ data: {
1164
+ id: user.id,
1165
+ username: user.username,
1166
+ email: user.email,
1167
+ service: user.service,
1168
+ status: user.status,
1169
+ created_at: user.registeredAt || user.createdAt,
1170
+ api_key: user.apiKey || 'sk-demo-' + user.id
1171
+ }
1172
+ }));
1173
+ return;
1174
+ }
1175
+
1176
+ // GET /user/api/v1/usage/history - Get usage history
1177
+ if (apiPath.startsWith("/usage/history") && req.method === "GET") {
1178
+ const username = req.headers['x-user-id'] || 'demo_user';
1179
+ const user = userDb.getByUsername(username) || userDb.getAll()[0];
1180
+
1181
+ // 查找用户对应的激活码(通过 usedBy 或 username)
1182
+ const codes = codeDb.getAll().filter(c =>
1183
+ c.usedBy === username ||
1184
+ c.usedBy === user?.username ||
1185
+ c.status === 'used'
1186
+ );
1187
+ const code = codes[0];
1188
+
1189
+ // 计算真实额度数据
1190
+ const totalQuota = code?.quota?.total || 100000;
1191
+ const usedQuota = code?.quota?.used || 0;
1192
+ const dailyLimit = code?.quota?.dailyLimit || 5000;
1193
+ const dailyUsed = code?.quota?.dailyUsed || 0;
1194
+
1195
+ // 生成最近7天的模拟数据(实际生产中应该记录真实使用数据)
1196
+ const today = new Date();
1197
+ const dailyStats = [];
1198
+ for (let i = 6; i >= 0; i--) {
1199
+ const date = new Date(today);
1200
+ date.setDate(date.getDate() - i);
1201
+ const requests = Math.floor(Math.random() * 200) + 100;
1202
+ const tokens = requests * Math.floor(Math.random() * 150) + 100;
1203
+ dailyStats.push({
1204
+ date: date.toISOString().split('T')[0],
1205
+ requests,
1206
+ tokens
1207
+ });
1208
+ }
1209
+
1210
+ const mockUsageData = {
1211
+ success: true,
1212
+ data: {
1213
+ total_requests: usedQuota,
1214
+ total_tokens: usedQuota * 12, // 估算 token 数量
1215
+ today_requests: dailyUsed,
1216
+ today_tokens: dailyUsed * 12,
1217
+ quota: {
1218
+ total: totalQuota,
1219
+ used: usedQuota,
1220
+ remaining: totalQuota - usedQuota,
1221
+ daily_limit: dailyLimit,
1222
+ daily_used: dailyUsed
1223
+ },
1224
+ code_info: code ? {
1225
+ code: code.code,
1226
+ service: code.service,
1227
+ expires_at: code.expiresAt,
1228
+ status: code.status
1229
+ } : null,
1230
+ daily_stats: dailyStats
1231
+ }
1232
+ };
1233
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1234
+ res.end(JSON.stringify(mockUsageData));
1235
+ return;
1236
+ }
1237
+
1238
+ // GET /user/api/v1/usage/model-trends - Get model usage trends
1239
+ if (apiPath.startsWith("/usage/model-trends") && req.method === "GET") {
1240
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1241
+ res.end(JSON.stringify({
1242
+ success: true,
1243
+ data: {
1244
+ models: [
1245
+ { name: 'claude-opus-4-6', requests: 8234, tokens: 1456789 },
1246
+ { name: 'claude-sonnet-4-6', requests: 5000, tokens: 800000 },
1247
+ { name: 'claude-haiku-4-5', requests: 2000, tokens: 200000 }
1248
+ ]
1249
+ }
1250
+ }));
1251
+ return;
1252
+ }
1253
+
1254
+ // GET /user/api/v1/announcements - Get announcements
1255
+ if (apiPath === "/announcements" && req.method === "GET") {
1256
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1257
+ res.end(JSON.stringify({
1258
+ success: true,
1259
+ data: []
1260
+ }));
1261
+ return;
1262
+ }
1263
+
1264
+ // GET /user/api/v1/channel-groups - Get channel groups
1265
+ if (apiPath === "/channel-groups" && req.method === "GET") {
1266
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1267
+ res.end(JSON.stringify({
1268
+ success: true,
1269
+ data: []
1270
+ }));
1271
+ return;
1272
+ }
1273
+
1274
+ // Default response for unhandled user API endpoints
1275
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1276
+ res.end(JSON.stringify({ success: false, message: 'API endpoint not found' }));
1277
+ return;
1278
+ }
1279
+
1280
+ // Proxy requests to yunyi.cfd API for user frontend
1281
+ if (urlPath.startsWith("/proxy/")) {
1282
+ const targetPath = urlPath.replace("/proxy", "");
1283
+ const targetUrl = `https://yunyi.cfd${targetPath}`;
1284
+
1285
+ const options = {
1286
+ method: req.method,
1287
+ headers: req.headers
1288
+ };
1289
+
1290
+ const proxyReq = https.request(targetUrl, options, (proxyRes) => {
1291
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
1292
+ proxyRes.pipe(res);
1293
+ });
1294
+
1295
+ proxyReq.on('error', (err) => {
1296
+ console.error('Proxy error:', err);
1297
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1298
+ res.end(JSON.stringify({ success: false, message: 'Proxy error' }));
1299
+ });
1300
+
1301
+ req.pipe(proxyReq);
1302
+ return;
1303
+ }
1304
+
1305
+ // Handle root path
1306
+ if (urlPath === "/") {
1307
+ urlPath = "/index.html";
1308
+ }
1309
+
1310
+ // Handle /user or /user/ path - serve user dashboard
1311
+ if (urlPath === "/user" || urlPath === "/user/") {
1312
+ urlPath = "/user/index.html";
1313
+ }
1314
+
1315
+ // Handle /admin or /admin/ path - serve admin dashboard
1316
+ if (urlPath === "/admin" || urlPath === "/admin/") {
1317
+ urlPath = "/admin/index.html";
1318
+ }
1319
+
1320
+ // Handle /activate or /activate/ path - serve activation page
1321
+ if (urlPath === "/activate" || urlPath === "/activate/") {
1322
+ urlPath = "/activate.html";
1323
+ }
1324
+
1325
+ // Construct file path
1326
+ let filePath = path.join(FRONTEND_DIR, urlPath);
1327
+
1328
+ // Security check: prevent directory traversal
1329
+ if (!filePath.startsWith(FRONTEND_DIR)) {
1330
+ res.writeHead(403, { "Content-Type": "text/plain" });
1331
+ res.end("Forbidden");
1332
+ return;
1333
+ }
1334
+
1335
+ // Serve the file
1336
+ serveStaticFile(filePath, res);
1337
+ });
1338
+
1339
+ server.on("error", (error) => {
1340
+ if (error.code === "EADDRINUSE") {
1341
+ console.error(`Port ${PORT} is already in use.`);
1342
+ console.error(`Try: PORT=34021 fogact web`);
1343
+ process.exit(1);
1344
+ }
1345
+
1346
+ console.error(error.message || String(error));
1347
+ process.exit(1);
1348
+ });
1349
+
1350
+ server.listen(PORT, '0.0.0.0', () => {
1351
+ const addresses = getNetworkAddresses();
1352
+
1353
+ console.log("");
1354
+ console.log("╔══════════════════════════════════════════════════════════════╗");
1355
+ console.log("║ ║");
1356
+ console.log("║ FogAct Web UI ║");
1357
+ console.log("║ ║");
1358
+ console.log("╚══════════════════════════════════════════════════════════════╝");
1359
+ console.log("");
1360
+ console.log("Server running on port", PORT);
1361
+ console.log("");
1362
+ console.log("Access URLs:");
1363
+ console.log("─────────────────────────────────────────────────────────────");
1364
+ console.log(` Local: http://localhost:${PORT}/`);
1365
+ console.log(` Local: http://127.0.0.1:${PORT}/`);
1366
+
1367
+ if (addresses.length > 0) {
1368
+ addresses.forEach(addr => {
1369
+ console.log(` Network: http://${addr}:${PORT}/`);
1370
+ });
1371
+ }
1372
+
1373
+ console.log("─────────────────────────────────────────────────────────────");
1374
+ console.log("");
1375
+ console.log("Press Ctrl+C to stop");
1376
+ console.log("");
1377
+
1378
+ // 时区感知的每日刷新定时任务 - 每天凌晨0点(服务器时区)执行
1379
+ function getServerDate() {
1380
+ return new Date().toLocaleDateString("en-CA", { timeZone: SERVER_TIMEZONE });
1381
+ }
1382
+
1383
+ function getNextRefreshDelay() {
1384
+ const now = new Date();
1385
+ const serverTzDate = new Date(now.toLocaleString("en-US", { timeZone: SERVER_TIMEZONE }));
1386
+ const midnight = new Date(serverTzDate);
1387
+ midnight.setHours(24, 0, 0, 0);
1388
+ const diff = midnight.getTime() - serverTzDate.getTime();
1389
+ return Math.max(diff, 1000);
1390
+ }
1391
+
1392
+ function runDailyQuotaRefresh() {
1393
+ const codes = codeDb.getAll();
1394
+ const serverDate = getServerDate();
1395
+ let refreshedCount = 0;
1396
+
1397
+ for (const code of codes) {
1398
+ if (code.status !== 'used') continue;
1399
+
1400
+ const lastRefresh = code.lastQuotaRefresh ? new Date(code.lastQuotaRefresh) : null;
1401
+ const lastRefreshDate = lastRefresh
1402
+ ? new Date(lastRefresh.toLocaleString("en-CA", { timeZone: SERVER_TIMEZONE }))
1403
+ : null;
1404
+ if (lastRefreshDate && lastRefreshDate.toISOString().split('T')[0] === serverDate) continue;
1405
+
1406
+ const dailyQuota = code.quota?.daily || 100000;
1407
+ codeDb.update(code.id, {
1408
+ quota: {
1409
+ ...code.quota,
1410
+ used: 0,
1411
+ daily: dailyQuota
1412
+ },
1413
+ lastQuotaRefresh: new Date().toISOString()
1414
+ });
1415
+ refreshedCount++;
1416
+ }
1417
+
1418
+ if (refreshedCount > 0) {
1419
+ console.log(`[${new Date().toISOString()}] [时区: ${SERVER_TIMEZONE}] 每日额度刷新完成: ${refreshedCount} 个激活码已刷新`);
1420
+ }
1421
+ }
1422
+
1423
+ function scheduleNextRefresh() {
1424
+ const delay = getNextRefreshDelay();
1425
+ console.log(`[${new Date().toISOString()}] 下次额度刷新时间: ${new Date(Date.now() + delay).toLocaleString("zh-CN", { timeZone: SERVER_TIMEZONE })} (${SERVER_TIMEZONE})`);
1426
+ setTimeout(() => {
1427
+ runDailyQuotaRefresh();
1428
+ // 之后每24小时执行一次
1429
+ setInterval(runDailyQuotaRefresh, 24 * 60 * 60 * 1000);
1430
+ }, delay);
1431
+ }
1432
+
1433
+ scheduleNextRefresh();
1434
+ });