ai-zero-token 1.0.4 → 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 CHANGED
@@ -1,5 +1,20 @@
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
+
11
+ ## 1.0.5 - 2026-04-27
12
+
13
+ - Added management-page proxy configuration for upstream requests, persisted in local settings.
14
+ - Routed upstream requests through configured curl proxy settings when enabled.
15
+ - Removed the local fixed-size allowlist for image generation `size`, allowing upstream validation to decide supported values.
16
+ - Documented the proxy configuration workflow without including a specific proxy address.
17
+
3
18
  ## 1.0.4 - 2026-04-24
4
19
 
5
20
  - Moved persistent account and settings state to the user home directory at `~/.ai-zero-token/.state`.
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,10 @@ azt start
227
229
 
228
230
  管理页里邮箱默认脱敏显示,需要手动点击“查看邮箱”才会显示明文。
229
231
 
232
+ 导出的账号 JSON 包含完整 `access_token` 和 `refresh_token`,等同于账号登录凭据,只适合在可信环境中传递。
233
+
234
+ 如果当前网络访问海外上游不稳定,可以在管理页的“接口测试 / 系统设置”区域启用“上游代理”,并填写你自己的代理地址。保存后,OAuth 换取 token、模型刷新和接口转发都会通过该代理访问上游;本地管理页和 `127.0.0.1` 默认保持直连。
235
+
230
236
  默认监听地址:
231
237
 
