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 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@latest</code>
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 = document.getElementById("loginBtn");
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", login);
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);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-zero-token",
3
- "version": "1.0.5",
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",