ai-zero-token 1.0.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +32 -0
- package/README.zh-CN.md +32 -0
- package/admin-ui/dist/assets/app-mark-Gd2QnHMO.svg +9 -0
- package/admin-ui/dist/assets/index-CCywUil_.js +1745 -0
- package/admin-ui/dist/assets/index-DNzR8XR7.css +1 -0
- package/admin-ui/dist/assets/wechat-contact-Dlaib1YP.png +0 -0
- package/admin-ui/dist/index.html +14 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/dist/core/context.js +3 -0
- package/dist/core/providers/http-client.js +10 -4
- package/dist/core/providers/openai-codex/chat.js +23 -0
- package/dist/core/providers/openai-codex/oauth.js +24 -1
- package/dist/core/services/auth-service.js +176 -24
- package/dist/core/services/chat-service.js +2 -2
- package/dist/core/services/image-service.js +2 -2
- package/dist/core/services/network-detect-service.js +239 -0
- package/dist/desktop/main.js +143 -0
- package/dist/server/admin-page.js +112 -19
- package/dist/server/app.js +98 -5
- package/docs/DESKTOP_RELEASE.md +104 -0
- package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +429 -0
- package/package.json +71 -4
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { requestText } from "../providers/http-client.js";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const PLATFORM_PROBE_HEADERS = {
|
|
7
|
+
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
8
|
+
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
9
|
+
"cache-control": "no-cache",
|
|
10
|
+
"pragma": "no-cache",
|
|
11
|
+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
|
|
12
|
+
};
|
|
13
|
+
function parseKeyValueText(input) {
|
|
14
|
+
return Object.fromEntries(
|
|
15
|
+
input.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
16
|
+
const index = line.indexOf("=");
|
|
17
|
+
if (index <= 0) {
|
|
18
|
+
return ["", ""];
|
|
19
|
+
}
|
|
20
|
+
return [line.slice(0, index), line.slice(index + 1)];
|
|
21
|
+
}).filter(([key]) => key)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function parseDnsServers(text) {
|
|
25
|
+
const servers = /* @__PURE__ */ new Set();
|
|
26
|
+
for (const match of text.matchAll(/nameserver\[\d+\]\s*:\s*([0-9a-fA-F:.]+)/g)) {
|
|
27
|
+
servers.add(match[1]);
|
|
28
|
+
}
|
|
29
|
+
return [...servers];
|
|
30
|
+
}
|
|
31
|
+
function isPrivateOrReservedIp(ip) {
|
|
32
|
+
return /^(10|127|169\.254|172\.(1[6-9]|2\d|3[0-1])|192\.168)\./.test(ip) || /^0\./.test(ip) || /^100\.(6[4-9]|[7-9]\d|1\d\d|2[0-3]\d|24[0-7])\./.test(ip) || /^198\.(18|19)\./.test(ip) || /^203\.0\.113\./.test(ip) || /^192\.0\.2\./.test(ip) || /^198\.51\.100\./.test(ip) || /^fc00:/i.test(ip) || /^fd00:/i.test(ip) || /^fe80:/i.test(ip) || /^::1$/.test(ip);
|
|
33
|
+
}
|
|
34
|
+
function toCountryName(code) {
|
|
35
|
+
if (!code) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return new Intl.DisplayNames(["zh-CN"], { type: "region" }).of(code) ?? code;
|
|
40
|
+
} catch {
|
|
41
|
+
return code;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function runCommand(command, args, timeoutMs = 2500) {
|
|
45
|
+
const result = await execFileAsync(command, args, {
|
|
46
|
+
timeout: timeoutMs,
|
|
47
|
+
maxBuffer: 1024 * 128
|
|
48
|
+
});
|
|
49
|
+
return String(result.stdout || "");
|
|
50
|
+
}
|
|
51
|
+
async function readDnsServers() {
|
|
52
|
+
const servers = /* @__PURE__ */ new Set();
|
|
53
|
+
let source = "\u7CFB\u7EDF DNS";
|
|
54
|
+
try {
|
|
55
|
+
const scutil = await runCommand("scutil", ["--dns"], 2500);
|
|
56
|
+
parseDnsServers(scutil).forEach((item) => servers.add(item));
|
|
57
|
+
source = "scutil --dns";
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const wifiDns = await runCommand("networksetup", ["-getdnsservers", "Wi-Fi"], 2500);
|
|
62
|
+
for (const line of wifiDns.split(/\r?\n/)) {
|
|
63
|
+
const value = line.trim();
|
|
64
|
+
if (value && /^[0-9a-fA-F:.]+$/.test(value)) {
|
|
65
|
+
servers.add(value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (servers.size > 0) {
|
|
69
|
+
source = `${source} + networksetup`;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
servers: [...servers],
|
|
75
|
+
source
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function probePublicIpv4(proxy) {
|
|
79
|
+
const startedAt = performance.now();
|
|
80
|
+
const response = await requestText({
|
|
81
|
+
method: "GET",
|
|
82
|
+
url: "https://ifconfig.me/ip",
|
|
83
|
+
timeoutMs: 8e3,
|
|
84
|
+
proxyOverride: proxy
|
|
85
|
+
});
|
|
86
|
+
if (response.status < 200 || response.status >= 500) {
|
|
87
|
+
throw new Error(`\u516C\u7F51 IPv4 \u63A2\u6D4B\u5931\u8D25: HTTP ${response.status}`);
|
|
88
|
+
}
|
|
89
|
+
const ip = response.body.trim();
|
|
90
|
+
let trace = {};
|
|
91
|
+
try {
|
|
92
|
+
const traceResponse = await requestText({
|
|
93
|
+
method: "GET",
|
|
94
|
+
url: "https://www.cloudflare.com/cdn-cgi/trace",
|
|
95
|
+
timeoutMs: 8e3,
|
|
96
|
+
proxyOverride: proxy
|
|
97
|
+
});
|
|
98
|
+
trace = parseKeyValueText(traceResponse.body);
|
|
99
|
+
} catch {
|
|
100
|
+
trace = {};
|
|
101
|
+
}
|
|
102
|
+
const countryCode = trace.loc;
|
|
103
|
+
return {
|
|
104
|
+
ip,
|
|
105
|
+
countryCode,
|
|
106
|
+
countryName: toCountryName(countryCode),
|
|
107
|
+
colo: trace.colo,
|
|
108
|
+
source: "ifconfig.me + cloudflare trace",
|
|
109
|
+
elapsedMs: Math.round(performance.now() - startedAt)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function probePublicIpv6(proxy) {
|
|
113
|
+
const startedAt = performance.now();
|
|
114
|
+
const ipv6Candidates = [
|
|
115
|
+
"https://ifconfig.me/ipv6",
|
|
116
|
+
"https://ipv6.test-ipv6.com/"
|
|
117
|
+
];
|
|
118
|
+
for (const url of ipv6Candidates) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await requestText({
|
|
121
|
+
method: "GET",
|
|
122
|
+
url,
|
|
123
|
+
timeoutMs: 6e3,
|
|
124
|
+
proxyOverride: proxy
|
|
125
|
+
});
|
|
126
|
+
const body = response.body.trim();
|
|
127
|
+
if (!body) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (/^[0-9a-fA-F:]+$/.test(body) && body.includes(":")) {
|
|
131
|
+
return {
|
|
132
|
+
available: true,
|
|
133
|
+
ip: body,
|
|
134
|
+
source: url,
|
|
135
|
+
detail: "\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\u3002",
|
|
136
|
+
elapsedMs: Math.round(performance.now() - startedAt)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
available: false,
|
|
144
|
+
source: "ifconfig.me / test-ipv6.com",
|
|
145
|
+
detail: "\u672A\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\uFF0C\u5F53\u524D\u8FDE\u63A5\u53EF\u80FD\u53EA\u8D70 IPv4\u3002"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function probePlatform(url, label, proxy) {
|
|
149
|
+
const startedAt = performance.now();
|
|
150
|
+
try {
|
|
151
|
+
const response = await requestText({
|
|
152
|
+
method: "GET",
|
|
153
|
+
url,
|
|
154
|
+
headers: PLATFORM_PROBE_HEADERS,
|
|
155
|
+
timeoutMs: 8e3,
|
|
156
|
+
proxyOverride: proxy
|
|
157
|
+
});
|
|
158
|
+
const elapsedMs = Math.round(performance.now() - startedAt);
|
|
159
|
+
if (response.status >= 200 && response.status < 400) {
|
|
160
|
+
return {
|
|
161
|
+
key: label,
|
|
162
|
+
label,
|
|
163
|
+
url,
|
|
164
|
+
status: "\u53EF\u8FBE",
|
|
165
|
+
detail: `HTTP ${response.status} \u53EF\u6B63\u5E38\u8BBF\u95EE`,
|
|
166
|
+
tone: "green",
|
|
167
|
+
httpStatus: response.status,
|
|
168
|
+
elapsedMs
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (response.status >= 400 && response.status < 500) {
|
|
172
|
+
return {
|
|
173
|
+
key: label,
|
|
174
|
+
label,
|
|
175
|
+
url,
|
|
176
|
+
status: "\u53D7\u9650",
|
|
177
|
+
detail: `HTTP ${response.status}\uFF0C\u7F51\u7EDC\u53EF\u8FBE\u4F46\u7AD9\u70B9\u8FD4\u56DE\u9650\u5236`,
|
|
178
|
+
tone: "orange",
|
|
179
|
+
httpStatus: response.status,
|
|
180
|
+
elapsedMs
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
key: label,
|
|
185
|
+
label,
|
|
186
|
+
url,
|
|
187
|
+
status: "\u4E0D\u53EF\u7528",
|
|
188
|
+
detail: `HTTP ${response.status}\uFF0C\u7AD9\u70B9\u4E0D\u53EF\u7528`,
|
|
189
|
+
tone: "red",
|
|
190
|
+
httpStatus: response.status,
|
|
191
|
+
elapsedMs
|
|
192
|
+
};
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
key: label,
|
|
196
|
+
label,
|
|
197
|
+
url,
|
|
198
|
+
status: "\u4E0D\u53EF\u7528",
|
|
199
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
200
|
+
tone: "red",
|
|
201
|
+
elapsedMs: Math.round(performance.now() - startedAt)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
class NetworkDetectService {
|
|
206
|
+
async collectReport(proxy) {
|
|
207
|
+
const [publicIpv4, publicIpv6, dns, platforms] = await Promise.all([
|
|
208
|
+
probePublicIpv4(proxy),
|
|
209
|
+
probePublicIpv6(proxy),
|
|
210
|
+
readDnsServers(),
|
|
211
|
+
Promise.all([
|
|
212
|
+
probePlatform("https://www.google.com/generate_204", "Google", proxy),
|
|
213
|
+
probePlatform("https://chatgpt.com/", "ChatGPT", proxy),
|
|
214
|
+
probePlatform("https://claude.ai/", "Claude", proxy),
|
|
215
|
+
probePlatform("https://www.youtube.com/", "YouTube", proxy),
|
|
216
|
+
probePlatform("https://x.com/", "X", proxy)
|
|
217
|
+
])
|
|
218
|
+
]);
|
|
219
|
+
const dnsDetail = dns.servers.length > 0 ? `\u7CFB\u7EDF DNS: ${dns.servers.join(" / ")}` : "\u672A\u8BFB\u53D6\u5230\u7CFB\u7EDF DNS\u3002";
|
|
220
|
+
return {
|
|
221
|
+
checkedAt: Date.now(),
|
|
222
|
+
publicIpv4,
|
|
223
|
+
publicIpv6,
|
|
224
|
+
dns: {
|
|
225
|
+
servers: dns.servers,
|
|
226
|
+
source: dns.source,
|
|
227
|
+
detail: dnsDetail
|
|
228
|
+
},
|
|
229
|
+
proxy: {
|
|
230
|
+
enabled: Boolean(proxy?.enabled && proxy.url.trim()),
|
|
231
|
+
url: proxy?.url?.trim() || void 0
|
|
232
|
+
},
|
|
233
|
+
platforms
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export {
|
|
238
|
+
NetworkDetectService
|
|
239
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { app as electronApp, BrowserWindow, dialog, shell } from "electron";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { startServer } from "../server/index.js";
|
|
6
|
+
let gatewayServer = null;
|
|
7
|
+
let mainWindow = null;
|
|
8
|
+
let isQuitting = false;
|
|
9
|
+
const desktopDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const appIconPath = path.resolve(desktopDir, "../../build/icon.png");
|
|
11
|
+
electronApp.setName("AI Zero Token");
|
|
12
|
+
function createBrowserUrl(host, port) {
|
|
13
|
+
if (host === "0.0.0.0" || host === "::") {
|
|
14
|
+
return `http://127.0.0.1:${port}`;
|
|
15
|
+
}
|
|
16
|
+
return `http://${host}:${port}`;
|
|
17
|
+
}
|
|
18
|
+
function isGatewayUrl(targetUrl, gatewayUrl) {
|
|
19
|
+
try {
|
|
20
|
+
const target = new URL(targetUrl);
|
|
21
|
+
const gateway = new URL(gatewayUrl);
|
|
22
|
+
return target.origin === gateway.origin;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function resolveAdminUrl(gatewayUrl) {
|
|
28
|
+
const devUrl = process.env.AZT_ADMIN_UI_DEV_URL?.trim();
|
|
29
|
+
return devUrl || gatewayUrl;
|
|
30
|
+
}
|
|
31
|
+
async function ensureGatewayServer() {
|
|
32
|
+
if (gatewayServer) {
|
|
33
|
+
return gatewayServer;
|
|
34
|
+
}
|
|
35
|
+
gatewayServer = await startServer();
|
|
36
|
+
const adminUrl = createBrowserUrl(gatewayServer.host, gatewayServer.port);
|
|
37
|
+
console.log("AI Zero Token desktop gateway started.");
|
|
38
|
+
console.log(`admin: ${adminUrl}`);
|
|
39
|
+
console.log(`apiBase: ${adminUrl}/v1`);
|
|
40
|
+
console.log(`listen: http://${gatewayServer.host}:${gatewayServer.port}`);
|
|
41
|
+
return gatewayServer;
|
|
42
|
+
}
|
|
43
|
+
async function createMainWindow() {
|
|
44
|
+
const server = await ensureGatewayServer();
|
|
45
|
+
const gatewayUrl = createBrowserUrl(server.host, server.port);
|
|
46
|
+
const adminUrl = resolveAdminUrl(gatewayUrl);
|
|
47
|
+
mainWindow = new BrowserWindow({
|
|
48
|
+
width: 1440,
|
|
49
|
+
height: 960,
|
|
50
|
+
minWidth: 1100,
|
|
51
|
+
minHeight: 720,
|
|
52
|
+
title: "AI Zero Token",
|
|
53
|
+
icon: appIconPath,
|
|
54
|
+
backgroundColor: "#f8fafc",
|
|
55
|
+
webPreferences: {
|
|
56
|
+
contextIsolation: true,
|
|
57
|
+
nodeIntegration: false,
|
|
58
|
+
sandbox: true
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
62
|
+
void shell.openExternal(url);
|
|
63
|
+
return { action: "deny" };
|
|
64
|
+
});
|
|
65
|
+
mainWindow.webContents.on("will-navigate", (event, url) => {
|
|
66
|
+
if (isGatewayUrl(url, adminUrl) || isGatewayUrl(url, gatewayUrl)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
void shell.openExternal(url);
|
|
71
|
+
});
|
|
72
|
+
mainWindow.on("closed", () => {
|
|
73
|
+
mainWindow = null;
|
|
74
|
+
});
|
|
75
|
+
await mainWindow.loadURL(adminUrl);
|
|
76
|
+
}
|
|
77
|
+
function focusMainWindow() {
|
|
78
|
+
if (!mainWindow) {
|
|
79
|
+
void createMainWindow().catch(handleStartupError);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (mainWindow.isMinimized()) {
|
|
83
|
+
mainWindow.restore();
|
|
84
|
+
}
|
|
85
|
+
mainWindow.focus();
|
|
86
|
+
}
|
|
87
|
+
async function closeGatewayServer() {
|
|
88
|
+
if (!gatewayServer) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const server = gatewayServer;
|
|
92
|
+
gatewayServer = null;
|
|
93
|
+
await server.app.close();
|
|
94
|
+
}
|
|
95
|
+
function handleStartupError(error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
console.error("[desktop:error]", error);
|
|
98
|
+
if (electronApp.isReady()) {
|
|
99
|
+
dialog.showErrorBox("AI Zero Token \u542F\u52A8\u5931\u8D25", message);
|
|
100
|
+
}
|
|
101
|
+
electronApp.quit();
|
|
102
|
+
}
|
|
103
|
+
const hasSingleInstanceLock = electronApp.requestSingleInstanceLock();
|
|
104
|
+
if (!hasSingleInstanceLock) {
|
|
105
|
+
electronApp.quit();
|
|
106
|
+
} else {
|
|
107
|
+
electronApp.on("second-instance", () => {
|
|
108
|
+
focusMainWindow();
|
|
109
|
+
});
|
|
110
|
+
electronApp.whenReady().then(() => {
|
|
111
|
+
if (process.platform === "darwin") {
|
|
112
|
+
electronApp.dock?.setIcon(appIconPath);
|
|
113
|
+
}
|
|
114
|
+
electronApp.setAboutPanelOptions({
|
|
115
|
+
applicationName: "AI Zero Token",
|
|
116
|
+
applicationVersion: electronApp.getVersion(),
|
|
117
|
+
iconPath: appIconPath
|
|
118
|
+
});
|
|
119
|
+
return createMainWindow();
|
|
120
|
+
}).catch(handleStartupError);
|
|
121
|
+
electronApp.on("activate", () => {
|
|
122
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
123
|
+
focusMainWindow();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
electronApp.on("window-all-closed", () => {
|
|
127
|
+
if (process.platform !== "darwin") {
|
|
128
|
+
electronApp.quit();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
electronApp.on("before-quit", (event) => {
|
|
132
|
+
if (!gatewayServer || isQuitting) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
isQuitting = true;
|
|
137
|
+
void closeGatewayServer().catch((error) => {
|
|
138
|
+
console.error("[desktop:gateway:close]", error);
|
|
139
|
+
}).finally(() => {
|
|
140
|
+
electronApp.quit();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -1898,6 +1898,7 @@ function renderAdminPage() {
|
|
|
1898
1898
|
<option value="all">\u5168\u90E8\u72B6\u6001</option>
|
|
1899
1899
|
<option value="healthy">\u5065\u5EB7</option>
|
|
1900
1900
|
<option value="warning">\u5373\u5C06\u8017\u5C3D</option>
|
|
1901
|
+
<option value="invalid">\u767B\u5F55\u5931\u6548</option>
|
|
1901
1902
|
<option value="expired">\u5DF2\u8FC7\u671F</option>
|
|
1902
1903
|
<option value="active">\u4F7F\u7528\u4E2D</option>
|
|
1903
1904
|
</select>
|
|
@@ -2302,6 +2303,13 @@ function renderAdminPage() {
|
|
|
2302
2303
|
return date.toLocaleString("zh-CN", { hour12: false });
|
|
2303
2304
|
}
|
|
2304
2305
|
|
|
2306
|
+
function timestampToMillis(value) {
|
|
2307
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
return value < 1000000000000 ? value * 1000 : value;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2305
2313
|
function formatShortTime(value) {
|
|
2306
2314
|
if (!value) {
|
|
2307
2315
|
return "--:--";
|
|
@@ -2528,6 +2536,15 @@ function renderAdminPage() {
|
|
|
2528
2536
|
};
|
|
2529
2537
|
}
|
|
2530
2538
|
|
|
2539
|
+
if (profile.authStatus && (profile.authStatus.state === "token_invalidated" || profile.authStatus.state === "auth_error")) {
|
|
2540
|
+
return {
|
|
2541
|
+
supported: false,
|
|
2542
|
+
label: "\u8BA4\u8BC1\u5931\u6548",
|
|
2543
|
+
detail: "\u8D26\u53F7\u8BA4\u8BC1\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u4F7F\u7528\u56FE\u7247\u751F\u6210\u3002",
|
|
2544
|
+
badgeClass: "red",
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2531
2548
|
const planType = getPlanType(profile);
|
|
2532
2549
|
if (planType === "free") {
|
|
2533
2550
|
return {
|
|
@@ -2546,6 +2563,17 @@ function renderAdminPage() {
|
|
|
2546
2563
|
};
|
|
2547
2564
|
}
|
|
2548
2565
|
|
|
2566
|
+
function describeAuthStatus(profile) {
|
|
2567
|
+
const authStatus = profile && profile.authStatus ? profile.authStatus : null;
|
|
2568
|
+
if (!authStatus || authStatus.state === "ok") {
|
|
2569
|
+
return authStatus && authStatus.checkedAt ? "\u6B63\u5E38 \xB7 " + formatTime(authStatus.checkedAt) : "\u6B63\u5E38";
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
const prefix = authStatus.state === "token_invalidated" ? "\u767B\u5F55\u5931\u6548" : "\u8BA4\u8BC1\u5F02\u5E38";
|
|
2573
|
+
const detail = authStatus.code || authStatus.httpStatus ? " (" + (authStatus.code || authStatus.httpStatus) + ")" : "";
|
|
2574
|
+
return prefix + detail + " \xB7 " + formatTime(authStatus.checkedAt);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2549
2577
|
function maskEmail(email) {
|
|
2550
2578
|
if (typeof email !== "string" || email.indexOf("@") === -1) {
|
|
2551
2579
|
return email || "";
|
|
@@ -2620,6 +2648,10 @@ function renderAdminPage() {
|
|
|
2620
2648
|
return Math.max(0, Math.min(100, value));
|
|
2621
2649
|
}
|
|
2622
2650
|
|
|
2651
|
+
function getPrimaryRemaining(profile) {
|
|
2652
|
+
return 100 - getPrimaryUsage(profile);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2623
2655
|
function getSecondaryUsage(profile) {
|
|
2624
2656
|
const value = profile && profile.quota && typeof profile.quota.secondaryUsedPercent === "number"
|
|
2625
2657
|
? profile.quota.secondaryUsedPercent
|
|
@@ -2664,6 +2696,22 @@ function renderAdminPage() {
|
|
|
2664
2696
|
|
|
2665
2697
|
function getProfileHealth(profile) {
|
|
2666
2698
|
const now = Date.now();
|
|
2699
|
+
if (profile && profile.authStatus && profile.authStatus.state === "token_invalidated") {
|
|
2700
|
+
return {
|
|
2701
|
+
key: "invalid",
|
|
2702
|
+
label: "\u767B\u5F55\u5931\u6548",
|
|
2703
|
+
badgeClass: "red",
|
|
2704
|
+
barClass: "red",
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
if (profile && profile.authStatus && profile.authStatus.state === "auth_error") {
|
|
2708
|
+
return {
|
|
2709
|
+
key: "invalid",
|
|
2710
|
+
label: "\u8BA4\u8BC1\u5F02\u5E38",
|
|
2711
|
+
badgeClass: "red",
|
|
2712
|
+
barClass: "red",
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2667
2715
|
if (profile && profile.expiresAt && profile.expiresAt <= now) {
|
|
2668
2716
|
return {
|
|
2669
2717
|
key: "expired",
|
|
@@ -2700,6 +2748,22 @@ function renderAdminPage() {
|
|
|
2700
2748
|
};
|
|
2701
2749
|
}
|
|
2702
2750
|
|
|
2751
|
+
function isProfileUnavailable(profile) {
|
|
2752
|
+
const health = getProfileHealth(profile);
|
|
2753
|
+
return health.key === "invalid" || health.key === "expired";
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
function getProfileSortGroup(profile, codexAccountId) {
|
|
2757
|
+
const isCodexActive = Boolean(codexAccountId && profile.accountId === codexAccountId);
|
|
2758
|
+
if (profile.isActive || isCodexActive) {
|
|
2759
|
+
return 0;
|
|
2760
|
+
}
|
|
2761
|
+
if (isProfileUnavailable(profile)) {
|
|
2762
|
+
return 2;
|
|
2763
|
+
}
|
|
2764
|
+
return 1;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2703
2767
|
function describeReset(profile, slot) {
|
|
2704
2768
|
if (!profile || !profile.quota) {
|
|
2705
2769
|
return "\u6682\u65E0\u6570\u636E";
|
|
@@ -2708,8 +2772,9 @@ function renderAdminPage() {
|
|
|
2708
2772
|
const quota = profile.quota;
|
|
2709
2773
|
const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
|
|
2710
2774
|
const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
|
|
2711
|
-
|
|
2712
|
-
|
|
2775
|
+
const resetAtMillis = timestampToMillis(resetAt);
|
|
2776
|
+
if (resetAtMillis) {
|
|
2777
|
+
return formatTime(resetAtMillis);
|
|
2713
2778
|
}
|
|
2714
2779
|
if (typeof resetAfter === "number" && resetAfter > 0) {
|
|
2715
2780
|
return formatCompactDuration(resetAfter) + "\u540E";
|
|
@@ -2739,11 +2804,13 @@ function renderAdminPage() {
|
|
|
2739
2804
|
const quota = profile.quota;
|
|
2740
2805
|
const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
|
|
2741
2806
|
const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
|
|
2742
|
-
|
|
2743
|
-
|
|
2807
|
+
const resetAtMillis = timestampToMillis(resetAt);
|
|
2808
|
+
if (resetAtMillis) {
|
|
2809
|
+
return formatCompactDateTime(resetAtMillis);
|
|
2744
2810
|
}
|
|
2745
2811
|
if (typeof resetAfter === "number" && resetAfter > 0) {
|
|
2746
|
-
|
|
2812
|
+
const capturedAt = timestampToMillis(quota.capturedAt);
|
|
2813
|
+
return capturedAt ? formatCompactDateTime(capturedAt + resetAfter * 1000) : formatCompactDuration(resetAfter) + "\u540E";
|
|
2747
2814
|
}
|
|
2748
2815
|
return "\u672A\u77E5";
|
|
2749
2816
|
}
|
|
@@ -3169,6 +3236,9 @@ function renderAdminPage() {
|
|
|
3169
3236
|
if (status === "warning" && health.key !== "warning") {
|
|
3170
3237
|
return false;
|
|
3171
3238
|
}
|
|
3239
|
+
if (status === "invalid" && health.key !== "invalid") {
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
3172
3242
|
if (status === "expired" && health.key !== "expired") {
|
|
3173
3243
|
return false;
|
|
3174
3244
|
}
|
|
@@ -3176,24 +3246,18 @@ function renderAdminPage() {
|
|
|
3176
3246
|
});
|
|
3177
3247
|
|
|
3178
3248
|
filtered.sort(function (a, b) {
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
if (activeDiff !== 0) {
|
|
3183
|
-
return activeDiff;
|
|
3184
|
-
}
|
|
3185
|
-
const gatewayDiff = Number(b.isActive) - Number(a.isActive);
|
|
3186
|
-
if (gatewayDiff !== 0) {
|
|
3187
|
-
return gatewayDiff;
|
|
3188
|
-
}
|
|
3189
|
-
const codexDiff = Number(bCodexActive) - Number(aCodexActive);
|
|
3190
|
-
if (codexDiff !== 0) {
|
|
3191
|
-
return codexDiff;
|
|
3249
|
+
const groupDiff = getProfileSortGroup(a, codexAccountId) - getProfileSortGroup(b, codexAccountId);
|
|
3250
|
+
if (groupDiff !== 0) {
|
|
3251
|
+
return groupDiff;
|
|
3192
3252
|
}
|
|
3193
3253
|
const planDiff = getPlanRank(b) - getPlanRank(a);
|
|
3194
3254
|
if (planDiff !== 0) {
|
|
3195
3255
|
return planDiff;
|
|
3196
3256
|
}
|
|
3257
|
+
const primaryRemainingDiff = getPrimaryRemaining(b) - getPrimaryRemaining(a);
|
|
3258
|
+
if (primaryRemainingDiff !== 0) {
|
|
3259
|
+
return primaryRemainingDiff;
|
|
3260
|
+
}
|
|
3197
3261
|
if (sort === "latency-asc") {
|
|
3198
3262
|
const aCapturedAt = getQuotaSnapshotTime(a) || 0;
|
|
3199
3263
|
const bCapturedAt = getQuotaSnapshotTime(b) || 0;
|
|
@@ -3344,6 +3408,7 @@ function renderAdminPage() {
|
|
|
3344
3408
|
? '<div class="meta-grid">'
|
|
3345
3409
|
+ '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
|
|
3346
3410
|
+ '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
|
|
3411
|
+
+ '<div class="meta-item"><label>\u8BA4\u8BC1\u72B6\u6001</label><strong>' + escapeHtml(describeAuthStatus(profile)) + "</strong></div>"
|
|
3347
3412
|
+ '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
|
|
3348
3413
|
+ '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
|
|
3349
3414
|
+ '<div class="meta-item"><label>Account ID</label><code>' + escapeHtml(state.showEmails ? (profile.accountId || "\u672A\u63D0\u4F9B") : maskIdentifier(profile.accountId || "\u672A\u63D0\u4F9B")) + "</code></div>"
|
|
@@ -3353,6 +3418,7 @@ function renderAdminPage() {
|
|
|
3353
3418
|
+ '<div class="account-actions">'
|
|
3354
3419
|
+ actionButton
|
|
3355
3420
|
+ codexButton
|
|
3421
|
+
+ '<button class="btn-secondary" type="button" data-profile-action="sync-quota" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5237\u65B0\u989D\u5EA6</button>'
|
|
3356
3422
|
+ '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
|
|
3357
3423
|
+ '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
|
|
3358
3424
|
+ "</div>"
|
|
@@ -3862,6 +3928,28 @@ function renderAdminPage() {
|
|
|
3862
3928
|
await applyProfileToCodex(profileId, button);
|
|
3863
3929
|
return;
|
|
3864
3930
|
}
|
|
3931
|
+
if (action === "sync-quota") {
|
|
3932
|
+
setBusy(button, true);
|
|
3933
|
+
authStatus.textContent = "\u6B63\u5728\u5237\u65B0\u8D26\u53F7\u989D\u5EA6...";
|
|
3934
|
+
try {
|
|
3935
|
+
const config = await fetchJson("/_gateway/admin/profiles/sync-quota", {
|
|
3936
|
+
method: "POST",
|
|
3937
|
+
headers: {
|
|
3938
|
+
"Content-Type": "application/json",
|
|
3939
|
+
},
|
|
3940
|
+
body: formatJson({
|
|
3941
|
+
profileId: profileId,
|
|
3942
|
+
}),
|
|
3943
|
+
});
|
|
3944
|
+
renderConfig(config);
|
|
3945
|
+
authStatus.textContent = "\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65\u3002";
|
|
3946
|
+
} catch (error) {
|
|
3947
|
+
authStatus.textContent = error.message;
|
|
3948
|
+
} finally {
|
|
3949
|
+
setBusy(button, false);
|
|
3950
|
+
}
|
|
3951
|
+
return;
|
|
3952
|
+
}
|
|
3865
3953
|
|
|
3866
3954
|
setBusy(button, true);
|
|
3867
3955
|
authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
|
|
@@ -4285,7 +4373,12 @@ function renderAdminPage() {
|
|
|
4285
4373
|
authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
|
|
4286
4374
|
refreshConfig({
|
|
4287
4375
|
syncRuntime: true,
|
|
4288
|
-
}).then(function () {
|
|
4376
|
+
}).then(function (config) {
|
|
4377
|
+
if (config && config.quotaSync) {
|
|
4378
|
+
const sync = config.quotaSync;
|
|
4379
|
+
authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0: " + String(sync.synced) + "/" + String(sync.total) + " \u4E2A\u8D26\u53F7\u6210\u529F" + (sync.failed ? "\uFF0C" + String(sync.failed) + " \u4E2A\u5931\u8D25" : "") + (sync.skipped ? "\uFF0C" + String(sync.skipped) + " \u4E2A\u767B\u5F55\u5931\u6548\u5DF2\u8DF3\u8FC7" : "") + "\u3002";
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4289
4382
|
authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0\u3002";
|
|
4290
4383
|
}).catch(function (error) {
|
|
4291
4384
|
authStatus.textContent = error && error.message ? error.message : String(error);
|