232
238
  ```text
@@ -401,7 +407,16 @@ curl http://127.0.0.1:8787/v1/images/generations \
401
407
 
402
408
  ## 兼容说明
403
409
 
404
- 代码里仍然保留了 `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
+ ```
405
420
 
406
421
  README 不再把这些命令作为推荐使用方式。默认使用路径就是:
407
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;
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import { loadSettings } from "../store/settings-store.js";
3
4
  const CURL_STATUS_MARKER = "\n__CURL_STATUS__:";
4
5
  const CURL_HEADERS_MARKER = "\n__CURL_HEADERS__:";
5
6
  let requestSequence = 0;
@@ -68,6 +69,12 @@ async function runCurlRequest(init, params) {
68
69
  "--write-out",
69
70
  `${CURL_STATUS_MARKER}%{http_code}${CURL_HEADERS_MARKER}%{header_json}`
70
71
  ];
72
+ if (params?.proxy?.enabled && params.proxy.url.trim()) {
73
+ args.push("--proxy", params.proxy.url.trim());
74
+ if (params.proxy.noProxy.trim()) {
75
+ args.push("--noproxy", params.proxy.noProxy.trim());
76
+ }
77
+ }
71
78
  for (const [key, value] of Object.entries(init.headers ?? {})) {
72
79
  args.push("--header", `${key}: ${value}`);
73
80
  }
@@ -148,12 +155,25 @@ async function runCurlRequest(init, params) {
148
155
  headers
149
156
  };
150
157
  }
158
+ async function loadNetworkProxySettings() {
159
+ try {
160
+ const settings = await loadSettings();
161
+ return settings.networkProxy;
162
+ } catch (error) {
163
+ console.warn("[http] failed to load network proxy settings", {
164
+ error: error instanceof Error ? error.message : String(error)
165
+ });
166
+ return void 0;
167
+ }
168
+ }
151
169
  async function requestText(init) {
152
170
  const requestId = nextRequestId();
171
+ const proxy = await loadNetworkProxySettings();
153
172
  const useCurlOnly = process.env.OAUTH_DEMO_USE_CURL === "1";
173
+ const useConfiguredProxy = !!proxy?.enabled && !!proxy.url.trim();
154
174
  const timeoutMs = init.timeoutMs;
155
175
  const signal = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : void 0;
156
- if (!useCurlOnly) {
176
+ if (!useCurlOnly && !useConfiguredProxy) {
157
177
  const startedAt = performance.now();
158
178
  const phases = {};
159
179
  try {
@@ -199,7 +219,8 @@ async function requestText(init) {
199
219
  }
200
220
  return runCurlRequest(init, {
201
221
  requestId,
202
- fallbackFrom: useCurlOnly ? void 0 : "fetch"
222
+ fallbackFrom: useCurlOnly || useConfiguredProxy ? void 0 : "fetch",
223
+ proxy
203
224
  });
204
225
  }
205
226
  export {
@@ -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) {
@@ -41,6 +41,36 @@ class ConfigService {
41
41
  await saveSettings(next);
42
42
  return next;
43
43
  }
44
+ async setNetworkProxy(params) {
45
+ const url = params.url?.trim() ?? "";
46
+ const noProxy = params.noProxy?.trim() || "localhost,127.0.0.1,::1";
47
+ if (params.enabled) {
48
+ if (!url) {
49
+ throw new Error("\u542F\u7528\u4EE3\u7406\u65F6\u5FC5\u987B\u586B\u5199\u4EE3\u7406\u5730\u5740\u3002");
50
+ }
51
+ let parsed;
52
+ try {
53
+ parsed = new URL(url);
54
+ } catch {
55
+ throw new Error("\u4EE3\u7406\u5730\u5740\u683C\u5F0F\u9519\u8BEF\uFF0C\u8BF7\u586B\u5199\u5B8C\u6574\u7684\u4EE3\u7406 URL\u3002");
56
+ }
57
+ const supportedProtocols = /* @__PURE__ */ new Set(["http:", "https:", "socks4:", "socks4a:", "socks5:", "socks5h:"]);
58
+ if (!supportedProtocols.has(parsed.protocol)) {
59
+ throw new Error("\u4EE3\u7406\u5730\u5740\u4EC5\u652F\u6301 http\u3001https\u3001socks4\u3001socks4a\u3001socks5 \u6216 socks5h\u3002");
60
+ }
61
+ }
62
+ const settings = await this.getSettings();
63
+ const next = {
64
+ ...settings,
65
+ networkProxy: {
66
+ enabled: params.enabled,
67
+ url,
68
+ noProxy
69
+ }
70
+ };
71
+ await saveSettings(next);
72
+ return next;
73
+ }
44
74
  async getServerConfig() {
45
75
  const settings = await this.getSettings();
46
76
  return settings.server;
@@ -6,11 +6,6 @@ const SUPPORTED_IMAGE_MODELS = /* @__PURE__ */ new Set([
6
6
  "gpt-image-1.5",
7
7
  "gpt-image-2"
8
8
  ]);
9
- const SUPPORTED_IMAGE_SIZES = /* @__PURE__ */ new Set([
10
- "1024x1024",
11
- "1024x1536",
12
- "1536x1024"
13
- ]);
14
9
  const SUPPORTED_IMAGE_QUALITIES = /* @__PURE__ */ new Set([
15
10
  "low",
16
11
  "medium",
@@ -62,11 +57,11 @@ function toImageGenerationEventOutput(value) {
62
57
  return null;
63
58
  }
64
59
  function normalizeReturnedSize(size, fallback) {
65
- if (typeof size === "string" && SUPPORTED_IMAGE_SIZES.has(size)) {
66
- return size;
60
+ if (typeof size === "string" && size.trim()) {
61
+ return size.trim();
67
62
  }
68
- if (typeof fallback === "string" && SUPPORTED_IMAGE_SIZES.has(fallback)) {
69
- return fallback;
63
+ if (typeof fallback === "string" && fallback.trim()) {
64
+ return fallback.trim();
70
65
  }
71
66
  return void 0;
72
67
  }
@@ -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
+ };
@@ -10,6 +10,11 @@ function createDefaultSettings() {
10
10
  version: 1,
11
11
  defaultProvider: "openai-codex",
12
12
  defaultModel: "gpt-5.4",
13
+ networkProxy: {
14
+ enabled: false,
15
+ url: "",
16
+ noProxy: "localhost,127.0.0.1,::1"
17
+ },
13
18
  server: {
14
19
  host: "0.0.0.0",
15
20
  port: 8787
@@ -26,6 +31,11 @@ async function loadSettings() {
26
31
  version: 1,
27
32
  defaultProvider: parsed.defaultProvider ?? defaults.defaultProvider,
28
33
  defaultModel: parsed.defaultModel ?? defaults.defaultModel,
34
+ networkProxy: {
35
+ enabled: parsed.networkProxy?.enabled ?? defaults.networkProxy.enabled,
36
+ url: parsed.networkProxy?.url ?? defaults.networkProxy.url,
37
+ noProxy: parsed.networkProxy?.noProxy ?? defaults.networkProxy.noProxy
38
+ },
29
39
  server: {
30
40
  host: parsed.server?.host ?? defaults.server.host,
31
41
  port: parsed.server?.port ?? defaults.server.port
@@ -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 {
@@ -990,6 +1027,21 @@ function renderAdminPage() {
990
1027
  color: var(--text-soft);
991
1028
  }
992
1029
 
1030
+ .checkbox-row {
1031
+ display: flex;
1032
+ align-items: center;
1033
+ gap: 10px;
1034
+ color: var(--text-soft);
1035
+ font-size: 13px;
1036
+ font-weight: 600;
1037
+ }
1038
+
1039
+ .checkbox-row input {
1040
+ width: 16px;
1041
+ height: 16px;
1042
+ accent-color: var(--brand);
1043
+ }
1044
+
993
1045
  .pre {
994
1046
  margin: 0;
995
1047
  padding: 14px;
@@ -1350,7 +1402,7 @@ function renderAdminPage() {
1350
1402
  <strong id="updatePanelTitle">\u53D1\u73B0\u65B0\u7248\u672C</strong>
1351
1403
  <span id="updatePanelDetail"></span>
1352
1404
  </div>
1353
- <code class="update-command" id="updatePanelCommand">npm install -g ai-zero-token@latest</code>
1405
+ <code class="update-command" id="updatePanelCommand">npm install -g ai-zero-token</code>
1354
1406
  </section>
1355
1407
 
1356
1408
  <section class="summary-grid" id="summaryGrid"></section>
@@ -1365,6 +1417,7 @@ function renderAdminPage() {
1365
1417
  </div>
1366
1418
  <div class="actions">
1367
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>
1368
1421
  </div>
1369
1422
  </div>
1370
1423
 
@@ -1383,8 +1436,8 @@ function renderAdminPage() {
1383
1436
  <option value="expiry-asc">\u6309\u8FC7\u671F\u65F6\u95F4</option>
1384
1437
  <option value="name-asc">\u6309\u90AE\u7BB1\u6392\u5E8F</option>
1385
1438
  </select>
1439
+ <span class="account-selected-count" id="selectedProfileCount">\u5DF2\u9009\u62E9 0 \u4E2A</span>
1386
1440
  </div>
1387
-
1388
1441
  <div class="account-grid" id="profileList"></div>
1389
1442
  </section>
1390
1443
 
@@ -1465,6 +1518,21 @@ function renderAdminPage() {
1465
1518
  </div>
1466
1519
  </div>
1467
1520
 
1521
+ <div class="field">
1522
+ <label class="checkbox-row" for="proxyEnabled">
1523
+ <input id="proxyEnabled" type="checkbox" />
1524
+ \u542F\u7528\u4E0A\u6E38\u4EE3\u7406
1525
+ </label>
1526
+ <label for="proxyUrl">\u4EE3\u7406\u5730\u5740</label>
1527
+ <input class="input" id="proxyUrl" type="text" placeholder="\u586B\u5199\u4F60\u7684\u4EE3\u7406\u5730\u5740" />
1528
+ <label for="proxyNoProxy">\u76F4\u8FDE\u5730\u5740</label>
1529
+ <input class="input" id="proxyNoProxy" type="text" placeholder="localhost,127.0.0.1,::1" />
1530
+ <p class="hint">\u542F\u7528\u540E\uFF0COAuth \u6362\u53D6 token\u3001\u6A21\u578B\u5237\u65B0\u548C\u63A5\u53E3\u8F6C\u53D1\u4F1A\u901A\u8FC7\u6B64\u4EE3\u7406\u8BBF\u95EE\u6D77\u5916\u4E0A\u6E38\u3002</p>
1531
+ <div class="actions">
1532
+ <button class="btn-primary" id="saveProxyBtn" type="button">\u4FDD\u5B58\u4EE3\u7406\u914D\u7F6E</button>
1533
+ </div>
1534
+ </div>
1535
+
1468
1536
  <div class="field">
1469
1537
  <label for="requestBody">\u8BF7\u6C42\u4F53 JSON</label>
1470
1538
  <textarea class="textarea" id="requestBody" spellcheck="false"></textarea>
@@ -1548,6 +1616,41 @@ function renderAdminPage() {
1548
1616
  </section>
1549
1617
  </div>
1550
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
+
1551
1654
  <div class="modal-backdrop" id="contactModal" aria-hidden="true">
1552
1655
  <section class="modal-card" role="dialog" aria-modal="true" aria-labelledby="contactModalTitle">
1553
1656
  <div class="modal-head">
@@ -1592,6 +1695,7 @@ function renderAdminPage() {
1592
1695
  status: "all",
1593
1696
  sort: "quota-desc",
1594
1697
  },
1698
+ selectedProfileIds: {},
1595
1699
  testerResultTab: "response",
1596
1700
  };
1597
1701
 
@@ -1629,6 +1733,7 @@ function renderAdminPage() {
1629
1733
  const imageCapabilityHint = document.getElementById("imageCapabilityHint");
1630
1734
  const runTestBtn = document.getElementById("runTestBtn");
1631
1735
  const toggleEmailBtn = document.getElementById("toggleEmailBtn");
1736
+ const accountModal = document.getElementById("accountModal");
1632
1737
  const contactModal = document.getElementById("contactModal");
1633
1738
  const imagePreviewModal = document.getElementById("imagePreviewModal");
1634
1739
  const contactBtn = document.getElementById("contactBtn");
@@ -1638,6 +1743,15 @@ function renderAdminPage() {
1638
1743
  const profileSearch = document.getElementById("profileSearch");
1639
1744
  const profileStatusFilter = document.getElementById("profileStatusFilter");
1640
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");
1752
+ const proxyEnabled = document.getElementById("proxyEnabled");
1753
+ const proxyUrl = document.getElementById("proxyUrl");
1754
+ const proxyNoProxy = document.getElementById("proxyNoProxy");
1641
1755
 
1642
1756
  function setBusy(button, busy) {
1643
1757
  if (button) {
@@ -2336,8 +2450,35 @@ function renderAdminPage() {
2336
2450
  return filtered;
2337
2451
  }
2338
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
+
2339
2478
  function renderProfiles(config) {
2340
2479
  const container = document.getElementById("profileList");
2480
+ syncSelectedProfiles(config);
2481
+ updateSelectedProfileControls();
2341
2482
  const profiles = getFilteredProfiles(config);
2342
2483
  const gridClass = profiles.length <= 0
2343
2484
  ? ""
@@ -2356,6 +2497,7 @@ function renderAdminPage() {
2356
2497
  }
2357
2498
 
2358
2499
  container.innerHTML = profiles.map(function (profile) {
2500
+ const selected = !!state.selectedProfileIds[profile.profileId];
2359
2501
  const isSingleProfile = profiles.length === 1;
2360
2502
  const health = getProfileHealth(profile);
2361
2503
  const planType = getPlanType(profile);
@@ -2385,6 +2527,7 @@ function renderAdminPage() {
2385
2527
  + '<span class="badge ' + escapeHtml(imageCapability.badgeClass) + '">' + escapeHtml(imageCapability.label) + "</span>"
2386
2528
  + "</div>"
2387
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>"
2388
2531
  + "</div>"
2389
2532
  + '<div class="account-metrics">'
2390
2533
  + '<div class="quota-row">'
@@ -2407,6 +2550,7 @@ function renderAdminPage() {
2407
2550
  + "</div>"
2408
2551
  + '<div class="account-actions">'
2409
2552
  + actionButton
2553
+ + '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
2410
2554
  + '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
2411
2555
  + "</div>"
2412
2556
  + "</article>";
@@ -2567,6 +2711,7 @@ function renderAdminPage() {
2567
2711
  ["Base URL", config.baseUrl],
2568
2712
  ["Provider", config.status.activeProvider || "openai-codex"],
2569
2713
  ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2714
+ ["\u4E0A\u6E38\u4EE3\u7406", config.settings.networkProxy && config.settings.networkProxy.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"],
2570
2715
  ["\u5F53\u524D\u7248\u672C", getVersionValue(config)],
2571
2716
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2572
2717
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
@@ -2585,6 +2730,7 @@ function renderAdminPage() {
2585
2730
  ["API Base URL", config.baseUrl],
2586
2731
  ["\u5F53\u524D\u8D26\u53F7", getProfileDisplayLabel(config.profile)],
2587
2732
  ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2733
+ ["\u4E0A\u6E38\u4EE3\u7406", config.settings.networkProxy && config.settings.networkProxy.enabled ? config.settings.networkProxy.url : "\u672A\u542F\u7528"],
2588
2734
  ["\u7248\u672C\u72B6\u6001", getVersionDetail(config)],
2589
2735
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2590
2736
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
@@ -2640,6 +2786,17 @@ function renderAdminPage() {
2640
2786
  hint.textContent = parts.join("\uFF0C") + "\u3002";
2641
2787
  }
2642
2788
 
2789
+ function renderProxySettings(config) {
2790
+ const proxy = config.settings.networkProxy || {
2791
+ enabled: false,
2792
+ url: "",
2793
+ noProxy: "localhost,127.0.0.1,::1",
2794
+ };
2795
+ proxyEnabled.checked = !!proxy.enabled;
2796
+ proxyUrl.value = proxy.url || "";
2797
+ proxyNoProxy.value = proxy.noProxy || "localhost,127.0.0.1,::1";
2798
+ }
2799
+
2643
2800
  function syncHero(config) {
2644
2801
  const profileText = config.profile
2645
2802
  ? "\u5F53\u524D\u8D26\u53F7\u4E3A " + getProfileDisplayLabel(config.profile) + "\uFF0C\u5957\u9910 " + getPlanType(config.profile) + "\uFF0C\u53EF\u5728\u53F3\u4FA7\u5B8C\u6210\u6A21\u578B\u5207\u6362\u548C\u63A5\u53E3\u8C03\u8BD5\u3002"
@@ -2676,6 +2833,7 @@ function renderAdminPage() {
2676
2833
  renderProfiles(config);
2677
2834
  renderModelOptions(config);
2678
2835
  renderModelCatalogStatus(config);
2836
+ renderProxySettings(config);
2679
2837
  renderUpdatePanel(config);
2680
2838
  renderEndpoints(config);
2681
2839
  renderServiceInfo(config);
@@ -2785,7 +2943,7 @@ function renderAdminPage() {
2785
2943
  }
2786
2944
 
2787
2945
  async function login() {
2788
- const button = document.getElementById("loginBtn");
2946
+ const button = oauthLoginBtn;
2789
2947
  setBusy(button, true);
2790
2948
  authStatus.textContent = "\u6B63\u5728\u65B0\u589E\u8D26\u53F7\u3001\u7B49\u5F85 OAuth \u5B8C\u6210\uFF0C\u5E76\u540C\u6B65\u989D\u5EA6\u4FE1\u606F...";
2791
2949
  try {
@@ -2798,6 +2956,7 @@ function renderAdminPage() {
2798
2956
  if (config.profile && config.profile.quota) {
2799
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);
2800
2958
  }
2959
+ closeAccountModal();
2801
2960
  } catch (error) {
2802
2961
  authStatus.textContent = error.message;
2803
2962
  } finally {
@@ -2824,6 +2983,11 @@ function renderAdminPage() {
2824
2983
  }
2825
2984
 
2826
2985
  async function runProfileAction(action, profileId, button) {
2986
+ if (action === "export") {
2987
+ await exportProfile(profileId, button);
2988
+ return;
2989
+ }
2990
+
2827
2991
  setBusy(button, true);
2828
2992
  authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
2829
2993
  try {
@@ -2856,6 +3020,116 @@ function renderAdminPage() {
2856
3020
  }
2857
3021
  }
2858
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
+
2859
3133
  async function saveModel() {
2860
3134
  const button = document.getElementById("saveModelBtn");
2861
3135
  const select = document.getElementById("defaultModel");
@@ -2880,6 +3154,33 @@ function renderAdminPage() {
2880
3154
  }
2881
3155
  }
2882
3156
 
3157
+ async function saveProxy() {
3158
+ const button = document.getElementById("saveProxyBtn");
3159
+ setBusy(button, true);
3160
+ authStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u4EE3\u7406\u914D\u7F6E...";
3161
+ try {
3162
+ const config = await fetchJson("/_gateway/admin/settings", {
3163
+ method: "PUT",
3164
+ headers: {
3165
+ "Content-Type": "application/json",
3166
+ },
3167
+ body: formatJson({
3168
+ networkProxy: {
3169
+ enabled: proxyEnabled.checked,
3170
+ url: proxyUrl.value,
3171
+ noProxy: proxyNoProxy.value,
3172
+ },
3173
+ }),
3174
+ });
3175
+ renderConfig(config);
3176
+ authStatus.textContent = proxyEnabled.checked ? "\u4EE3\u7406\u914D\u7F6E\u5DF2\u542F\u7528\u3002" : "\u4EE3\u7406\u914D\u7F6E\u5DF2\u5173\u95ED\u3002";
3177
+ } catch (error) {
3178
+ authStatus.textContent = error.message;
3179
+ } finally {
3180
+ setBusy(button, false);
3181
+ }
3182
+ }
3183
+
2883
3184
  async function refreshModels() {
2884
3185
  const button = document.getElementById("refreshModelsBtn");
2885
3186
  setBusy(button, true);
@@ -3007,6 +3308,16 @@ function renderAdminPage() {
3007
3308
  }
3008
3309
  }
3009
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
+
3010
3321
  function openContactModal() {
3011
3322
  contactModal.classList.add("is-open");
3012
3323
  contactModal.setAttribute("aria-hidden", "false");
@@ -3017,7 +3328,7 @@ function renderAdminPage() {
3017
3328
  contactModal.setAttribute("aria-hidden", "true");
3018
3329
  }
3019
3330
 
3020
- document.getElementById("loginBtn").addEventListener("click", login);
3331
+ document.getElementById("loginBtn").addEventListener("click", openAccountModal);
3021
3332
  document.getElementById("refreshBtn").addEventListener("click", function () {
3022
3333
  authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
3023
3334
  refreshConfig({
@@ -3029,11 +3340,17 @@ function renderAdminPage() {
3029
3340
  });
3030
3341
  });
3031
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);
3032
3348
  contactBtn.addEventListener("click", openContactModal);
3033
3349
  document.getElementById("closeContactBtn").addEventListener("click", closeContactModal);
3034
3350
  document.getElementById("closeImagePreviewBtn").addEventListener("click", closeImagePreviewModal);
3035
3351
  document.getElementById("refreshModelsBtn").addEventListener("click", refreshModels);
3036
3352
  document.getElementById("saveModelBtn").addEventListener("click", saveModel);
3353
+ document.getElementById("saveProxyBtn").addEventListener("click", saveProxy);
3037
3354
  runTestBtn.addEventListener("click", runTest);
3038
3355
  document.querySelectorAll("[data-result-tab]").forEach(function (button) {
3039
3356
  button.addEventListener("click", function () {
@@ -3049,6 +3366,10 @@ function renderAdminPage() {
3049
3366
  });
3050
3367
 
3051
3368
  document.getElementById("profileList").addEventListener("click", function (event) {
3369
+ if (event.target.closest("[data-profile-select]")) {
3370
+ return;
3371
+ }
3372
+
3052
3373
  const button = event.target.closest("[data-profile-action]");
3053
3374
  if (!button) {
3054
3375
  return;
@@ -3063,6 +3384,25 @@ function renderAdminPage() {
3063
3384
  runProfileAction(action, profileId, button);
3064
3385
  });
3065
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
+
3066
3406
  document.getElementById("activateCurrentBtn").addEventListener("click", function () {
3067
3407
  if (!state.config || !state.config.profile || !state.config.profile.profileId) {
3068
3408
  return;
@@ -3134,6 +3474,12 @@ function renderAdminPage() {
3134
3474
  }
3135
3475
  });
3136
3476
 
3477
+ accountModal.addEventListener("click", function (event) {
3478
+ if (event.target === accountModal) {
3479
+ closeAccountModal();
3480
+ }
3481
+ });
3482
+
3137
3483
  imagePreviewModal.addEventListener("click", function (event) {
3138
3484
  if (event.target === imagePreviewModal) {
3139
3485
  closeImagePreviewModal();
@@ -3147,6 +3493,9 @@ function renderAdminPage() {
3147
3493
  if (event.key === "Escape" && contactModal.classList.contains("is-open")) {
3148
3494
  closeContactModal();
3149
3495
  }
3496
+ if (event.key === "Escape" && accountModal.classList.contains("is-open")) {
3497
+ closeAccountModal();
3498
+ }
3150
3499
  });
3151
3500
 
3152
3501
  setTesterResultTab(state.testerResultTab);
@@ -67,17 +67,30 @@ const chatCompletionsBodySchema = z.object({
67
67
  user: z.string().optional()
68
68
  }).passthrough();
69
69
  const settingsUpdateSchema = z.object({
70
- defaultModel: z.string().min(1)
70
+ defaultModel: z.string().min(1).optional(),
71
+ networkProxy: z.object({
72
+ enabled: z.boolean(),
73
+ url: z.string().optional(),
74
+ noProxy: z.string().optional()
75
+ }).optional()
71
76
  });
72
77
  const profileActionSchema = z.object({
73
78
  profileId: z.string().min(1)
74
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
+ });
75
88
  const imageGenerationsBodySchema = z.object({
76
89
  prompt: z.string().min(1),
77
90
  model: z.string().optional(),
78
91
  n: z.number().int().positive().optional(),
79
92
  quality: z.enum(["low", "medium", "high", "auto"]).optional(),
80
- size: z.enum(["1024x1024", "1024x1536", "1536x1024", "auto"]).optional(),
93
+ size: z.string().min(1).optional(),
81
94
  background: z.enum(["transparent", "opaque", "auto"]).optional(),
82
95
  output_format: z.enum(["png", "webp", "jpeg"]).optional(),
83
96
  output_compression: z.number().int().min(0).max(100).optional(),
@@ -462,6 +475,49 @@ function createApp(params) {
462
475
  await ctx.authService.removeProfile(parsed.data.profileId);
463
476
  return buildAdminConfig(request);
464
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
+ });
465
521
  app.put("/_gateway/admin/settings", async (request, reply) => {
466
522
  const parsed = settingsUpdateSchema.safeParse(request.body);
467
523
  if (!parsed.success) {
@@ -473,7 +529,12 @@ function createApp(params) {
473
529
  }
474
530
  };
475
531
  }
476
- await ctx.configService.setDefaultModel(parsed.data.defaultModel);
532
+ if (parsed.data.defaultModel) {
533
+ await ctx.configService.setDefaultModel(parsed.data.defaultModel);
534
+ }
535
+ if (parsed.data.networkProxy) {
536
+ await ctx.configService.setNetworkProxy(parsed.data.networkProxy);
537
+ }
477
538
  return buildAdminConfig(request);
478
539
  });
479
540
  app.get("/v1/models", async () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-zero-token",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Local-first OpenAI-compatible AI CLI and gateway with Codex OAuth, multi-account management, and gpt-image-2 image generation.",
5
5
  "license": "MIT",
6
6
  "type": "module",