fogact 1.1.10 → 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/README.md CHANGED
@@ -51,7 +51,7 @@ Minimum bootstrap requirement: the machine needs `curl` or `wget`. The installer
51
51
  2. Choose `1. Activate service`.
52
52
  3. Enter the activation / redeem code.
53
53
  4. FogAct auto-detects the Codex / Claude entitlement and shows only supported targets.
54
- 5. Confirm the plan and restart the target tool.
54
+ 5. Confirm activation, let FogAct write the config, then restart the target tool.
55
55
 
56
56
  FogAct backs up existing configuration before writing new files.
57
57
 
package/README.zh-CN.md CHANGED
@@ -51,7 +51,7 @@ fogact
51
51
  2. 选择 `1. 激活服务`。
52
52
  3. 输入激活码 / 兑换码。
53
53
  4. FogAct 自动识别 Codex / Claude 能力,并只展示可激活的平台。
54
- 5. 确认激活计划,然后重启对应工具。
54
+ 5. 确认激活后自动写入配置,然后重启对应工具。
55
55
 
56
56
  FogAct 写入新配置前会自动备份旧配置。
57
57
 
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 = '';
@@ -585,6 +765,22 @@ const server = http.createServer((req, res) => {
585
765
  return;
586
766
  }
587
767
 
768
+ if (urlPath === "/health" && req.method === "GET") {
769
+ res.writeHead(200, { 'Content-Type': 'application/json' });
770
+ res.end(JSON.stringify({ success: true, status: 'ok', service: 'fogact' }));
771
+ return;
772
+ }
773
+
774
+ if (urlPath === "/api/nodes" && req.method === "GET") {
775
+ const publicUrl = getPublicBaseUrl(req);
776
+ const nodes = [
777
+ { name: "FogAct", url: publicUrl, region: "Global" },
778
+ ];
779
+ res.writeHead(200, { 'Content-Type': 'application/json' });
780
+ res.end(JSON.stringify({ success: true, nodes }));
781
+ return;
782
+ }
783
+
588
784
  // Handle users API
589
785
  if (urlPath === "/api/users" && req.method === "GET") {
590
786
  if (!isAuthenticated(req)) {
@@ -910,10 +1106,15 @@ const server = http.createServer((req, res) => {
910
1106
  return;
911
1107
  }
912
1108
 
913
- // 检查激活码状态
914
- if (normalizeCodeStatus(code) === 'used') {
1109
+ const statusKey = normalizeCodeStatus(code);
1110
+ if (statusKey === 'disabled') {
915
1111
  res.writeHead(400, { 'Content-Type': 'application/json' });
916
- 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: '激活码已过期' }));
917
1118
  return;
918
1119
  }
919
1120
 
@@ -924,15 +1125,15 @@ const server = http.createServer((req, res) => {
924
1125
  return;
925
1126
  }
926
1127
 
927
- const activationData = buildActivationData(serializedCode);
928
- if (!ensureActivationDataReady(res, activationData)) {
1128
+ const activationData = buildActivationData(serializedCode, req);
1129
+ if (!ensureProxyReady(res, serializedCode.serviceKey)) {
929
1130
  return;
930
1131
  }
931
1132
 
932
- // 更新激活码状态
1133
+ // 记录最近一次配置时间;不消费激活码,允许同一个 Key 反复自动配置。
933
1134
  const updatedCode = codeDb.update(code.id, {
934
- status: 'used',
935
- usedBy: userId || username || email || 'unknown',
1135
+ status: 'active',
1136
+ usedBy: userId || username || email || code.usedBy || 'unknown',
936
1137
  lastUsedAt: new Date().toISOString(),
937
1138
  activatedService: serializedCode.service,
938
1139
  activatedServiceKey: serializedCode.serviceKey
@@ -950,7 +1151,7 @@ const server = http.createServer((req, res) => {
950
1151
  success: true,
951
1152
  message: '激活成功',
952
1153
  data: {
953
- ...buildActivationData(serializeCode(updatedCode)),
1154
+ ...buildActivationData(serializeCode(updatedCode), req),
954
1155
  activatedAt: updatedCode.lastUsedAt
955
1156
  }
956
1157
  }));
@@ -1046,9 +1247,9 @@ const server = http.createServer((req, res) => {
1046
1247
  const totalUsers = users.length;
1047
1248
  const activeUsers = users.filter(u => u.statusKey === 'active').length;
1048
1249
  const totalCodes = codes.length;
1049
- const usedCodes = codes.filter(c => c.status === 'used').length;
1050
- const unusedCodes = codes.filter(c => c.status === 'unused').length;
1051
- 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;
1052
1253
 
1053
1254
  const stats = {
1054
1255
  totalUsers,
@@ -1148,14 +1349,19 @@ const server = http.createServer((req, res) => {
1148
1349
  }
1149
1350
 
1150
1351
  const serializedCode = serializeCode(code);
1151
- if (serializedCode.status !== 'unused') {
1352
+ if (serializedCode.status === 'expired') {
1152
1353
  res.writeHead(200, { 'Content-Type': 'application/json' });
1153
- 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: '此激活码已被禁用,无法激活配置' }));
1154
1360
  return;
1155
1361
  }
1156
1362
 
1157
- const activationData = buildActivationData(serializedCode);
1158
- if (!ensureActivationDataReady(res, activationData)) {
1363
+ const activationData = buildActivationData(serializedCode, req);
1364
+ if (!ensureProxyReady(res, serializedCode.serviceKey)) {
1159
1365
  return;
1160
1366
  }
1161
1367
 
@@ -1169,26 +1375,11 @@ const server = http.createServer((req, res) => {
1169
1375
  return;
1170
1376
  }
1171
1377
 
1172
- if (data.api_key && data.api_key.startsWith('sk-test-')) {
1173
- res.writeHead(200, { 'Content-Type': 'application/json' });
1174
- res.end(JSON.stringify({
1175
- success: true,
1176
- valid: true,
1177
- message: '验证成功',
1178
- data: {
1179
- username: 'test_user',
1180
- email: 'test@example.com',
1181
- service: 'Claude Code'
1182
- }
1183
- }));
1184
- return;
1185
- }
1186
-
1187
1378
  res.writeHead(200, { 'Content-Type': 'application/json' });
1188
1379
  res.end(JSON.stringify({
1189
1380
  success: false,
1190
1381
  valid: false,
1191
- message: '验证失败,请检查 API Key 是否正确'
1382
+ message: '请使用 FogAct 激活码验证'
1192
1383
  }));
1193
1384
  }).catch(() => {
1194
1385
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -1238,7 +1429,7 @@ const server = http.createServer((req, res) => {
1238
1429
  const codes = codeDb.getAll().filter(c =>
1239
1430
  c.usedBy === username ||
1240
1431
  c.usedBy === user?.username ||
1241
- c.status === 'used'
1432
+ normalizeCodeStatus(c) === 'active'
1242
1433
  );
1243
1434
  const code = codes[0];
1244
1435
 
@@ -1327,42 +1518,53 @@ const server = http.createServer((req, res) => {
1327
1518
  return;
1328
1519
  }
1329
1520
 
1330
- // Default response for unhandled user API endpoints
1331
- res.writeHead(404, { 'Content-Type': 'application/json' });
1332
- res.end(JSON.stringify({ success: false, message: 'API endpoint not found' }));
1333
- return;
1334
- }
1335
-
1336
- // Proxy requests to configured upstream API for user frontend
1337
- if (urlPath.startsWith("/proxy/")) {
1338
- const targetPath = urlPath.replace("/proxy", "");
1339
- const proxyBaseUrl = trimTrailingSlash(process.env.FOGACT_PROXY_TARGET || readRawUpstreamConfig().baseUrl || "");
1340
1521
 
1341
- if (!proxyBaseUrl) {
1342
- res.writeHead(502, { "Content-Type": "application/json" });
1343
- 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
+ });
1344
1543
  return;
1345
1544
  }
1346
1545
 
1347
- const targetUrl = `${proxyBaseUrl}${targetPath}`;
1348
-
1349
- const options = {
1350
- method: req.method,
1351
- headers: req.headers
1352
- };
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
+ }
1353
1552
 
1354
- const proxyReq = https.request(targetUrl, options, (proxyRes) => {
1355
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
1356
- proxyRes.pipe(res);
1357
- });
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
+ }
1358
1558
 
1359
- proxyReq.on('error', (err) => {
1360
- console.error('Proxy error:', err);
1361
- res.writeHead(500, { 'Content-Type': 'application/json' });
1362
- res.end(JSON.stringify({ success: false, message: 'Proxy error' }));
1363
- });
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
+ }
1364
1564
 
1365
- 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: '接口不存在' }));
1366
1568
  return;
1367
1569
  }
1368
1570
 
@@ -1371,8 +1573,12 @@ const server = http.createServer((req, res) => {
1371
1573
  urlPath = "/index.html";
1372
1574
  }
1373
1575
 
1374
- // Handle /user or /user/ path - serve user dashboard
1375
- 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
+ ) {
1376
1582
  urlPath = "/user/index.html";
1377
1583
  }
1378
1584
 
@@ -1459,7 +1665,7 @@ server.listen(PORT, '0.0.0.0', () => {
1459
1665
  let refreshedCount = 0;
1460
1666
 
1461
1667
  for (const code of codes) {
1462
- if (code.status !== 'used') continue;
1668
+ if (normalizeCodeStatus(code) !== 'active') continue;
1463
1669
 
1464
1670
  const lastRefresh = code.lastQuotaRefresh ? new Date(code.lastQuotaRefresh) : null;
1465
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>