ai-zero-token 1.0.5 → 1.0.7

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.7 - 2026-04-29
4
+
5
+ - Added JSON `POST /v1/images/edits` support for image-to-image workflows with URL, base64 data URL, or raw base64 image references.
6
+ - Added management-page Edits test examples and documented the JSON image editing request format.
7
+ - Added `id_token` persistence for login, refresh, import, export, and account transfer JSON.
8
+ - Added "Apply to Codex" account action to back up and update local `~/.codex/auth.json` for new Codex sessions.
9
+ - Updated the local AI-Zero-Token skill documentation package with image editing and Codex account switching guidance.
10
+
11
+ ## 1.0.6 - 2026-04-28
12
+
13
+ - Added account JSON import/export support in the management page.
14
+ - Added batch account import from a single object, an array, or a `profiles` bundle.
15
+ - Added selectable batch export for checked accounts in the management page.
16
+ - Added `azt profiles import/export` CLI commands for account transfer workflows.
17
+ - Added an account import template endpoint for quick JSON format reference.
18
+
3
19
  ## 1.0.5 - 2026-04-27
4
20
 
5
21
  - Added management-page proxy configuration for upstream requests, persisted in local settings.
package/README.md CHANGED
@@ -7,6 +7,7 @@ AI Zero Token 是一个本地优先的单用户 AI CLI 和本地网关。
7
7
  它把账号授权能力整理成 OpenAI 风格接口,重点是把图片生成能力也代理出来:
8
8
 
9
9
  - `POST /v1/images/generations`
10
+ - `POST /v1/images/edits`
10
11
  - `POST /v1/responses`
11
12
  - `POST /v1/chat/completions`
12
13
  - `GET /v1/models`
@@ -15,9 +16,10 @@ AI Zero Token 是一个本地优先的单用户 AI CLI 和本地网关。
15
16
 
16
17
  ## 这次迭代亮点
17
18
 
18
- - 直接代理 `gpt-image-2`,把图片生成能力暴露成 OpenAI 风格 `images.generations` 接口
19
+ - 直接代理 `gpt-image-2`,把图片生成能力暴露成 OpenAI 风格 `images.generations` / `images.edits` 接口
19
20
  - 启动 `azt start` 后即可获得本地管理页和本地网关,适合脚本、前端和自动化流程接入
20
21
  - 支持多账号保存、切换当前账号、查看账号套餐 plan,以及当前账号是否支持生图
22
+ - 支持账号 JSON 批量导入/勾选导出,并可一键把已保存账号应用到本机 Codex
21
23
  - 模型列表会优先同步本机 `~/.codex/models_cache.json`,不需要每次为新模型重新 build
22
24
  - 管理页会每 10 分钟自动同步额度快照和版本状态,并提示当前版本是否可更新
23
25
  - `free` 账号会在管理页直接预警,并在网关层明确拦截生图请求
@@ -220,6 +222,9 @@ azt start
220
222
  - 查看当前账号、已保存账号列表、过期时间、token 摘要
221
223
  - 查看账号套餐 plan 和当前账号是否支持生图
222
224
  - 在多个已保存账号之间切换当前使用账号
225
+ - 在“新增账号”里选择 OAuth 登录,或粘贴外部账号 JSON 批量导入
226
+ - 导出单个账号,或勾选多个账号后批量导出所选账号 JSON
227
+ - 将任一已保存账号“应用到 Codex”,自动备份并更新本机 `~/.codex/auth.json`
223
228
  - 删除单个本地账号,或一键清空全部本地账号
224
229
  - 切换默认模型
225
230
  - 测试 `models` / `responses` / `chat.completions`
@@ -227,6 +232,10 @@ azt start
227
232
 
228
233
  管理页里邮箱默认脱敏显示,需要手动点击“查看邮箱”才会显示明文。
229
234
 
