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.
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/README.zh-CN.md +244 -0
- package/bin/cli.js +9 -0
- package/bin/web-server.js +1434 -0
- package/config/upstream.example.json +14 -0
- package/frontend/activate.html +249 -0
- package/frontend/admin/admin-panel-v2.js +1899 -0
- package/frontend/admin/index.html +705 -0
- package/frontend/assets/market-ui.css +1876 -0
- package/frontend/color-test.html +136 -0
- package/frontend/index.html +191 -0
- package/frontend/user/assets/AnnouncementDetail-Dvxmwz0A.js +12 -0
- package/frontend/user/assets/Announcements-CS1tF2mx.js +11 -0
- package/frontend/user/assets/CardBind-CsCxihhP.js +21 -0
- package/frontend/user/assets/CardContent.vue_vue_type_script_setup_true_lang-D2L-uqSl.js +1 -0
- package/frontend/user/assets/CardDescription.vue_vue_type_script_setup_true_lang-D-v5Pl7F.js +1 -0
- package/frontend/user/assets/CardTitle.vue_vue_type_script_setup_true_lang-a0CCN6D5.js +1 -0
- package/frontend/user/assets/Dashboard-rPsmltm5.js +51 -0
- package/frontend/user/assets/DashboardLayout-BUCWGlXC.css +1 -0
- package/frontend/user/assets/DashboardLayout-DDkxHYFj.js +80 -0
- package/frontend/user/assets/Input.vue_vue_type_script_setup_true_lang-B0SyPmYb.js +6 -0
- package/frontend/user/assets/Label.vue_vue_type_script_setup_true_lang-CxYORSgN.js +1 -0
- package/frontend/user/assets/Progress.vue_vue_type_script_setup_true_lang-2_QbPsEQ.js +1 -0
- package/frontend/user/assets/QuotaPack-B_tJ7Psm.js +6 -0
- package/frontend/user/assets/Renewal-BSDhDmwv.js +6 -0
- package/frontend/user/assets/ScrollArea.vue_vue_type_script_setup_true_lang-DMYwcfpz.js +1 -0
- package/frontend/user/assets/Separator.vue_vue_type_script_setup_true_lang-Ckg8EXj_.js +1 -0
- package/frontend/user/assets/Settings-CBdAa3lw.js +11 -0
- package/frontend/user/assets/TooltipTrigger.vue_vue_type_script_setup_true_lang-DtSBjzGo.js +16 -0
- package/frontend/user/assets/Welcome-7IfzEli4.css +1 -0
- package/frontend/user/assets/Welcome-Dtfp6oER.js +1 -0
- package/frontend/user/assets/_plugin-vue_export-helper-5cjT4u0R.js +16 -0
- package/frontend/user/assets/activity-wYWtyqTJ.js +6 -0
- package/frontend/user/assets/announcement-35mOnjRL.js +16 -0
- package/frontend/user/assets/calendar-BFNuCata.js +6 -0
- package/frontend/user/assets/chart-vendor-CULJE59K.js +37 -0
- package/frontend/user/assets/chevron-down-kDbuU1Py.js +6 -0
- package/frontend/user/assets/chevron-right-BayASIm0.js +6 -0
- package/frontend/user/assets/eye-CY62vip0.js +6 -0
- package/frontend/user/assets/gauge-C5NQ-mV8.js +6 -0
- package/frontend/user/assets/index-B8QSyYhS.css +1 -0
- package/frontend/user/assets/index-Da98HOxL.js +91 -0
- package/frontend/user/assets/link-2-DT5R5nGO.js +6 -0
- package/frontend/user/assets/package-rUbExUEn.js +6 -0
- package/frontend/user/assets/plus-CQc6C8wG.js +11 -0
- package/frontend/user/assets/refresh-cw-Y9hCloPL.js +6 -0
- package/frontend/user/assets/useUserPageRefresh-BYZvpNR9.js +1 -0
- package/frontend/user/assets/zap-l5zbZqrM.js +11 -0
- package/frontend/user/index.html +67 -0
- package/install.sh +402 -0
- package/lib/commands/activate.js +144 -0
- package/lib/commands/restore.js +102 -0
- package/lib/commands/test.js +40 -0
- package/lib/config/claude.js +81 -0
- package/lib/config/codex.js +164 -0
- package/lib/config/upstream.js +79 -0
- package/lib/index.js +164 -0
- package/lib/platforms/claude-code.js +35 -0
- package/lib/platforms/codex-cli.js +35 -0
- package/lib/platforms/editor-codex.js +138 -0
- package/lib/platforms/index.js +32 -0
- package/lib/platforms/openclaw.js +118 -0
- package/lib/platforms/opencode.js +89 -0
- package/lib/services/activation-orchestrator.js +666 -0
- package/lib/services/backup-service.js +162 -0
- package/lib/services/cliproxy-api.js +174 -0
- package/lib/services/database.js +461 -0
- package/lib/services/newapi.js +97 -0
- package/lib/services/node-service.js +49 -0
- package/lib/utils/json-file.js +33 -0
- 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
|
+
});
|