ai-zero-token 1.0.5 → 1.0.6
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 +14 -1
- package/dist/cli/commands/help.js +4 -0
- package/dist/cli/commands/profiles.js +63 -0
- package/dist/cli/index.js +4 -0
- package/dist/core/services/auth-service.js +47 -0
- package/dist/core/store/profile-transfer.js +142 -0
- package/dist/server/admin-page.js +278 -4
- package/dist/server/app.js +51 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.6 - 2026-04-28
|
|
4
|
+
|
|
5
|
+
- Added account JSON import/export support in the management page.
|
|
6
|
+
- Added batch account import from a single object, an array, or a `profiles` bundle.
|
|
7
|
+
- Added selectable batch export for checked accounts in the management page.
|
|
8
|
+
- Added `azt profiles import/export` CLI commands for account transfer workflows.
|
|
9
|
+
- Added an account import template endpoint for quick JSON format reference.
|
|
10
|
+
|
|
3
11
|
## 1.0.5 - 2026-04-27
|
|
4
12
|
|
|
5
13
|
- Added management-page proxy configuration for upstream requests, persisted in local settings.
|
package/README.md
CHANGED
|
@@ -220,6 +220,8 @@ azt start
|
|
|
220
220
|
- 查看当前账号、已保存账号列表、过期时间、token 摘要
|
|
221
221
|
- 查看账号套餐 plan 和当前账号是否支持生图
|
|
222
222
|
- 在多个已保存账号之间切换当前使用账号
|
|
223
|
+
- 在“新增账号”里选择 OAuth 登录,或粘贴外部账号 JSON 批量导入
|
|
224
|
+
- 导出单个账号,或勾选多个账号后批量导出所选账号 JSON
|
|
223
225
|
- 删除单个本地账号,或一键清空全部本地账号
|
|
224
226
|
- 切换默认模型
|
|
225
227
|
- 测试 `models` / `responses` / `chat.completions`
|
|
@@ -227,6 +229,8 @@ azt start
|
|
|
227
229
|
|
|
228
230
|
管理页里邮箱默认脱敏显示,需要手动点击“查看邮箱”才会显示明文。
|
|
229
231
|
|
|
232
|
+
导出的账号 JSON 包含完整 `access_token` 和 `refresh_token`,等同于账号登录凭据,只适合在可信环境中传递。
|
|
233
|
+
|
|
230
234
|
如果当前网络访问海外上游不稳定,可以在管理页的“接口测试 / 系统设置”区域启用“上游代理”,并填写你自己的代理地址。保存后,OAuth 换取 token、模型刷新和接口转发都会通过该代理访问上游;本地管理页和 `127.0.0.1` 默认保持直连。
|
|
231
235
|
|
|
232
236
|
默认监听地址:
|
|
@@ -403,7 +407,16 @@ curl http://127.0.0.1:8787/v1/images/generations \
|
|
|
403
407
|
|
|
404
408
|
## 兼容说明
|
|
405
409
|
|
|
406
|
-
代码里仍然保留了 `login`、`status`、`models`、`ask`、`serve`、`clear` 等 CLI 命令,主要用于调试、兼容和后续扩展。
|
|
410
|
+
代码里仍然保留了 `login`、`status`、`models`、`profiles`、`ask`、`serve`、`clear` 等 CLI 命令,主要用于调试、兼容和后续扩展。
|
|
411
|
+
|
|
412
|
+
账号 JSON 也可以通过 CLI 导入/导出:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
azt profiles import ./profile.json
|
|
416
|
+
azt profiles export active ./profile.json
|
|
417
|
+
azt profiles export all ./profiles.json
|
|
418
|
+
azt profiles export "openai-codex:<accountId>" ./profile.json
|
|
419
|
+
```
|
|
407
420
|
|
|
408
421
|
README 不再把这些命令作为推荐使用方式。默认使用路径就是:
|
|
409
422
|
|
|
@@ -5,6 +5,9 @@ function printHelp() {
|
|
|
5
5
|
azt login
|
|
6
6
|
azt models
|
|
7
7
|
azt models --refresh
|
|
8
|
+
azt profiles import ./profile.json
|
|
9
|
+
azt profiles export active ./profile.json
|
|
10
|
+
azt profiles export all ./profiles.json
|
|
8
11
|
azt status
|
|
9
12
|
azt ask "\u4F60\u597D\uFF0C\u8BF7\u7B80\u5355\u4ECB\u7ECD\u4E00\u4E0B\u81EA\u5DF1"
|
|
10
13
|
azt ask --model gpt-5.3-codex "\u4F60\u597D"
|
|
@@ -18,6 +21,7 @@ function printHelp() {
|
|
|
18
21
|
|
|
19
22
|
login \u8D70\u771F\u5B9E OpenAI Codex OAuth\uFF0C\u65B0\u589E\u5E76\u4FDD\u5B58\u4E00\u4E2A\u8D26\u53F7 profile
|
|
20
23
|
models \u67E5\u770B\u5F53\u524D\u53EF\u7528\u6A21\u578B\u5217\u8868\uFF1B\u4F18\u5148\u8BFB\u53D6 ~/.codex/models_cache.json\uFF0C--refresh \u53EF\u624B\u52A8\u91CD\u8BFB
|
|
24
|
+
profiles \u5BFC\u5165/\u5BFC\u51FA\u8D26\u53F7 JSON\uFF1B\u5BFC\u51FA\u6587\u4EF6\u5305\u542B\u5B8C\u6574 refresh token\uFF0C\u8BF7\u53EA\u5206\u4EAB\u7ED9\u53EF\u4FE1\u5BF9\u8C61
|
|
21
25
|
status \u67E5\u770B\u5F53\u524D demo \u5F53\u524D\u6FC0\u6D3B\u8D26\u53F7\u3001\u8D26\u53F7\u6570\u91CF\u548C\u8FC7\u671F\u65F6\u95F4
|
|
22
26
|
ask \u7528\u4FDD\u5B58\u7684 token \u8C03\u771F\u5B9E Codex Responses API
|
|
23
27
|
\u5B9E\u9A8C\u6A21\u5F0F\u53EF\u7528 --payload-file \u900F\u4F20\u989D\u5916\u8BF7\u6C42\u4F53\uFF0C\u914D\u5408 --dump-raw / --print-raw \u89C2\u5BDF SSE \u539F\u59CB\u4E8B\u4EF6
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { createGatewayContext } from "../../core/context.js";
|
|
4
|
+
import { formatExpiry } from "../shared.js";
|
|
5
|
+
function printProfilesHelp() {
|
|
6
|
+
console.log(`\u7528\u6CD5:
|
|
7
|
+
|
|
8
|
+
azt profiles import ./profile.json
|
|
9
|
+
azt profiles export active ./profile.json
|
|
10
|
+
azt profiles export all ./profiles.json
|
|
11
|
+
azt profiles export <profileId> ./profile.json
|
|
12
|
+
|
|
13
|
+
\u8BF4\u660E:
|
|
14
|
+
|
|
15
|
+
import \u5BFC\u5165\u5916\u90E8\u8D26\u53F7 JSON\uFF0C\u652F\u6301\u5355\u4E2A\u5BF9\u8C61\u3001\u5BF9\u8C61\u6570\u7EC4\u6216 { "profiles": [...] }
|
|
16
|
+
export \u5BFC\u51FA\u5F53\u524D\u8D26\u53F7\u3001\u5168\u90E8\u8D26\u53F7\u6216\u6307\u5B9A profileId \u7684\u5B8C\u6574\u51ED\u636E JSON
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
async function runProfilesCommand(argv) {
|
|
20
|
+
const [action, target, outputPath] = argv;
|
|
21
|
+
const ctx = createGatewayContext();
|
|
22
|
+
if (action === "import") {
|
|
23
|
+
if (!target) {
|
|
24
|
+
throw new Error("\u7F3A\u5C11\u5BFC\u5165\u6587\u4EF6\u8DEF\u5F84\u3002\u7528\u6CD5: azt profiles import ./profile.json");
|
|
25
|
+
}
|
|
26
|
+
const raw = await fs.readFile(target, "utf8");
|
|
27
|
+
const profiles = await ctx.authService.importProfiles(JSON.parse(raw));
|
|
28
|
+
console.log(`\u8D26\u53F7\u5BFC\u5165\u6210\u529F\uFF0C\u5171 ${profiles.length} \u4E2A\u3002`);
|
|
29
|
+
for (const profile of profiles) {
|
|
30
|
+
console.log(`profileId: ${profile.profileId}`);
|
|
31
|
+
console.log(`accountId: ${profile.accountId}`);
|
|
32
|
+
if (profile.email) {
|
|
33
|
+
console.log(`email: ${profile.email}`);
|
|
34
|
+
}
|
|
35
|
+
console.log(`expires: ${formatExpiry(profile.expires)}`);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (action === "export") {
|
|
40
|
+
const isAll = target === "all";
|
|
41
|
+
const profileId = outputPath && target !== "active" && !isAll ? target : void 0;
|
|
42
|
+
const resolvedOutputPath = outputPath ?? (target !== "active" && !isAll ? target : void 0);
|
|
43
|
+
if (!resolvedOutputPath) {
|
|
44
|
+
throw new Error("\u7F3A\u5C11\u5BFC\u51FA\u6587\u4EF6\u8DEF\u5F84\u3002\u7528\u6CD5: azt profiles export active ./profile.json");
|
|
45
|
+
}
|
|
46
|
+
const exported = isAll ? await ctx.authService.exportProfiles() : await ctx.authService.exportProfile(profileId);
|
|
47
|
+
await fs.writeFile(resolvedOutputPath, `${JSON.stringify(exported, null, 2)}
|
|
48
|
+
`, "utf8");
|
|
49
|
+
console.log("\u8D26\u53F7\u5BFC\u51FA\u6210\u529F\u3002");
|
|
50
|
+
if ("profiles" in exported) {
|
|
51
|
+
console.log(`profileCount: ${exported.profiles.length}`);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(`profileId: ${exported.profile_id}`);
|
|
54
|
+
console.log(`accountId: ${exported.account_id}`);
|
|
55
|
+
}
|
|
56
|
+
console.log(`file: ${resolvedOutputPath}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
printProfilesHelp();
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
runProfilesCommand
|
|
63
|
+
};
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { runClearCommand } from "./commands/clear.js";
|
|
|
4
4
|
import { printHelp } from "./commands/help.js";
|
|
5
5
|
import { runLoginCommand } from "./commands/login.js";
|
|
6
6
|
import { runModelsCommand } from "./commands/models.js";
|
|
7
|
+
import { runProfilesCommand } from "./commands/profiles.js";
|
|
7
8
|
import { runServeCommand } from "./commands/serve.js";
|
|
8
9
|
import { runStartCommand } from "./commands/start.js";
|
|
9
10
|
import { runStatusCommand } from "./commands/status.js";
|
|
@@ -19,6 +20,9 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
19
20
|
case "models":
|
|
20
21
|
await runModelsCommand(rest);
|
|
21
22
|
return;
|
|
23
|
+
case "profiles":
|
|
24
|
+
await runProfilesCommand(rest);
|
|
25
|
+
return;
|
|
22
26
|
case "ask":
|
|
23
27
|
await runAskCommand(rest);
|
|
24
28
|
return;
|
|
@@ -13,6 +13,13 @@ import {
|
|
|
13
13
|
refreshOpenAICodexToken
|
|
14
14
|
} from "../providers/openai-codex/oauth.js";
|
|
15
15
|
import { askOpenAICodex } from "../providers/openai-codex/chat.js";
|
|
16
|
+
import {
|
|
17
|
+
exportProfilesToJson,
|
|
18
|
+
exportProfileToJson,
|
|
19
|
+
getProfileImportTemplate,
|
|
20
|
+
importProfileFromJson,
|
|
21
|
+
importProfilesFromJson
|
|
22
|
+
} from "../store/profile-transfer.js";
|
|
16
23
|
class AuthService {
|
|
17
24
|
constructor(configService) {
|
|
18
25
|
this.configService = configService;
|
|
@@ -50,6 +57,46 @@ class AuthService {
|
|
|
50
57
|
await saveProfile(profile);
|
|
51
58
|
return this.toManagedProfile(profile);
|
|
52
59
|
}
|
|
60
|
+
async importProfile(value, provider = "openai-codex") {
|
|
61
|
+
if (provider !== "openai-codex") {
|
|
62
|
+
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
63
|
+
}
|
|
64
|
+
const profile = importProfileFromJson(value);
|
|
65
|
+
await saveProfile(profile);
|
|
66
|
+
return this.toManagedProfile(profile);
|
|
67
|
+
}
|
|
68
|
+
async importProfiles(value, provider = "openai-codex") {
|
|
69
|
+
if (provider !== "openai-codex") {
|
|
70
|
+
throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
|
|
71
|
+
}
|
|
72
|
+
const profiles = importProfilesFromJson(value);
|
|
73
|
+
for (const profile of profiles) {
|
|
74
|
+
await saveProfile(profile);
|
|
75
|
+
}
|
|
76
|
+
return profiles.map((profile) => this.toManagedProfile(profile));
|
|
77
|
+
}
|
|
78
|
+
async exportProfile(profileId, provider = "openai-codex") {
|
|
79
|
+
const profiles = await listProfiles();
|
|
80
|
+
const activeProfile = await this.getActiveProfile(provider);
|
|
81
|
+
const targetProfileId = profileId?.trim() || activeProfile?.profileId;
|
|
82
|
+
const profile = profiles.find((item) => item.provider === provider && item.profileId === targetProfileId);
|
|
83
|
+
if (!profile) {
|
|
84
|
+
throw new Error(targetProfileId ? `\u6CA1\u6709\u627E\u5230\u53EF\u5BFC\u51FA\u7684\u8D26\u53F7: ${targetProfileId}` : "\u6CA1\u6709\u53EF\u5BFC\u51FA\u7684\u5F53\u524D\u8D26\u53F7\u3002");
|
|
85
|
+
}
|
|
86
|
+
return exportProfileToJson(profile);
|
|
87
|
+
}
|
|
88
|
+
async exportProfiles(profileIds, provider = "openai-codex") {
|
|
89
|
+
const profiles = await listProfiles();
|
|
90
|
+
const idSet = profileIds && profileIds.length > 0 ? new Set(profileIds.map((item) => item.trim()).filter(Boolean)) : null;
|
|
91
|
+
const selected = profiles.filter((item) => item.provider === provider).filter((item) => !idSet || idSet.has(item.profileId));
|
|
92
|
+
if (selected.length === 0) {
|
|
93
|
+
throw new Error("\u6CA1\u6709\u627E\u5230\u53EF\u5BFC\u51FA\u7684\u8D26\u53F7\u3002");
|
|
94
|
+
}
|
|
95
|
+
return exportProfilesToJson(selected);
|
|
96
|
+
}
|
|
97
|
+
getProfileImportTemplate() {
|
|
98
|
+
return getProfileImportTemplate();
|
|
99
|
+
}
|
|
53
100
|
async getActiveProfile(provider = "openai-codex") {
|
|
54
101
|
const profile = await getActiveProfile();
|
|
55
102
|
if (!profile || profile.provider !== provider) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const AUTH_CLAIM_PATH = "https://api.openai.com/auth";
|
|
3
|
+
const PROFILE_CLAIM_PATH = "https://api.openai.com/profile";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function decodeJwtPayload(token) {
|
|
8
|
+
try {
|
|
9
|
+
const parts = token.split(".");
|
|
10
|
+
if (parts.length !== 3) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const payload = parts[1] ?? "";
|
|
14
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
15
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
16
|
+
return JSON.parse(Buffer.from(normalized + padding, "base64").toString("utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getString(value) {
|
|
22
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
23
|
+
}
|
|
24
|
+
function getNumber(value) {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
26
|
+
}
|
|
27
|
+
function extractAccountId(payload, fallback) {
|
|
28
|
+
const authClaim = payload?.[AUTH_CLAIM_PATH];
|
|
29
|
+
const accountId = isRecord(authClaim) ? getString(authClaim.chatgpt_account_id) : void 0;
|
|
30
|
+
const fallbackAccountId = getString(fallback);
|
|
31
|
+
const resolved = accountId ?? fallbackAccountId;
|
|
32
|
+
if (!resolved) {
|
|
33
|
+
throw new Error("\u5BFC\u5165\u5931\u8D25: \u65E0\u6CD5\u4ECE access_token \u4E2D\u63D0\u53D6 accountId\u3002");
|
|
34
|
+
}
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
function extractEmail(payload, fallback) {
|
|
38
|
+
const profileClaim = payload?.[PROFILE_CLAIM_PATH];
|
|
39
|
+
const profileEmail = isRecord(profileClaim) ? getString(profileClaim.email) : void 0;
|
|
40
|
+
return profileEmail ?? getString(payload?.email) ?? getString(fallback);
|
|
41
|
+
}
|
|
42
|
+
function parseExpiry(input, payload) {
|
|
43
|
+
const jwtExp = getNumber(payload?.exp);
|
|
44
|
+
if (jwtExp) {
|
|
45
|
+
return jwtExp * 1e3;
|
|
46
|
+
}
|
|
47
|
+
const directExpires = getNumber(input.expires);
|
|
48
|
+
if (directExpires) {
|
|
49
|
+
return directExpires > 1e10 ? directExpires : directExpires * 1e3;
|
|
50
|
+
}
|
|
51
|
+
const expiresAt = getNumber(input.expires_at);
|
|
52
|
+
if (expiresAt) {
|
|
53
|
+
return expiresAt > 1e10 ? expiresAt : expiresAt * 1e3;
|
|
54
|
+
}
|
|
55
|
+
const expired = getString(input.expired) ?? getString(input.expiresAt);
|
|
56
|
+
if (expired) {
|
|
57
|
+
const parsed = Date.parse(expired);
|
|
58
|
+
if (Number.isFinite(parsed)) {
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new Error("\u5BFC\u5165\u5931\u8D25: \u7F3A\u5C11\u6709\u6548\u7684\u8FC7\u671F\u65F6\u95F4\u3002");
|
|
63
|
+
}
|
|
64
|
+
function importProfileFromJson(value) {
|
|
65
|
+
if (!isRecord(value)) {
|
|
66
|
+
throw new Error("\u5BFC\u5165\u5931\u8D25: JSON \u6839\u8282\u70B9\u5FC5\u987B\u662F\u5BF9\u8C61\u3002");
|
|
67
|
+
}
|
|
68
|
+
const access = getString(value.access_token) ?? getString(value.access);
|
|
69
|
+
const refresh = getString(value.refresh_token) ?? getString(value.refresh);
|
|
70
|
+
if (!access || !refresh) {
|
|
71
|
+
throw new Error("\u5BFC\u5165\u5931\u8D25: \u7F3A\u5C11 access_token/access \u6216 refresh_token/refresh\u3002");
|
|
72
|
+
}
|
|
73
|
+
const payload = decodeJwtPayload(access);
|
|
74
|
+
const accountId = extractAccountId(payload, value.account_id ?? value.accountId);
|
|
75
|
+
const email = extractEmail(payload, value.email);
|
|
76
|
+
const expires = parseExpiry(value, payload);
|
|
77
|
+
return {
|
|
78
|
+
provider: "openai-codex",
|
|
79
|
+
profileId: `openai-codex:${accountId}`,
|
|
80
|
+
mode: "oauth_account",
|
|
81
|
+
access,
|
|
82
|
+
refresh,
|
|
83
|
+
expires,
|
|
84
|
+
accountId,
|
|
85
|
+
email
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function importProfilesFromJson(value) {
|
|
89
|
+
const items = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.profiles) ? value.profiles : [value];
|
|
90
|
+
return items.map((item, index) => {
|
|
91
|
+
try {
|
|
92
|
+
return importProfileFromJson(item);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
throw new Error(`\u7B2C ${index + 1} \u4E2A\u8D26\u53F7${message.startsWith("\u5BFC\u5165\u5931\u8D25") ? message : `\u5BFC\u5165\u5931\u8D25: ${message}`}`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function exportProfileToJson(profile) {
|
|
100
|
+
return {
|
|
101
|
+
type: "codex",
|
|
102
|
+
access_token: profile.access,
|
|
103
|
+
refresh_token: profile.refresh,
|
|
104
|
+
expired: new Date(profile.expires).toISOString(),
|
|
105
|
+
email: profile.email,
|
|
106
|
+
account_id: profile.accountId,
|
|
107
|
+
profile_id: profile.profileId,
|
|
108
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function exportProfilesToJson(profiles) {
|
|
112
|
+
return {
|
|
113
|
+
type: "codex_profiles",
|
|
114
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
115
|
+
profiles: profiles.map((profile) => exportProfileToJson(profile))
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function getProfileImportTemplate() {
|
|
119
|
+
return {
|
|
120
|
+
type: "codex_profiles",
|
|
121
|
+
exported_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
122
|
+
profiles: [
|
|
123
|
+
{
|
|
124
|
+
type: "codex",
|
|
125
|
+
access_token: "eyJ...access_token",
|
|
126
|
+
refresh_token: "rt_...",
|
|
127
|
+
expired: "2026-05-04T22:13:00.000Z",
|
|
128
|
+
email: "user@example.com",
|
|
129
|
+
account_id: "\u53EF\u9009\uFF0C\u901A\u5E38\u4F1A\u4ECE access_token \u81EA\u52A8\u89E3\u6790",
|
|
130
|
+
profile_id: "\u53EF\u9009\uFF0C\u5BFC\u5165\u65F6\u4F1A\u6309 account_id \u81EA\u52A8\u751F\u6210",
|
|
131
|
+
exported_at: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
exportProfileToJson,
|
|
138
|
+
exportProfilesToJson,
|
|
139
|
+
getProfileImportTemplate,
|
|
140
|
+
importProfileFromJson,
|
|
141
|
+
importProfilesFromJson
|
|
142
|
+
};
|
|
@@ -647,6 +647,19 @@ function renderAdminPage() {
|
|
|
647
647
|
min-width: 156px;
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
+
.account-selected-count {
|
|
651
|
+
min-height: 40px;
|
|
652
|
+
display: inline-flex;
|
|
653
|
+
align-items: center;
|
|
654
|
+
color: var(--text-muted);
|
|
655
|
+
font-size: 12px;
|
|
656
|
+
font-weight: 600;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.account-modal-body .textarea {
|
|
660
|
+
min-height: 280px;
|
|
661
|
+
}
|
|
662
|
+
|
|
650
663
|
.account-grid {
|
|
651
664
|
display: grid;
|
|
652
665
|
grid-auto-rows: 1fr;
|
|
@@ -691,6 +704,30 @@ function renderAdminPage() {
|
|
|
691
704
|
display: grid;
|
|
692
705
|
gap: 6px;
|
|
693
706
|
min-width: 0;
|
|
707
|
+
flex: 1;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.account-select {
|
|
711
|
+
display: inline-flex;
|
|
712
|
+
align-items: center;
|
|
713
|
+
gap: 6px;
|
|
714
|
+
min-height: 28px;
|
|
715
|
+
padding: 0 8px;
|
|
716
|
+
border: 1px solid var(--line);
|
|
717
|
+
border-radius: 8px;
|
|
718
|
+
background: #fff;
|
|
719
|
+
color: var(--text-muted);
|
|
720
|
+
font-size: 12px;
|
|
721
|
+
font-weight: 600;
|
|
722
|
+
cursor: pointer;
|
|
723
|
+
user-select: none;
|
|
724
|
+
white-space: nowrap;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.account-select input {
|
|
728
|
+
width: 14px;
|
|
729
|
+
height: 14px;
|
|
730
|
+
margin: 0;
|
|
694
731
|
}
|
|
695
732
|
|
|
696
733
|
.account-name {
|
|
@@ -1365,7 +1402,7 @@ function renderAdminPage() {
|
|
|
1365
1402
|
<strong id="updatePanelTitle">\u53D1\u73B0\u65B0\u7248\u672C</strong>
|
|
1366
1403
|
<span id="updatePanelDetail"></span>
|
|
1367
1404
|
</div>
|
|
1368
|
-
<code class="update-command" id="updatePanelCommand">npm install -g ai-zero-token
|
|
1405
|
+
<code class="update-command" id="updatePanelCommand">npm install -g ai-zero-token</code>
|
|
1369
1406
|
</section>
|
|
1370
1407
|
|
|
1371
1408
|
<section class="summary-grid" id="summaryGrid"></section>
|
|
@@ -1380,6 +1417,7 @@ function renderAdminPage() {
|
|
|
1380
1417
|
</div>
|
|
1381
1418
|
<div class="actions">
|
|
1382
1419
|
<button class="btn-secondary" type="button" id="activateCurrentBtn">\u5B9A\u4F4D\u5F53\u524D\u8D26\u53F7</button>
|
|
1420
|
+
<button class="btn-secondary" type="button" id="exportSelectedProfilesBtn">\u5BFC\u51FA\u6240\u9009</button>
|
|
1383
1421
|
</div>
|
|
1384
1422
|
</div>
|
|
1385
1423
|
|
|
@@ -1398,8 +1436,8 @@ function renderAdminPage() {
|
|
|
1398
1436
|
<option value="expiry-asc">\u6309\u8FC7\u671F\u65F6\u95F4</option>
|
|
1399
1437
|
<option value="name-asc">\u6309\u90AE\u7BB1\u6392\u5E8F</option>
|
|
1400
1438
|
</select>
|
|
1439
|
+
<span class="account-selected-count" id="selectedProfileCount">\u5DF2\u9009\u62E9 0 \u4E2A</span>
|
|
1401
1440
|
</div>
|
|
1402
|
-
|
|
1403
1441
|
<div class="account-grid" id="profileList"></div>
|
|
1404
1442
|
</section>
|
|
1405
1443
|
|
|
@@ -1578,6 +1616,41 @@ function renderAdminPage() {
|
|
|
1578
1616
|
</section>
|
|
1579
1617
|
</div>
|
|
1580
1618
|
|
|
1619
|
+
<div class="modal-backdrop" id="accountModal" aria-hidden="true">
|
|
1620
|
+
<section class="modal-card" role="dialog" aria-modal="true" aria-labelledby="accountModalTitle">
|
|
1621
|
+
<div class="modal-head">
|
|
1622
|
+
<div>
|
|
1623
|
+
<h3 id="accountModalTitle">\u65B0\u589E\u8D26\u53F7</h3>
|
|
1624
|
+
<p>\u9009\u62E9 OAuth \u767B\u5F55\uFF0C\u6216\u7C98\u8D34\u5355\u4E2A/\u6279\u91CF\u8D26\u53F7 JSON \u5BFC\u5165\u3002</p>
|
|
1625
|
+
</div>
|
|
1626
|
+
<div class="actions">
|
|
1627
|
+
<button class="btn-secondary" id="closeAccountModalBtn" type="button">\u5173\u95ED</button>
|
|
1628
|
+
</div>
|
|
1629
|
+
</div>
|
|
1630
|
+
<div class="modal-body account-modal-body">
|
|
1631
|
+
<div class="contact-notes">
|
|
1632
|
+
<div class="contact-note">
|
|
1633
|
+
<strong>\u767B\u5F55\u65B0\u589E</strong>
|
|
1634
|
+
<span>\u6253\u5F00 OpenAI OAuth \u6388\u6743\u6D41\u7A0B\uFF0C\u767B\u5F55\u6210\u529F\u540E\u81EA\u52A8\u4FDD\u5B58\u5E76\u5207\u6362\u4E3A\u5F53\u524D\u8D26\u53F7\u3002</span>
|
|
1635
|
+
<button class="btn-primary" id="oauthLoginBtn" type="button">\u767B\u5F55</button>
|
|
1636
|
+
</div>
|
|
1637
|
+
<div class="contact-note">
|
|
1638
|
+
<strong>\u6279\u91CF\u5BFC\u5165</strong>
|
|
1639
|
+
<span>\u652F\u6301\u5355\u4E2A\u5BF9\u8C61\u3001\u5BF9\u8C61\u6570\u7EC4\uFF0C\u6216\u5305\u542B profiles \u6570\u7EC4\u7684\u5BF9\u8C61\u3002\u5BFC\u5165\u540E\u6700\u540E\u4E00\u4E2A\u8D26\u53F7\u4F1A\u6210\u4E3A\u5F53\u524D\u8D26\u53F7\u3002</span>
|
|
1640
|
+
<div class="actions">
|
|
1641
|
+
<button class="btn-secondary" id="loadImportTemplateBtn" type="button">\u586B\u5165\u53C2\u8003\u683C\u5F0F</button>
|
|
1642
|
+
<button class="btn-primary" id="importProfileBtn" type="button">\u5BFC\u5165</button>
|
|
1643
|
+
</div>
|
|
1644
|
+
</div>
|
|
1645
|
+
</div>
|
|
1646
|
+
<div>
|
|
1647
|
+
<textarea class="textarea" id="profileImportJson" spellcheck="false" placeholder='\u7C98\u8D34\u8D26\u53F7 JSON\uFF0C\u652F\u6301 { "profiles": [...] } \u6279\u91CF\u5BFC\u5165'></textarea>
|
|
1648
|
+
<p class="hint">\u5BFC\u5165\u548C\u5BFC\u51FA\u7684 JSON \u90FD\u5305\u542B\u5B8C\u6574 access token \u548C refresh token\uFF0C\u8BF7\u53EA\u5728\u53EF\u4FE1\u73AF\u5883\u4E2D\u5904\u7406\u3002</p>
|
|
1649
|
+
</div>
|
|
1650
|
+
</div>
|
|
1651
|
+
</section>
|
|
1652
|
+
</div>
|
|
1653
|
+
|
|
1581
1654
|
<div class="modal-backdrop" id="contactModal" aria-hidden="true">
|
|
1582
1655
|
<section class="modal-card" role="dialog" aria-modal="true" aria-labelledby="contactModalTitle">
|
|
1583
1656
|
<div class="modal-head">
|
|
@@ -1622,6 +1695,7 @@ function renderAdminPage() {
|
|
|
1622
1695
|
status: "all",
|
|
1623
1696
|
sort: "quota-desc",
|
|
1624
1697
|
},
|
|
1698
|
+
selectedProfileIds: {},
|
|
1625
1699
|
testerResultTab: "response",
|
|
1626
1700
|
};
|
|
1627
1701
|
|
|
@@ -1659,6 +1733,7 @@ function renderAdminPage() {
|
|
|
1659
1733
|
const imageCapabilityHint = document.getElementById("imageCapabilityHint");
|
|
1660
1734
|
const runTestBtn = document.getElementById("runTestBtn");
|
|
1661
1735
|
const toggleEmailBtn = document.getElementById("toggleEmailBtn");
|
|
1736
|
+
const accountModal = document.getElementById("accountModal");
|
|
1662
1737
|
const contactModal = document.getElementById("contactModal");
|
|
1663
1738
|
const imagePreviewModal = document.getElementById("imagePreviewModal");
|
|
1664
1739
|
const contactBtn = document.getElementById("contactBtn");
|
|
@@ -1668,6 +1743,12 @@ function renderAdminPage() {
|
|
|
1668
1743
|
const profileSearch = document.getElementById("profileSearch");
|
|
1669
1744
|
const profileStatusFilter = document.getElementById("profileStatusFilter");
|
|
1670
1745
|
const profileSort = document.getElementById("profileSort");
|
|
1746
|
+
const profileImportJson = document.getElementById("profileImportJson");
|
|
1747
|
+
const importProfileBtn = document.getElementById("importProfileBtn");
|
|
1748
|
+
const oauthLoginBtn = document.getElementById("oauthLoginBtn");
|
|
1749
|
+
const loadImportTemplateBtn = document.getElementById("loadImportTemplateBtn");
|
|
1750
|
+
const exportSelectedProfilesBtn = document.getElementById("exportSelectedProfilesBtn");
|
|
1751
|
+
const selectedProfileCount = document.getElementById("selectedProfileCount");
|
|
1671
1752
|
const proxyEnabled = document.getElementById("proxyEnabled");
|
|
1672
1753
|
const proxyUrl = document.getElementById("proxyUrl");
|
|
1673
1754
|
const proxyNoProxy = document.getElementById("proxyNoProxy");
|
|
@@ -2369,8 +2450,35 @@ function renderAdminPage() {
|
|
|
2369
2450
|
return filtered;
|
|
2370
2451
|
}
|
|
2371
2452
|
|
|
2453
|
+
function getSelectedProfileIds() {
|
|
2454
|
+
return Object.keys(state.selectedProfileIds).filter(function (profileId) {
|
|
2455
|
+
return !!state.selectedProfileIds[profileId];
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function syncSelectedProfiles(config) {
|
|
2460
|
+
const profiles = Array.isArray(config.profiles) ? config.profiles : [];
|
|
2461
|
+
const availableIds = profiles.reduce(function (result, profile) {
|
|
2462
|
+
result[profile.profileId] = true;
|
|
2463
|
+
return result;
|
|
2464
|
+
}, {});
|
|
2465
|
+
getSelectedProfileIds().forEach(function (profileId) {
|
|
2466
|
+
if (!availableIds[profileId]) {
|
|
2467
|
+
delete state.selectedProfileIds[profileId];
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
function updateSelectedProfileControls() {
|
|
2473
|
+
const count = getSelectedProfileIds().length;
|
|
2474
|
+
selectedProfileCount.textContent = "\u5DF2\u9009\u62E9 " + String(count) + " \u4E2A";
|
|
2475
|
+
exportSelectedProfilesBtn.disabled = count === 0;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2372
2478
|
function renderProfiles(config) {
|
|
2373
2479
|
const container = document.getElementById("profileList");
|
|
2480
|
+
syncSelectedProfiles(config);
|
|
2481
|
+
updateSelectedProfileControls();
|
|
2374
2482
|
const profiles = getFilteredProfiles(config);
|
|
2375
2483
|
const gridClass = profiles.length <= 0
|
|
2376
2484
|
? ""
|
|
@@ -2389,6 +2497,7 @@ function renderAdminPage() {
|
|
|
2389
2497
|
}
|
|
2390
2498
|
|
|
2391
2499
|
container.innerHTML = profiles.map(function (profile) {
|
|
2500
|
+
const selected = !!state.selectedProfileIds[profile.profileId];
|
|
2392
2501
|
const isSingleProfile = profiles.length === 1;
|
|
2393
2502
|
const health = getProfileHealth(profile);
|
|
2394
2503
|
const planType = getPlanType(profile);
|
|
@@ -2418,6 +2527,7 @@ function renderAdminPage() {
|
|
|
2418
2527
|
+ '<span class="badge ' + escapeHtml(imageCapability.badgeClass) + '">' + escapeHtml(imageCapability.label) + "</span>"
|
|
2419
2528
|
+ "</div>"
|
|
2420
2529
|
+ "</div>"
|
|
2530
|
+
+ '<label class="account-select"><input type="checkbox" data-profile-select data-profile-id="' + escapeHtml(profile.profileId) + '"' + (selected ? " checked" : "") + " /><span>\u9009\u62E9</span></label>"
|
|
2421
2531
|
+ "</div>"
|
|
2422
2532
|
+ '<div class="account-metrics">'
|
|
2423
2533
|
+ '<div class="quota-row">'
|
|
@@ -2440,6 +2550,7 @@ function renderAdminPage() {
|
|
|
2440
2550
|
+ "</div>"
|
|
2441
2551
|
+ '<div class="account-actions">'
|
|
2442
2552
|
+ actionButton
|
|
2553
|
+
+ '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
|
|
2443
2554
|
+ '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
|
|
2444
2555
|
+ "</div>"
|
|
2445
2556
|
+ "</article>";
|
|
@@ -2832,7 +2943,7 @@ function renderAdminPage() {
|
|
|
2832
2943
|
}
|
|
2833
2944
|
|
|
2834
2945
|
async function login() {
|
|
2835
|
-
const button =
|
|
2946
|
+
const button = oauthLoginBtn;
|
|
2836
2947
|
setBusy(button, true);
|
|
2837
2948
|
authStatus.textContent = "\u6B63\u5728\u65B0\u589E\u8D26\u53F7\u3001\u7B49\u5F85 OAuth \u5B8C\u6210\uFF0C\u5E76\u540C\u6B65\u989D\u5EA6\u4FE1\u606F...";
|
|
2838
2949
|
try {
|
|
@@ -2845,6 +2956,7 @@ function renderAdminPage() {
|
|
|
2845
2956
|
if (config.profile && config.profile.quota) {
|
|
2846
2957
|
authStatus.textContent = "\u8D26\u53F7\u5DF2\u4FDD\u5B58\uFF0C\u5DF2\u5207\u6362\u4E3A\u5F53\u524D\u4F7F\u7528\u8D26\u53F7\u5E76\u540C\u6B65\u989D\u5EA6\u4FE1\u606F: " + getProfileDisplayLabel(config.profile);
|
|
2847
2958
|
}
|
|
2959
|
+
closeAccountModal();
|
|
2848
2960
|
} catch (error) {
|
|
2849
2961
|
authStatus.textContent = error.message;
|
|
2850
2962
|
} finally {
|
|
@@ -2871,6 +2983,11 @@ function renderAdminPage() {
|
|
|
2871
2983
|
}
|
|
2872
2984
|
|
|
2873
2985
|
async function runProfileAction(action, profileId, button) {
|
|
2986
|
+
if (action === "export") {
|
|
2987
|
+
await exportProfile(profileId, button);
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2874
2991
|
setBusy(button, true);
|
|
2875
2992
|
authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
|
|
2876
2993
|
try {
|
|
@@ -2903,6 +3020,116 @@ function renderAdminPage() {
|
|
|
2903
3020
|
}
|
|
2904
3021
|
}
|
|
2905
3022
|
|
|
3023
|
+
function downloadJsonFile(fileName, value) {
|
|
3024
|
+
const blob = new Blob([formatJson(value) + "\\n"], { type: "application/json" });
|
|
3025
|
+
const url = URL.createObjectURL(blob);
|
|
3026
|
+
const link = document.createElement("a");
|
|
3027
|
+
link.href = url;
|
|
3028
|
+
link.download = fileName;
|
|
3029
|
+
document.body.appendChild(link);
|
|
3030
|
+
link.click();
|
|
3031
|
+
link.remove();
|
|
3032
|
+
URL.revokeObjectURL(url);
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
async function importProfile() {
|
|
3036
|
+
const raw = profileImportJson.value.trim();
|
|
3037
|
+
if (!raw) {
|
|
3038
|
+
authStatus.textContent = "\u8BF7\u5148\u7C98\u8D34\u8981\u5BFC\u5165\u7684\u8D26\u53F7 JSON\u3002";
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
let payload;
|
|
3043
|
+
try {
|
|
3044
|
+
payload = JSON.parse(raw);
|
|
3045
|
+
} catch (_error) {
|
|
3046
|
+
authStatus.textContent = "\u5BFC\u5165\u5931\u8D25: JSON \u683C\u5F0F\u4E0D\u6B63\u786E\u3002";
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
setBusy(importProfileBtn, true);
|
|
3051
|
+
authStatus.textContent = "\u6B63\u5728\u5BFC\u5165\u8D26\u53F7\u5E76\u540C\u6B65\u989D\u5EA6\u4FE1\u606F...";
|
|
3052
|
+
try {
|
|
3053
|
+
let config = await fetchJson("/_gateway/admin/profiles/import", {
|
|
3054
|
+
method: "POST",
|
|
3055
|
+
headers: {
|
|
3056
|
+
"Content-Type": "application/json",
|
|
3057
|
+
},
|
|
3058
|
+
body: formatJson({
|
|
3059
|
+
profile: payload,
|
|
3060
|
+
}),
|
|
3061
|
+
});
|
|
3062
|
+
renderConfig(config);
|
|
3063
|
+
const count = config.importedProfileCount || 1;
|
|
3064
|
+
const baseMessage = "\u5DF2\u5BFC\u5165 " + String(count) + " \u4E2A\u8D26\u53F7\uFF0C\u5E76\u5DF2\u5207\u6362\u4E3A\u5F53\u524D\u4F7F\u7528\u8D26\u53F7: " + getProfileDisplayLabel(config.profile);
|
|
3065
|
+
config = await syncQuotaAfterProfileChange(config, baseMessage);
|
|
3066
|
+
if (config.profile && config.profile.quota) {
|
|
3067
|
+
authStatus.textContent = "\u5DF2\u5BFC\u5165 " + String(count) + " \u4E2A\u8D26\u53F7\uFF0C\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65: " + getProfileDisplayLabel(config.profile);
|
|
3068
|
+
}
|
|
3069
|
+
profileImportJson.value = "";
|
|
3070
|
+
closeAccountModal();
|
|
3071
|
+
} catch (error) {
|
|
3072
|
+
authStatus.textContent = error.message;
|
|
3073
|
+
} finally {
|
|
3074
|
+
setBusy(importProfileBtn, false);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
async function loadImportTemplate() {
|
|
3079
|
+
setBusy(loadImportTemplateBtn, true);
|
|
3080
|
+
authStatus.textContent = "\u6B63\u5728\u8BFB\u53D6\u5BFC\u5165\u53C2\u8003\u683C\u5F0F...";
|
|
3081
|
+
try {
|
|
3082
|
+
const result = await fetchJson("/_gateway/admin/profiles/import-template");
|
|
3083
|
+
profileImportJson.value = formatJson(result.profile);
|
|
3084
|
+
authStatus.textContent = "\u5DF2\u586B\u5165\u53C2\u8003\u683C\u5F0F\uFF0C\u53EF\u66FF\u6362\u5176\u4E2D\u7684 token \u540E\u5BFC\u5165\u3002";
|
|
3085
|
+
} catch (error) {
|
|
3086
|
+
authStatus.textContent = error.message;
|
|
3087
|
+
} finally {
|
|
3088
|
+
setBusy(loadImportTemplateBtn, false);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
async function exportProfile(profileId, button, options) {
|
|
3093
|
+
setBusy(button, true);
|
|
3094
|
+
authStatus.textContent = "\u6B63\u5728\u5BFC\u51FA\u8D26\u53F7\u914D\u7F6E...";
|
|
3095
|
+
try {
|
|
3096
|
+
const exportAll = !!(options && options.all);
|
|
3097
|
+
const profileIds = options && Array.isArray(options.profileIds) ? options.profileIds : null;
|
|
3098
|
+
const result = await fetchJson("/_gateway/admin/profiles/export", {
|
|
3099
|
+
method: "POST",
|
|
3100
|
+
headers: {
|
|
3101
|
+
"Content-Type": "application/json",
|
|
3102
|
+
},
|
|
3103
|
+
body: formatJson(profileIds ? { profileIds: profileIds } : exportAll ? { all: true } : { profileId: profileId }),
|
|
3104
|
+
});
|
|
3105
|
+
const profile = result.profile;
|
|
3106
|
+
const isBundle = profile && Array.isArray(profile.profiles);
|
|
3107
|
+
const suffix = isBundle
|
|
3108
|
+
? "profiles-" + String(profile.profiles.length)
|
|
3109
|
+
: profile && profile.account_id ? profile.account_id : "active";
|
|
3110
|
+
downloadJsonFile("ai-zero-token-" + suffix + ".json", profile);
|
|
3111
|
+
authStatus.textContent = isBundle
|
|
3112
|
+
? "\u5DF2\u6279\u91CF\u5BFC\u51FA " + String(profile.profiles.length) + " \u4E2A\u8D26\u53F7\u3002\u8BF7\u59A5\u5584\u4FDD\u7BA1\u5BFC\u51FA\u7684 refresh token\u3002"
|
|
3113
|
+
: "\u8D26\u53F7\u914D\u7F6E\u5DF2\u5BFC\u51FA\u3002\u8BF7\u59A5\u5584\u4FDD\u7BA1\u5BFC\u51FA\u7684 refresh token\u3002";
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
authStatus.textContent = error.message;
|
|
3116
|
+
} finally {
|
|
3117
|
+
setBusy(button, false);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
async function exportSelectedProfiles() {
|
|
3122
|
+
const profileIds = getSelectedProfileIds();
|
|
3123
|
+
if (profileIds.length === 0) {
|
|
3124
|
+
authStatus.textContent = "\u8BF7\u5148\u52FE\u9009\u8981\u5BFC\u51FA\u7684\u8D26\u53F7\u3002";
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
await exportProfile(null, exportSelectedProfilesBtn, {
|
|
3129
|
+
profileIds: profileIds,
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
|
|
2906
3133
|
async function saveModel() {
|
|
2907
3134
|
const button = document.getElementById("saveModelBtn");
|
|
2908
3135
|
const select = document.getElementById("defaultModel");
|
|
@@ -3081,6 +3308,16 @@ function renderAdminPage() {
|
|
|
3081
3308
|
}
|
|
3082
3309
|
}
|
|
3083
3310
|
|
|
3311
|
+
function openAccountModal() {
|
|
3312
|
+
accountModal.classList.add("is-open");
|
|
3313
|
+
accountModal.setAttribute("aria-hidden", "false");
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function closeAccountModal() {
|
|
3317
|
+
accountModal.classList.remove("is-open");
|
|
3318
|
+
accountModal.setAttribute("aria-hidden", "true");
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3084
3321
|
function openContactModal() {
|
|
3085
3322
|
contactModal.classList.add("is-open");
|
|
3086
3323
|
contactModal.setAttribute("aria-hidden", "false");
|
|
@@ -3091,7 +3328,7 @@ function renderAdminPage() {
|
|
|
3091
3328
|
contactModal.setAttribute("aria-hidden", "true");
|
|
3092
3329
|
}
|
|
3093
3330
|
|
|
3094
|
-
document.getElementById("loginBtn").addEventListener("click",
|
|
3331
|
+
document.getElementById("loginBtn").addEventListener("click", openAccountModal);
|
|
3095
3332
|
document.getElementById("refreshBtn").addEventListener("click", function () {
|
|
3096
3333
|
authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
|
|
3097
3334
|
refreshConfig({
|
|
@@ -3103,6 +3340,11 @@ function renderAdminPage() {
|
|
|
3103
3340
|
});
|
|
3104
3341
|
});
|
|
3105
3342
|
document.getElementById("logoutBtn").addEventListener("click", logout);
|
|
3343
|
+
oauthLoginBtn.addEventListener("click", login);
|
|
3344
|
+
importProfileBtn.addEventListener("click", importProfile);
|
|
3345
|
+
loadImportTemplateBtn.addEventListener("click", loadImportTemplate);
|
|
3346
|
+
exportSelectedProfilesBtn.addEventListener("click", exportSelectedProfiles);
|
|
3347
|
+
document.getElementById("closeAccountModalBtn").addEventListener("click", closeAccountModal);
|
|
3106
3348
|
contactBtn.addEventListener("click", openContactModal);
|
|
3107
3349
|
document.getElementById("closeContactBtn").addEventListener("click", closeContactModal);
|
|
3108
3350
|
document.getElementById("closeImagePreviewBtn").addEventListener("click", closeImagePreviewModal);
|
|
@@ -3124,6 +3366,10 @@ function renderAdminPage() {
|
|
|
3124
3366
|
});
|
|
3125
3367
|
|
|
3126
3368
|
document.getElementById("profileList").addEventListener("click", function (event) {
|
|
3369
|
+
if (event.target.closest("[data-profile-select]")) {
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3127
3373
|
const button = event.target.closest("[data-profile-action]");
|
|
3128
3374
|
if (!button) {
|
|
3129
3375
|
return;
|
|
@@ -3138,6 +3384,25 @@ function renderAdminPage() {
|
|
|
3138
3384
|
runProfileAction(action, profileId, button);
|
|
3139
3385
|
});
|
|
3140
3386
|
|
|
3387
|
+
document.getElementById("profileList").addEventListener("change", function (event) {
|
|
3388
|
+
const checkbox = event.target.closest("[data-profile-select]");
|
|
3389
|
+
if (!checkbox) {
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
const profileId = checkbox.getAttribute("data-profile-id");
|
|
3394
|
+
if (!profileId) {
|
|
3395
|
+
return;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
if (checkbox.checked) {
|
|
3399
|
+
state.selectedProfileIds[profileId] = true;
|
|
3400
|
+
} else {
|
|
3401
|
+
delete state.selectedProfileIds[profileId];
|
|
3402
|
+
}
|
|
3403
|
+
updateSelectedProfileControls();
|
|
3404
|
+
});
|
|
3405
|
+
|
|
3141
3406
|
document.getElementById("activateCurrentBtn").addEventListener("click", function () {
|
|
3142
3407
|
if (!state.config || !state.config.profile || !state.config.profile.profileId) {
|
|
3143
3408
|
return;
|
|
@@ -3209,6 +3474,12 @@ function renderAdminPage() {
|
|
|
3209
3474
|
}
|
|
3210
3475
|
});
|
|
3211
3476
|
|
|
3477
|
+
accountModal.addEventListener("click", function (event) {
|
|
3478
|
+
if (event.target === accountModal) {
|
|
3479
|
+
closeAccountModal();
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
|
|
3212
3483
|
imagePreviewModal.addEventListener("click", function (event) {
|
|
3213
3484
|
if (event.target === imagePreviewModal) {
|
|
3214
3485
|
closeImagePreviewModal();
|
|
@@ -3222,6 +3493,9 @@ function renderAdminPage() {
|
|
|
3222
3493
|
if (event.key === "Escape" && contactModal.classList.contains("is-open")) {
|
|
3223
3494
|
closeContactModal();
|
|
3224
3495
|
}
|
|
3496
|
+
if (event.key === "Escape" && accountModal.classList.contains("is-open")) {
|
|
3497
|
+
closeAccountModal();
|
|
3498
|
+
}
|
|
3225
3499
|
});
|
|
3226
3500
|
|
|
3227
3501
|
setTesterResultTab(state.testerResultTab);
|
package/dist/server/app.js
CHANGED
|
@@ -77,6 +77,14 @@ const settingsUpdateSchema = z.object({
|
|
|
77
77
|
const profileActionSchema = z.object({
|
|
78
78
|
profileId: z.string().min(1)
|
|
79
79
|
});
|
|
80
|
+
const profileImportSchema = z.object({
|
|
81
|
+
profile: z.unknown()
|
|
82
|
+
});
|
|
83
|
+
const profileExportSchema = z.object({
|
|
84
|
+
profileId: z.string().min(1).optional(),
|
|
85
|
+
profileIds: z.array(z.string().min(1)).optional(),
|
|
86
|
+
all: z.boolean().optional()
|
|
87
|
+
});
|
|
80
88
|
const imageGenerationsBodySchema = z.object({
|
|
81
89
|
prompt: z.string().min(1),
|
|
82
90
|
model: z.string().optional(),
|
|
@@ -467,6 +475,49 @@ function createApp(params) {
|
|
|
467
475
|
await ctx.authService.removeProfile(parsed.data.profileId);
|
|
468
476
|
return buildAdminConfig(request);
|
|
469
477
|
});
|
|
478
|
+
app.post("/_gateway/admin/profiles/import", async (request, reply) => {
|
|
479
|
+
const parsed = profileImportSchema.safeParse(request.body);
|
|
480
|
+
if (!parsed.success) {
|
|
481
|
+
reply.code(400);
|
|
482
|
+
return {
|
|
483
|
+
error: {
|
|
484
|
+
type: "validation_error",
|
|
485
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const importedProfiles = await ctx.authService.importProfiles(parsed.data.profile);
|
|
490
|
+
await ctx.authService.syncActiveProfileQuota("openai-codex", {
|
|
491
|
+
suppressErrors: true
|
|
492
|
+
});
|
|
493
|
+
return {
|
|
494
|
+
...await buildAdminConfig(request),
|
|
495
|
+
importedProfileCount: importedProfiles.length
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
app.get("/_gateway/admin/profiles/import-template", async () => ({
|
|
499
|
+
profile: ctx.authService.getProfileImportTemplate()
|
|
500
|
+
}));
|
|
501
|
+
app.post("/_gateway/admin/profiles/export", async (request, reply) => {
|
|
502
|
+
const parsed = profileExportSchema.safeParse(request.body ?? {});
|
|
503
|
+
if (!parsed.success) {
|
|
504
|
+
reply.code(400);
|
|
505
|
+
return {
|
|
506
|
+
error: {
|
|
507
|
+
type: "validation_error",
|
|
508
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (parsed.data.all || parsed.data.profileIds) {
|
|
513
|
+
return {
|
|
514
|
+
profile: await ctx.authService.exportProfiles(parsed.data.profileIds)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
profile: await ctx.authService.exportProfile(parsed.data.profileId)
|
|
519
|
+
};
|
|
520
|
+
});
|
|
470
521
|
app.put("/_gateway/admin/settings", async (request, reply) => {
|
|
471
522
|
const parsed = settingsUpdateSchema.safeParse(request.body);
|
|
472
523
|
if (!parsed.success) {
|
package/package.json
CHANGED