235
+ 导出的账号 JSON 包含完整 `access_token` 和 `refresh_token`,等同于账号登录凭据,只适合在可信环境中传递。
236
+
237
+ 账号卡片里的“应用到 Codex”会把该账号的 `access_token`、`refresh_token`、`id_token` 和 `account_id` 写入本机 Codex 的 `~/.codex/auth.json`。写入前会自动备份原文件;新开的 Codex 会话会使用该账号。
238
+
230
239
  如果当前网络访问海外上游不稳定,可以在管理页的“接口测试 / 系统设置”区域启用“上游代理”,并填写你自己的代理地址。保存后,OAuth 换取 token、模型刷新和接口转发都会通过该代理访问上游;本地管理页和 `127.0.0.1` 默认保持直连。
231
240
 
232
241
  默认监听地址:
@@ -318,9 +327,28 @@ curl http://127.0.0.1:8787/v1/images/generations \
318
327
  响应会返回 OpenAI 同类型结构的 `data[].b64_json`。如果你在管理页里测试,这张图片会直接显示预览。
319
328
  如果请求里不显式传 `model`,当前默认会使用 `gpt-image-2`。
320
329
 
330
+ JSON 版 `images.edits` 支持通过 URL 或 base64 data URL 传参考图:
331
+
332
+ ```bash
333
+ curl http://127.0.0.1:8787/v1/images/edits \
334
+ -H "content-type: application/json" \
335
+ -d '{
336
+ "model": "gpt-image-2",
337
+ "prompt": "参考这张图,生成一张更适合科技产品广告的版本。",
338
+ "images": [
339
+ {
340
+ "image_url": "data:image/png;base64,替换为你的图片base64"
341
+ }
342
+ ],
343
+ "size": "1024x1024",
344
+ "quality": "low",
345
+ "response_format": "b64_json"
346
+ }'
347
+ ```
348
+
321
349
  生图能力和账号套餐有关:
322
350
 
323
- - `plus` 或更高套餐账号可正常调用 `images.generations`
351
+ - `plus` 或更高套餐账号可正常调用 `images.generations` / `images.edits`
324
352
  - `free` 账号不支持生图,网关会直接返回明确错误,而不是继续请求上游
325
353
  - 管理页会显示当前账号的 `plan` 和“生图能力”状态
326
354
  - 当当前账号是 `free` 且你选中 `Images` 测试时,“发送请求”按钮会被直接禁用
@@ -341,6 +369,7 @@ curl http://127.0.0.1:8787/v1/images/generations \
341
369
  - `POST /v1/responses`
342
370
  - `POST /v1/chat/completions`
343
371
  - `POST /v1/images/generations`
372
+ - `POST /v1/images/edits`
344
373
 
345
374
  ### 7. 当前支持的主要参数
346
375
 
@@ -390,6 +419,24 @@ curl http://127.0.0.1:8787/v1/images/generations \
390
419
  - `response_format`
391
420
  - `user`
392
421
 
422
+ `POST /v1/images/edits` 当前主要支持 JSON 请求:
423
+
424
+ - `prompt`
425
+ - `images`
426
+ - `image`
427
+ - `model`
428
+ - `n`
429
+ - `size`
430
+ - `quality`
431
+ - `background`
432
+ - `output_format`
433
+ - `output_compression`
434
+ - `moderation`
435
+ - `response_format`
436
+ - `user`
437
+
438
+ 其中 `images` / `image` 支持 `image_url`,可以是 `https://...` URL、`data:image/...;base64,...`,或裸 base64 字符串。
439
+
393
440
  ### 8. 当前限制
394
441
 
395
442
  - `stream=true` 目前只识别,不返回真实流式结果
@@ -398,12 +445,22 @@ curl http://127.0.0.1:8787/v1/images/generations \
398
445
  - `images.generations` 暂不支持 `n > 1`
399
446
  - `images.generations` 当前只返回 `b64_json`,暂不支持托管图片 `url`
400
447
  - `images.generations` 当前只透传 GPT Image 路径,不兼容 DALL·E 专有参数
