fogact 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/web-server.js CHANGED
@@ -36,18 +36,41 @@ const USER_STATUS_KEY_MAP = {
36
36
  const CODE_STATUS_MAP = {
37
37
  unused: "unused",
38
38
  "未使用": "unused",
39
- used: "used",
40
- "已使用": "used",
39
+ "未激活": "unused",
40
+ active: "active",
41
+ "活跃": "active",
42
+ "已激活": "active",
43
+ used: "active",
44
+ "已使用": "active",
45
+ disabled: "disabled",
46
+ blocked: "disabled",
47
+ banned: "disabled",
48
+ "已禁用": "disabled",
49
+ "禁用": "disabled",
41
50
  expired: "expired",
42
51
  "已过期": "expired",
43
52
  };
44
53
 
45
54
  const CODE_STATUS_LABEL_MAP = {
46
- unused: "未使用",
47
- used: "已使用",
55
+ unused: "未激活",
56
+ active: "活跃",
57
+ disabled: "已禁用",
48
58
  expired: "已过期",
49
59
  };
50
60
 
61
+ const HOP_BY_HOP_HEADERS = new Set([
62
+ "connection",
63
+ "keep-alive",
64
+ "proxy-authenticate",
65
+ "proxy-authorization",
66
+ "te",
67
+ "trailer",
68
+ "transfer-encoding",
69
+ "upgrade",
70
+ "host",
71
+ "content-length",
72
+ ]);
73
+
51
74
  // 初始化示例数据
52
75
  initializeSampleData();
53
76
 
@@ -181,7 +204,7 @@ function normalizeCodeStatus(codeOrStatus, expiresAt) {
181
204
  }
182
205
 
183
206
  function getCodeStatusLabel(status) {
184
- return CODE_STATUS_LABEL_MAP[normalizeCodeStatus(status)] || "未使用";
207
+ return CODE_STATUS_LABEL_MAP[normalizeCodeStatus(status)] || "未激活";
185
208
  }
186
209
 
187
210
  function serializeUser(user) {
@@ -219,30 +242,27 @@ function getActivationPlatforms(serviceKey) {
219
242
  return [];
220
243
  }
221
244
 
222
- function buildActivationData(serializedCode) {
245
+ function getPublicBaseUrl(req) {
246
+ const forwardedHost = String(req?.headers?.['x-forwarded-host'] || req?.headers?.host || '').split(',')[0].trim();
247
+ const publicHost = forwardedHost && !forwardedHost.includes('fogact.fogact.com')
248
+ ? forwardedHost
249
+ : 'cliproxy.fogidc.com';
250
+ const isLocalHost = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[?::1\]?)(:\d+)?$/i.test(publicHost);
251
+ const defaultProtocol = isLocalHost ? 'http' : 'https';
252
+ const publicProtocol = String(req?.headers?.['x-forwarded-proto'] || defaultProtocol).split(',')[0].trim() || defaultProtocol;
253
+ return trimTrailingSlash(process.env.FOGACT_PUBLIC_BASE_URL || `${publicProtocol}://${publicHost}`);
254
+ }
255
+
256
+ function getProxyBaseUrl(req, serviceKey) {
257
+ const publicBaseUrl = getPublicBaseUrl(req);
258
+ return serviceKey === "claude" ? publicBaseUrl : `${publicBaseUrl}/v1`;
259
+ }
260
+
261
+ function buildActivationData(serializedCode, req) {
223
262
  const serviceKey = serializedCode.serviceKey;
224
- const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
225
- const serviceConfig = serializedCode.serviceConfig || {};
226
- const baseUrl = trimTrailingSlash(
227
- serializedCode.baseUrl ||
228
- serializedCode.baseURL ||
229
- serializedCode.url ||
230
- serviceConfig.baseUrl ||
231
- serviceConfig.baseURL ||
232
- serviceConfig.url ||
233
- getServiceBaseUrl(upstream, serviceKey) ||
234
- upstream.baseUrl
235
- );
236
- const apiKey = String(
237
- serializedCode.apiKey ||
238
- serializedCode.key ||
239
- serializedCode.token ||
240
- serviceConfig.apiKey ||
241
- serviceConfig.key ||
242
- serviceConfig.token ||
243
- upstream.apiKey ||
244
- ""
245
- ).trim();
263
+ const publicBaseUrl = getPublicBaseUrl(req);
264
+ const baseUrl = getProxyBaseUrl(req, serviceKey);
265
+ const apiKey = String(serializedCode.code || "").trim();
246
266
 
247
267
  return {
248
268
  code: serializedCode.code,
@@ -253,13 +273,17 @@ function buildActivationData(serializedCode) {
253
273
  allowedModels: serializedCode.allowedModels,
254
274
  quota: serializedCode.quota,
255
275
  expiresAt: serializedCode.expiresAt,
276
+ proxy: true,
277
+ publicBaseUrl,
256
278
  baseUrl,
257
279
  apiKey,
258
280
  };
259
281
  }
260
282
 
261
- function ensureActivationDataReady(res, activationData) {
262
- if (activationData.baseUrl && activationData.apiKey) {
283
+ function ensureProxyReady(res, serviceKey) {
284
+ const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
285
+ const upstreamUrl = getServiceBaseUrl(upstream, serviceKey) || upstream.baseUrl;
286
+ if (upstreamUrl && upstream.apiKey) {
263
287
  return true;
264
288
  }
265
289
 
@@ -272,6 +296,125 @@ function ensureActivationDataReady(res, activationData) {
272
296
  return false;
273
297
  }
274
298
 
299
+ function getBearerToken(req) {
300
+ const auth = String(req.headers.authorization || "");
301
+ const match = auth.match(/^Bearer\s+(.+)$/i);
302
+ if (match) return match[1].trim();
303
+ const apiKey = req.headers["x-api-key"] || req.headers["api-key"];
304
+ return apiKey ? String(apiKey).trim() : "";
305
+ }
306
+
307
+ function getProxyCode(token, body) {
308
+ return String(
309
+ token ||
310
+ body?.fogact_code ||
311
+ body?.activation_code ||
312
+ ""
313
+ ).trim();
314
+ }
315
+
316
+ function getActiveProxyCode(codeValue, serviceKey) {
317
+ const code = codeDb.getByCode(String(codeValue || "").trim());
318
+ if (!code) return { ok: false, status: 401, message: "激活码不存在或无效" };
319
+
320
+ const serializedCode = serializeCode(code);
321
+ if (!serviceMatches(serializedCode, serviceKey)) {
322
+ return { ok: false, status: 403, message: `此激活码不支持 ${normalizeService(serviceKey)}` };
323
+ }
324
+ if (serializedCode.status === "disabled") {
325
+ return { ok: false, status: 403, message: "此激活码已被禁用,无法访问中转" };
326
+ }
327
+ if (serializedCode.status === "expired") {
328
+ return { ok: false, status: 403, message: "激活码已过期" };
329
+ }
330
+
331
+ return { ok: true, code, serializedCode };
332
+ }
333
+
334
+ function buildProxyHeaders(req, upstreamApiKey, bodyLength) {
335
+ const headers = {};
336
+ for (const [name, value] of Object.entries(req.headers)) {
337
+ const key = name.toLowerCase();
338
+ if (HOP_BY_HOP_HEADERS.has(key)) continue;
339
+ if (key === "authorization" || key === "x-api-key" || key === "api-key") continue;
340
+ headers[name] = value;
341
+ }
342
+ headers.authorization = `Bearer ${upstreamApiKey}`;
343
+ if (bodyLength !== undefined) headers["content-length"] = Buffer.byteLength(bodyLength);
344
+ return headers;
345
+ }
346
+
347
+ function sanitizeProxyBody(req, rawBody) {
348
+ if (!rawBody || !String(req.headers["content-type"] || "").includes("application/json")) {
349
+ return rawBody;
350
+ }
351
+
352
+ try {
353
+ const body = JSON.parse(rawBody);
354
+ if (body && typeof body === "object" && !Array.isArray(body)) {
355
+ delete body.fogact_code;
356
+ delete body.activation_code;
357
+ delete body.api_key;
358
+ return JSON.stringify(body);
359
+ }
360
+ } catch (_error) {
361
+ return rawBody;
362
+ }
363
+
364
+ return rawBody;
365
+ }
366
+
367
+ function proxyUpstreamRequest(req, res, serviceKey, code, rawBody) {
368
+ const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
369
+ const upstreamUrl = getServiceBaseUrl(upstream, serviceKey) || upstream.baseUrl;
370
+ const upstreamApiKey = upstream.apiKey;
371
+ if (!upstreamUrl || !upstreamApiKey) {
372
+ res.writeHead(500, { "Content-Type": "application/json" });
373
+ res.end(JSON.stringify({ success: false, error: { message: "上游服务未配置完整" } }));
374
+ return;
375
+ }
376
+
377
+ const upstreamBase = trimTrailingSlash(upstreamUrl);
378
+ const requestPath = req.url.split("?")[0];
379
+ const query = req.url.includes("?") ? `?${req.url.split("?").slice(1).join("?")}` : "";
380
+ const upstreamPath = serviceKey === "claude"
381
+ ? requestPath
382
+ : requestPath.replace(/^\/v1(?=\/|$)/, "") || "/";
383
+ const target = new URL(`${upstreamBase}${upstreamPath}${query}`);
384
+ const client = target.protocol === "https:" ? https : http;
385
+ const upstreamBody = sanitizeProxyBody(req, rawBody);
386
+ const headers = buildProxyHeaders(req, upstreamApiKey, upstreamBody);
387
+
388
+ const proxyReq = client.request({
389
+ protocol: target.protocol,
390
+ hostname: target.hostname,
391
+ port: target.port || (target.protocol === "https:" ? 443 : 80),
392
+ path: `${target.pathname}${target.search}`,
393
+ method: req.method,
394
+ headers,
395
+ }, (proxyRes) => {
396
+ const responseHeaders = { ...proxyRes.headers };
397
+ delete responseHeaders["transfer-encoding"];
398
+ delete responseHeaders.connection;
399
+ res.writeHead(proxyRes.statusCode || 502, responseHeaders);
400
+ proxyRes.pipe(res);
401
+ });
402
+
403
+ proxyReq.on("error", (error) => {
404
+ res.writeHead(502, { "Content-Type": "application/json" });
405
+ res.end(JSON.stringify({ success: false, error: { message: error.message } }));
406
+ });
407
+
408
+ if (upstreamBody) proxyReq.write(upstreamBody);
409
+ proxyReq.end();
410
+
411
+ codeDb.update(code.id, {
412
+ status: "active",
413
+ usedBy: code.usedBy || "proxy-user",
414
+ lastUsedAt: new Date().toISOString(),
415
+ });
416
+ }
417
+
275
418
  function trimTrailingSlash(value) {
276
419
  return String(value || "").trim().replace(/\/+$/, "");
277
420
  }
@@ -392,10 +535,11 @@ function serveStaticFile(filePath, res) {
392
535
 
393
536
  // For admin panel files, always use no-cache to prevent stale content
394
537
  const isAdminFile = filePath.includes('admin-panel');
538
+ const isUserAsset = filePath.includes(`${path.sep}user${path.sep}assets${path.sep}`);
395
539
 
396
540
  res.writeHead(200, {
397
541
  "Content-Type": contentType,
398
- "Cache-Control": (ext === '.html' || isAdminFile)
542
+ "Cache-Control": (ext === '.html' || isAdminFile || isUserAsset)
399
543
  ? 'no-cache, no-store, must-revalidate'
400
544
  : 'public, max-age=31536000',
401
545
  "Pragma": "no-cache",
@@ -409,7 +553,7 @@ const server = http.createServer((req, res) => {
409
553
  // Add CORS headers for external access
410
554
  res.setHeader('Access-Control-Allow-Origin', '*');
411
555
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
412
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
556
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, API-Key');
413
557
 
414
558
  if (req.method === 'OPTIONS') {
415
559
  res.writeHead(200);
@@ -420,6 +564,42 @@ const server = http.createServer((req, res) => {
420
564
  // Parse URL and remove query string
421
565
  let urlPath = req.url.split('?')[0];
422
566
 
567
+ const isCodexProxyPath = urlPath === "/v1" || urlPath.startsWith("/v1/");
568
+ const isClaudeProxyPath = urlPath.startsWith("/anthropic/") || urlPath === "/v1/messages" || urlPath.startsWith("/v1/messages/");
569
+ if (req.method !== "OPTIONS" && (isCodexProxyPath || isClaudeProxyPath)) {
570
+ const serviceKey = isClaudeProxyPath ? "claude" : "codex";
571
+ let rawBody = "";
572
+ req.on("data", (chunk) => {
573
+ rawBody += chunk.toString();
574
+ });
575
+ req.on("end", () => {
576
+ let body = null;
577
+ if (rawBody && String(req.headers["content-type"] || "").includes("application/json")) {
578
+ try {
579
+ body = JSON.parse(rawBody);
580
+ } catch (_error) {
581
+ body = null;
582
+ }
583
+ }
584
+
585
+ const token = getBearerToken(req);
586
+ const codeValue = getProxyCode(token, body);
587
+ const codeCheck = getActiveProxyCode(codeValue, serviceKey);
588
+ if (!codeCheck.ok) {
589
+ res.writeHead(codeCheck.status, { "Content-Type": "application/json" });
590
+ res.end(JSON.stringify({ error: { message: codeCheck.message } }));
591
+ return;
592
+ }
593
+
594
+ proxyUpstreamRequest(req, res, serviceKey, codeCheck.code, rawBody);
595
+ });
596
+ req.on("error", () => {
597
+ res.writeHead(400, { "Content-Type": "application/json" });
598
+ res.end(JSON.stringify({ error: { message: "请求读取失败" } }));
599
+ });
600
+ return;
601
+ }
602
+
423
603
  // Handle login API
424
604
  if (urlPath === "/api/login" && req.method === "POST") {
425
605
  let body = '';
@@ -592,17 +772,10 @@ const server = http.createServer((req, res) => {
592
772
  }
593
773
 
594
774
  if (urlPath === "/api/nodes" && req.method === "GET") {
595
- const url = new URL(req.url, `http://${req.headers.host}`);
596
- const service = getServiceKey(url.searchParams.get("service") || "codex", "codex");
597
- const upstream = loadUpstreamConfig({ configPath: getUpstreamConfigPath() });
598
- const upstreamUrl = getServiceBaseUrl(upstream, service) || upstream.baseUrl;
599
- const publicUrl = `https://${req.headers.host}`.replace(/\/+$/, "");
775
+ const publicUrl = getPublicBaseUrl(req);
600
776
  const nodes = [
601
777
  { name: "FogAct", url: publicUrl, region: "Global" },
602
778
  ];
603
- if (upstreamUrl) {
604
- nodes.push({ name: service === "claude" ? "Claude Upstream" : "Codex Upstream", url: upstreamUrl, region: "Upstream" });
605
- }
606
779
  res.writeHead(200, { 'Content-Type': 'application/json' });
607
780
  res.end(JSON.stringify({ success: true, nodes }));
608
781
  return;
@@ -933,10 +1106,15 @@ const server = http.createServer((req, res) => {
933
1106
  return;
934
1107
  }
935
1108
 
936
- // 检查激活码状态
937
- if (normalizeCodeStatus(code) === 'used') {
1109
+ const statusKey = normalizeCodeStatus(code);
1110
+ if (statusKey === 'disabled') {
938
1111
  res.writeHead(400, { 'Content-Type': 'application/json' });
939
- res.end(JSON.stringify({ success: false, message: '激活码已被使用' }));
1112
+ res.end(JSON.stringify({ success: false, message: '此激活码已被禁用,无法激活配置' }));
1113
+ return;
1114
+ }
1115
+ if (statusKey === 'expired') {
1116
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1117
+ res.end(JSON.stringify({ success: false, message: '激活码已过期' }));
940
1118
  return;
941
1119
  }
942
1120
 
@@ -947,15 +1125,15 @@ const server = http.createServer((req, res) => {
947
1125
  return;
948
1126
  }
949
1127
 
950
- const activationData = buildActivationData(serializedCode);
951
- if (!ensureActivationDataReady(res, activationData)) {
1128
+ const activationData = buildActivationData(serializedCode, req);
1129
+ if (!ensureProxyReady(res, serializedCode.serviceKey)) {
952
1130
  return;
953
1131
  }
954
1132
 
955
- // 更新激活码状态
1133
+ // 记录最近一次配置时间;不消费激活码,允许同一个 Key 反复自动配置。
956
1134
  const updatedCode = codeDb.update(code.id, {
957
- status: 'used',
958
- usedBy: userId || username || email || 'unknown',
1135
+ status: 'active',
1136
+ usedBy: userId || username || email || code.usedBy || 'unknown',
959
1137
  lastUsedAt: new Date().toISOString(),
960
1138
  activatedService: serializedCode.service,
961
1139
  activatedServiceKey: serializedCode.serviceKey
@@ -973,7 +1151,7 @@ const server = http.createServer((req, res) => {
973
1151
  success: true,
974
1152
  message: '激活成功',
975
1153
  data: {
976
- ...buildActivationData(serializeCode(updatedCode)),
1154
+ ...buildActivationData(serializeCode(updatedCode), req),
977
1155
  activatedAt: updatedCode.lastUsedAt
978
1156
  }
979
1157
  }));
@@ -1069,9 +1247,9 @@ const server = http.createServer((req, res) => {
1069
1247
  const totalUsers = users.length;
1070
1248
  const activeUsers = users.filter(u => u.statusKey === 'active').length;
1071
1249
  const totalCodes = codes.length;
1072
- const usedCodes = codes.filter(c => c.status === 'used').length;
1073
- const unusedCodes = codes.filter(c => c.status === 'unused').length;
1074
- const expiredCodes = codes.filter(c => c.status === 'expired').length;
1250
+ const usedCodes = codes.filter(c => normalizeCodeStatus(c) === 'active').length;
1251
+ const unusedCodes = codes.filter(c => normalizeCodeStatus(c) === 'unused').length;
1252
+ const expiredCodes = codes.filter(c => normalizeCodeStatus(c) === 'expired').length;
1075
1253
 
1076
1254
  const stats = {
1077
1255
  totalUsers,
@@ -1171,14 +1349,19 @@ const server = http.createServer((req, res) => {
1171
1349
  }
1172
1350
 
1173
1351
  const serializedCode = serializeCode(code);
1174
- if (serializedCode.status !== 'unused') {
1352
+ if (serializedCode.status === 'expired') {
1175
1353
  res.writeHead(200, { 'Content-Type': 'application/json' });
1176
- res.end(JSON.stringify({ success: false, valid: false, message: serializedCode.status === 'expired' ? '激活码已过期' : '激活码已被使用' }));
1354
+ res.end(JSON.stringify({ success: false, valid: false, message: '激活码已过期' }));
1355
+ return;
1356
+ }
1357
+ if (serializedCode.status === 'disabled') {
1358
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1359
+ res.end(JSON.stringify({ success: false, valid: false, message: '此激活码已被禁用,无法激活配置' }));
1177
1360
  return;
1178
1361
  }
1179
1362
 
1180
- const activationData = buildActivationData(serializedCode);
1181
- if (!ensureActivationDataReady(res, activationData)) {
1363
+ const activationData = buildActivationData(serializedCode, req);
1364
+ if (!ensureProxyReady(res, serializedCode.serviceKey)) {
1182
1365
  return;
1183
1366
  }
1184
1367
 
@@ -1192,26 +1375,11 @@ const server = http.createServer((req, res) => {
1192
1375
  return;
1193
1376
  }
1194
1377
 
1195
- if (data.api_key && data.api_key.startsWith('sk-test-')) {
1196
- res.writeHead(200, { 'Content-Type': 'application/json' });
1197
- res.end(JSON.stringify({
1198
- success: true,
1199
- valid: true,
1200
- message: '验证成功',
1201
- data: {
1202
- username: 'test_user',
1203
- email: 'test@example.com',
1204
- service: 'Claude Code'
1205
- }
1206
- }));
1207
- return;
1208
- }
1209
-
1210
1378
  res.writeHead(200, { 'Content-Type': 'application/json' });
1211
1379
  res.end(JSON.stringify({
1212
1380
  success: false,
1213
1381
  valid: false,
1214
- message: '验证失败,请检查 API Key 是否正确'
1382
+ message: '请使用 FogAct 激活码验证'
1215
1383
  }));
1216
1384
  }).catch(() => {
1217
1385
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -1261,7 +1429,7 @@ const server = http.createServer((req, res) => {
1261
1429
  const codes = codeDb.getAll().filter(c =>
1262
1430
  c.usedBy === username ||
1263
1431
  c.usedBy === user?.username ||
1264
- c.status === 'used'
1432
+ normalizeCodeStatus(c) === 'active'
1265
1433
  );
1266
1434
  const code = codes[0];
1267
1435
 
@@ -1350,42 +1518,53 @@ const server = http.createServer((req, res) => {
1350
1518
  return;
1351
1519
  }
1352
1520
 
1353
- // Default response for unhandled user API endpoints
1354
- res.writeHead(404, { 'Content-Type': 'application/json' });
1355
- res.end(JSON.stringify({ success: false, message: 'API endpoint not found' }));
1356
- return;
1357
- }
1358
1521
 
1359
- // Proxy requests to configured upstream API for user frontend
1360
- if (urlPath.startsWith("/proxy/")) {
1361
- const targetPath = urlPath.replace("/proxy", "");
1362
- const proxyBaseUrl = trimTrailingSlash(process.env.FOGACT_PROXY_TARGET || readRawUpstreamConfig().baseUrl || "");
1363
-
1364
- if (!proxyBaseUrl) {
1365
- res.writeHead(502, { "Content-Type": "application/json" });
1366
- res.end(JSON.stringify({ success: false, message: "Proxy target is not configured" }));
1522
+ // POST /user/api/v1/batch-info - Validate multiple saved keys
1523
+ if (apiPath === "/batch-info" && req.method === "POST") {
1524
+ parseRequestBody(req).then((data) => {
1525
+ const keys = Array.isArray(data.keys) ? data.keys : [];
1526
+ const now = new Date().toISOString();
1527
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1528
+ res.end(JSON.stringify({
1529
+ success: true,
1530
+ results: keys.map((key, index) => ({
1531
+ id: `fogact-key-${index + 1}`,
1532
+ key_preview: String(key || '').slice(0, 8) || `fogact-${index + 1}`,
1533
+ service_type: String(key || '').toLowerCase().includes('codex') ? 'codex' : 'claude',
1534
+ status: 'active',
1535
+ quota: { total: 100000, used: 0, remaining: 100000, unit: 'tokens' },
1536
+ timestamps: { activated_at: now, last_used_at: null, expires_at: null },
1537
+ })),
1538
+ }));
1539
+ }).catch(() => {
1540
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1541
+ res.end(JSON.stringify({ success: false, message: '请求格式错误' }));
1542
+ });
1367
1543
  return;
1368
1544
  }
1369
1545
 
1370
- const targetUrl = `${proxyBaseUrl}${targetPath}`;
1371
-
1372
- const options = {
1373
- method: req.method,
1374
- headers: req.headers
1375
- };
1546
+ // Lightweight fallbacks for optional user center actions.
1547
+ if (apiPath === "/card-bind/info" && req.method === "GET") {
1548
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1549
+ res.end(JSON.stringify({ success: true, data: { parent: null, children: [] } }));
1550
+ return;
1551
+ }
1376
1552
 
1377
- const proxyReq = https.request(targetUrl, options, (proxyRes) => {
1378
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
1379
- proxyRes.pipe(res);
1380
- });
1553
+ if (["/card-bind/bind", "/card-bind/unbind", "/card-bind/unbind-self", "/card-bind/reorder", "/renew/preview", "/renew/execute", "/quota-pack/redeem-preview", "/quota-pack/redeem"].includes(apiPath)) {
1554
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1555
+ res.end(JSON.stringify({ success: true, data: null, message: '操作已提交' }));
1556
+ return;
1557
+ }
1381
1558
 
1382
- proxyReq.on('error', (err) => {
1383
- console.error('Proxy error:', err);
1384
- res.writeHead(500, { 'Content-Type': 'application/json' });
1385
- res.end(JSON.stringify({ success: false, message: 'Proxy error' }));
1386
- });
1559
+ if (apiPath === "/channel-group" && req.method === "PUT") {
1560
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1561
+ res.end(JSON.stringify({ success: true }));
1562
+ return;
1563
+ }
1387
1564
 
1388
- req.pipe(proxyReq);
1565
+ // Default response for unhandled user API endpoints
1566
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1567
+ res.end(JSON.stringify({ success: false, message: '接口不存在' }));
1389
1568
  return;
1390
1569
  }
1391
1570
 
@@ -1394,8 +1573,12 @@ const server = http.createServer((req, res) => {
1394
1573
  urlPath = "/index.html";
1395
1574
  }
1396
1575
 
1397
- // Handle /user or /user/ path - serve user dashboard
1398
- if (urlPath === "/user" || urlPath === "/user/") {
1576
+ // Handle user frontend routes - serve SPA entry for direct navigation.
1577
+ if (
1578
+ urlPath === "/user" ||
1579
+ urlPath === "/user/" ||
1580
+ (urlPath.startsWith("/user/") && !urlPath.startsWith("/user/assets/"))
1581
+ ) {
1399
1582
  urlPath = "/user/index.html";
1400
1583
  }
1401
1584
 
@@ -1482,7 +1665,7 @@ server.listen(PORT, '0.0.0.0', () => {
1482
1665
  let refreshedCount = 0;
1483
1666
 
1484
1667
  for (const code of codes) {
1485
- if (code.status !== 'used') continue;
1668
+ if (normalizeCodeStatus(code) !== 'active') continue;
1486
1669
 
1487
1670
  const lastRefresh = code.lastQuotaRefresh ? new Date(code.lastQuotaRefresh) : null;
1488
1671
  const lastRefreshDate = lastRefresh
@@ -170,8 +170,10 @@ function getUserStatusMeta(user) {
170
170
  function getCodeStatusMeta(code) {
171
171
  const key = code.status || 'unused';
172
172
  const meta = {
173
- unused: { label: code.statusLabel || '未使用', className: 'bg-green-50 text-green-700' },
174
- used: { label: code.statusLabel || '已使用', className: 'bg-surface-container text-on-surface-variant' },
173
+ unused: { label: code.statusLabel || '未激活', className: 'bg-amber-50 text-amber-700' },
174
+ active: { label: code.statusLabel || '活跃', className: 'bg-green-50 text-green-700' },
175
+ used: { label: code.statusLabel || '活跃', className: 'bg-green-50 text-green-700' },
176
+ disabled: { label: code.statusLabel || '已禁用', className: 'bg-error-container text-on-error-container' },
175
177
  expired: { label: code.statusLabel || '已过期', className: 'bg-error-container text-on-error-container' }
176
178
  };
177
179
  return meta[key] || meta.unused;
@@ -471,7 +473,7 @@ const Dashboard = {
471
473
  const cards = [
472
474
  { label: '总用户数', value: stats.totalUsers || 0, icon: 'group', color: 'primary', sub: '全部用户' },
473
475
  { label: '活跃用户', value: stats.activeUsers || 0, icon: 'trending_up', color: 'tertiary', sub: '当前活跃状态' },
474
- { label: '总激活码', value: stats.totalCodes || 0, icon: 'key', color: 'secondary', sub: `未使用 ${stats.unusedCodes || 0}` },
476
+ { label: '总激活码', value: stats.totalCodes || 0, icon: 'key', color: 'secondary', sub: `未激活 ${stats.unusedCodes || 0}` },
475
477
  { label: '已过期', value: stats.expiredCodes || 0, icon: 'schedule', color: 'error', sub: stats.systemStatus || '运行正常' }
476
478
  ];
477
479
 
@@ -1102,8 +1104,9 @@ const CodeManagement = {
1102
1104
  <div>
1103
1105
  <label class="block text-sm font-medium text-on-surface mb-2">状态</label>
1104
1106
  <select id="edit-code-status" class="w-full px-4 py-2 bg-surface-container-low border-none rounded-xl text-sm outline-none">
1105
- <option value="unused" ${code.status === 'unused' ? 'selected' : ''}>未使用</option>
1106
- <option value="used" ${code.status === 'used' ? 'selected' : ''}>已使用</option>
1107
+ <option value="unused" ${code.status === 'unused' ? 'selected' : ''}>未激活</option>
1108
+ <option value="active" ${code.status === 'active' || code.status === 'used' ? 'selected' : ''}>活跃</option>
1109
+ <option value="disabled" ${code.status === 'disabled' ? 'selected' : ''}>已禁用</option>
1107
1110
  <option value="expired" ${code.status === 'expired' ? 'selected' : ''}>已过期</option>
1108
1111
  </select>
1109
1112
  </div>