401
- - `images.generations` 对账号套餐有要求;`free` 账号会被网关直接拦截并返回“不支持图片生成”
448
+ - `images.edits` 当前只支持 `application/json`,暂不支持 `multipart/form-data`、`mask` 和 `file_id`
449
+ - `images.generations` / `images.edits` 对账号套餐有要求;`free` 账号会被网关直接拦截并返回“不支持图片生成”
402
450
  - 网关当前默认面向本地单用户使用
403
451
 
404
452
  ## 兼容说明
405
453
 
406
- 代码里仍然保留了 `login`、`status`、`models`、`ask`、`serve`、`clear` 等 CLI 命令,主要用于调试、兼容和后续扩展。
454
+ 代码里仍然保留了 `login`、`status`、`models`、`profiles`、`ask`、`serve`、`clear` 等 CLI 命令,主要用于调试、兼容和后续扩展。
455
+
456
+ 账号 JSON 也可以通过 CLI 导入/导出:
457
+
458
+ ```bash
459
+ azt profiles import ./profile.json
460
+ azt profiles export active ./profile.json
461
+ azt profiles export all ./profiles.json
462
+ azt profiles export "openai-codex:<accountId>" ./profile.json
463
+ ```
407
464
 
408
465
  README 不再把这些命令作为推荐使用方式。默认使用路径就是:
409
466
 
@@ -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;
@@ -80,7 +80,7 @@ function parseAuthorizationInput(value) {
80
80
  }
81
81
  return { code: trimmed };
82
82
  }
83
- function extractProfile(accessToken, refreshToken, expires) {
83
+ function extractProfile(accessToken, refreshToken, expires, idToken) {
84
84
  const payload = decodeJwtPayload(accessToken);
85
85
  const authClaim = payload?.[JWT_CLAIM_PATH];
86
86
  const accountId = authClaim?.chatgpt_account_id;
@@ -94,6 +94,7 @@ function extractProfile(accessToken, refreshToken, expires) {
94
94
  mode: "oauth_account",
95
95
  access: accessToken,
96
96
  refresh: refreshToken,
97
+ idToken,
97
98
  expires,
98
99
  accountId,
99
100
  email
@@ -124,6 +125,7 @@ async function exchangeAuthorizationCode(code, verifier) {
124
125
  return {
125
126
  access: json.access_token,
126
127
  refresh: json.refresh_token,
128
+ idToken: json.id_token,
127
129
  expires: Date.now() + json.expires_in * 1e3
128
130
  };
129
131
  }
@@ -150,7 +152,8 @@ async function refreshOpenAICodexToken(profile) {
150
152
  return extractProfile(
151
153
  json.access_token,
152
154
  json.refresh_token,
153
- Date.now() + json.expires_in * 1e3
155
+ Date.now() + json.expires_in * 1e3,
156
+ json.id_token ?? profile.idToken
154
157
  );
155
158
  }
156
159
  function tryOpenBrowser(url) {
@@ -284,7 +287,7 @@ async function loginOpenAICodex() {
284
287
  console.log("\u5DF2\u6536\u5230\u6388\u6743\u56DE\u8C03\uFF0C\u6B63\u5728\u4EA4\u6362 access token...");
285
288
  const token = await exchangeAuthorizationCode(code, verifier);
286
289
  console.log("token \u4EA4\u6362\u6210\u529F\uFF0C\u6B63\u5728\u89E3\u6790\u8D26\u53F7\u4FE1\u606F...");
287
- return extractProfile(token.access, token.refresh, token.expires);
290
+ return extractProfile(token.access, token.refresh, token.expires, token.idToken);
288
291
  } finally {
289
292
  callbackServer.close();
290
293
  }
@@ -13,6 +13,17 @@ 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
+ applyProfileToCodexAuth,
18
+ getCodexAuthStatus
19
+ } from "../store/codex-auth-store.js";
20
+ import {
21
+ exportProfilesToJson,
22
+ exportProfileToJson,
23
+ getProfileImportTemplate,
24
+ importProfileFromJson,
25
+ importProfilesFromJson
26
+ } from "../store/profile-transfer.js";
16
27
  class AuthService {
17
28
  constructor(configService) {
18
29
  this.configService = configService;
@@ -50,6 +61,53 @@ class AuthService {
50
61
  await saveProfile(profile);
51
62
  return this.toManagedProfile(profile);
52
63
  }
64
+ async importProfile(value, provider = "openai-codex") {
65
+ if (provider !== "openai-codex") {
66
+ throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
67
+ }
68
+ const profile = importProfileFromJson(value);
69
+ await saveProfile(profile);
70
+ return this.toManagedProfile(profile);
71
+ }
72
+ async importProfiles(value, provider = "openai-codex") {
73
+ if (provider !== "openai-codex") {
74
+ throw new Error(`\u6682\u4E0D\u652F\u6301 provider: ${provider}`);
75
+ }
76
+ const profiles = importProfilesFromJson(value);
77
+ for (const profile of profiles) {
78
+ await saveProfile(profile);
79
+ }
80
+ return profiles.map((profile) => this.toManagedProfile(profile));
81
+ }
82
+ async exportProfile(profileId, provider = "openai-codex") {
83
+ const profiles = await listProfiles();
84
+ const activeProfile = await this.getActiveProfile(provider);
85
+ const targetProfileId = profileId?.trim() || activeProfile?.profileId;
86
+ const profile = profiles.find((item) => item.provider === provider && item.profileId === targetProfileId);
87
+ if (!profile) {
88
+ 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");
89
+ }
90
+ return exportProfileToJson(profile);
91
+ }
92
+ async exportProfiles(profileIds, provider = "openai-codex") {
93
+ const profiles = await listProfiles();
94
+ const idSet = profileIds && profileIds.length > 0 ? new Set(profileIds.map((item) => item.trim()).filter(Boolean)) : null;
95
+ const selected = profiles.filter((item) => item.provider === provider).filter((item) => !idSet || idSet.has(item.profileId));
96
+ if (selected.length === 0) {
97
+ throw new Error("\u6CA1\u6709\u627E\u5230\u53EF\u5BFC\u51FA\u7684\u8D26\u53F7\u3002");
98
+ }
99
+ return exportProfilesToJson(selected);
100
+ }
101
+ getProfileImportTemplate() {
102
+ return getProfileImportTemplate();
103
+ }
104
+ async getCodexStatus() {
105
+ return getCodexAuthStatus();
106
+ }
107
+ async applyProfileToCodex(profileId, provider = "openai-codex") {
108
+ const profile = await this.requireFreshProfileWithIdToken(profileId, provider);
109
+ return applyProfileToCodexAuth(profile);
110
+ }
53
111
  async getActiveProfile(provider = "openai-codex") {
54
112
  const profile = await getActiveProfile();
55
113
  if (!profile || profile.provider !== provider) {
@@ -105,6 +163,22 @@ class AuthService {
105
163
  await saveProfile(refreshed);
106
164
  return this.toManagedProfile(refreshed);
107
165
  }
166
+ async requireFreshProfileWithIdToken(profileId, provider = "openai-codex") {
167
+ const profiles = await listProfiles();
168
+ const profile = profiles.find((item) => item.provider === provider && item.profileId === profileId);
169
+ if (!profile) {
170
+ throw new Error(`\u6CA1\u6709\u627E\u5230\u8D26\u53F7: ${profileId}`);
171
+ }
172
+ if (profile.idToken && Date.now() < profile.expires) {
173
+ return this.toManagedProfile(profile);
174
+ }
175
+ const refreshed = await refreshOpenAICodexToken(profile);
176
+ await saveProfile(refreshed);
177
+ if (!refreshed.idToken) {
178
+ throw new Error("\u5237\u65B0 token \u6210\u529F\uFF0C\u4F46\u4E0A\u6E38\u6CA1\u6709\u8FD4\u56DE id_token\u3002");
179
+ }
180
+ return this.toManagedProfile(refreshed);
181
+ }
108
182
  async logoutAll() {
109
183
  await clearStore();
110
184
  }
@@ -280,7 +280,8 @@ class ImageService {
280
280
  background: request.background ?? "default",
281
281
  outputFormat: request.outputFormat ?? "default",
282
282
  outputCompression: typeof request.outputCompression === "number" ? request.outputCompression : void 0,
283
- moderation: request.moderation ?? "default"
283
+ moderation: request.moderation ?? "default",
284
+ inputImageCount: request.inputImages?.length ?? 0
284
285
  };
285
286
  console.info("[gateway:image] upstream request", requestSummary);
286
287
  const tool = {
@@ -305,6 +306,9 @@ class ImageService {
305
306
  if (request.moderation) {
306
307
  tool.moderation = request.moderation;
307
308
  }
309
+ if (request.inputImages && request.inputImages.length > 0) {
310
+ tool.action = "edit";
311
+ }
308
312
  for (let attempt = 1; attempt <= IMAGE_GENERATION_MAX_ATTEMPTS; attempt += 1) {
309
313
  let result;
310
314
  try {
@@ -320,7 +324,11 @@ class ImageService {
320
324
  {
321
325
  type: "input_text",
322
326
  text: request.prompt
323
- }
327
+ },
328
+ ...(request.inputImages ?? []).map((image) => ({
329
+ type: "input_image",
330
+ image_url: image.imageUrl
331
+ }))
324
332
  ]
325
333
  }
326
334
  ],
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ function getCodexHomeDir() {
6
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
7
+ }
8
+ function getCodexAuthPath() {
9
+ return path.join(getCodexHomeDir(), "auth.json");
10
+ }
11
+ function createBackupSuffix() {
12
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13
+ }
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ async function readCodexAuth() {
18
+ try {
19
+ const raw = await fs.readFile(getCodexAuthPath(), "utf8");
20
+ const parsed = JSON.parse(raw);
21
+ return isRecord(parsed) ? parsed : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ async function getCodexAuthStatus() {
27
+ const authPath = getCodexAuthPath();
28
+ const auth = await readCodexAuth();
29
+ if (!auth) {
30
+ return {
31
+ path: authPath,
32
+ exists: false,
33
+ hasIdToken: false
34
+ };
35
+ }
36
+ const tokens = isRecord(auth.tokens) ? auth.tokens : {};
37
+ return {
38
+ path: authPath,
39
+ exists: true,
40
+ accountId: typeof tokens.account_id === "string" ? tokens.account_id : void 0,
41
+ hasIdToken: typeof tokens.id_token === "string" && tokens.id_token.length > 0,
42
+ lastRefresh: typeof auth.last_refresh === "string" ? auth.last_refresh : void 0
43
+ };
44
+ }
45
+ async function applyProfileToCodexAuth(profile) {
46
+ if (!profile.idToken) {
47
+ throw new Error("\u5F53\u524D\u8D26\u53F7\u7F3A\u5C11 id_token\u3002\u8BF7\u5148\u5237\u65B0\u8D26\u53F7 token \u6216\u91CD\u65B0\u5BFC\u5165\u5305\u542B id_token \u7684\u8D26\u53F7 JSON\u3002");
48
+ }
49
+ const authPath = getCodexAuthPath();
50
+ const codexHomeDir = path.dirname(authPath);
51
+ await fs.mkdir(codexHomeDir, { recursive: true });
52
+ let backupPath;
53
+ try {
54
+ await fs.access(authPath);
55
+ backupPath = `${authPath}.azt-backup-${createBackupSuffix()}`;
56
+ await fs.copyFile(authPath, backupPath);
57
+ } catch {
58
+ backupPath = void 0;
59
+ }
60
+ const authFile = {
61
+ auth_mode: "chatgpt",
62
+ OPENAI_API_KEY: null,
63
+ tokens: {
64
+ id_token: profile.idToken,
65
+ access_token: profile.access,
66
+ refresh_token: profile.refresh,
67
+ account_id: profile.accountId
68
+ },
69
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString()
70
+ };
71
+ const tmpPath = `${authPath}.tmp-${process.pid}`;
72
+ await fs.writeFile(tmpPath, `${JSON.stringify(authFile, null, 2)}
73
+ `, {
74
+ encoding: "utf8",
75
+ mode: 384
76
+ });
77
+ await fs.rename(tmpPath, authPath);
78
+ await fs.chmod(authPath, 384);
79
+ return {
80
+ path: authPath,
81
+ exists: true,
82
+ accountId: profile.accountId,
83
+ hasIdToken: true,
84
+ lastRefresh: authFile.last_refresh,
85
+ backupPath,
86
+ appliedProfileId: profile.profileId,
87
+ appliedEmail: profile.email
88
+ };
89
+ }
90
+ export {
91
+ applyProfileToCodexAuth,
92
+ getCodexAuthPath,
93
+ getCodexAuthStatus
94
+ };
@@ -0,0 +1,146 @@
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
+ const idToken = getString(value.id_token) ?? getString(value.idToken);
71
+ if (!access || !refresh) {
72
+ throw new Error("\u5BFC\u5165\u5931\u8D25: \u7F3A\u5C11 access_token/access \u6216 refresh_token/refresh\u3002");
73
+ }
74
+ const payload = decodeJwtPayload(access);
75
+ const accountId = extractAccountId(payload, value.account_id ?? value.accountId);
76
+ const email = extractEmail(payload, value.email);
77
+ const expires = parseExpiry(value, payload);
78
+ return {
79
+ provider: "openai-codex",
80
+ profileId: `openai-codex:${accountId}`,
81
+ mode: "oauth_account",
82
+ access,
83
+ refresh,
84
+ idToken,
85
+ expires,
86
+ accountId,
87
+ email
88
+ };
89
+ }
90
+ function importProfilesFromJson(value) {
91
+ const items = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.profiles) ? value.profiles : [value];
92
+ return items.map((item, index) => {
93
+ try {
94
+ return importProfileFromJson(item);
95
+ } catch (error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ throw new Error(`\u7B2C ${index + 1} \u4E2A\u8D26\u53F7${message.startsWith("\u5BFC\u5165\u5931\u8D25") ? message : `\u5BFC\u5165\u5931\u8D25: ${message}`}`);
98
+ }
99
+ });
100
+ }
101
+ function exportProfileToJson(profile) {
102
+ return {
103
+ type: "codex",
104
+ access_token: profile.access,
105
+ refresh_token: profile.refresh,
106
+ id_token: profile.idToken,
107
+ expired: new Date(profile.expires).toISOString(),
108
+ email: profile.email,
109
+ account_id: profile.accountId,
110
+ profile_id: profile.profileId,
111
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
112
+ };
113
+ }
114
+ function exportProfilesToJson(profiles) {
115
+ return {
116
+ type: "codex_profiles",
117
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
118
+ profiles: profiles.map((profile) => exportProfileToJson(profile))
119
+ };
120
+ }
121
+ function getProfileImportTemplate() {
122
+ return {
123
+ type: "codex_profiles",
124
+ exported_at: (/* @__PURE__ */ new Date(0)).toISOString(),
125
+ profiles: [
126
+ {
127
+ type: "codex",
128
+ access_token: "eyJ...access_token",
129
+ refresh_token: "rt_...",
130
+ id_token: "eyJ...id_token",
131
+ expired: "2026-05-04T22:13:00.000Z",
132
+ email: "user@example.com",
133
+ account_id: "\u53EF\u9009\uFF0C\u901A\u5E38\u4F1A\u4ECE access_token \u81EA\u52A8\u89E3\u6790",
134
+ profile_id: "\u53EF\u9009\uFF0C\u5BFC\u5165\u65F6\u4F1A\u6309 account_id \u81EA\u52A8\u751F\u6210",
135
+ exported_at: (/* @__PURE__ */ new Date(0)).toISOString()
136
+ }
137
+ ]
138
+ };
139
+ }
140
+ export {
141
+ exportProfileToJson,
142
+ exportProfilesToJson,
143
+ getProfileImportTemplate,
144
+ importProfileFromJson,
145
+ importProfilesFromJson
146
+ };