@foxden-app/foxclaw 0.4.10 → 0.4.11
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 +12 -0
- package/README.md +7 -5
- package/README_EN.md +6 -4
- package/dist/controller/controller.d.ts +1 -0
- package/dist/controller/controller.js +323 -57
- package/dist/i18n.d.ts +42 -4
- package/dist/i18n.js +42 -4
- package/dist/store/database.d.ts +4 -1
- package/dist/store/database.js +23 -2
- package/docs/user-manual.md +15 -8
- package/docs/zh/user-manual.md +15 -8
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
|
|
4
4
|
|
|
5
|
+
## 0.4.11 - 2026-06-02
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- `/auth` 面板改为每页 8 个候选,新增翻页、`全部 / 已启用 / 需关注` 筛选、文件名搜索和直接跳页命令,支持管理较大的本地候选清单。
|
|
9
|
+
- 额度快照保存真实窗口时长与套餐类型,面板不再假定所有账号都有 5 小时和 7 天窗口;单一月额度账号可显示为 `30d:97`。
|
|
10
|
+
- 候选摘要新增正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key 和无效 auth 文件状态;超过约 8 天未刷新只作为维护提醒,不会触发批量保活刷新。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- Paginated `/auth` at 8 candidates per page and added paging, `All / Enabled / Attention` filters, filename search, and direct page commands for larger local inventories.
|
|
14
|
+
- Persisted observed quota-window lengths and plan types so the panel no longer assumes every account has 5-hour and 7-day windows; a single monthly allowance can render as `30d:97`.
|
|
15
|
+
- Added ready, low-quota, exhausted, unknown, not-recently-refreshed, API-key, and invalid-auth-file summaries. The roughly 8-day stale hint remains informational and never triggers bulk keepalive refreshes.
|
|
16
|
+
|
|
5
17
|
## 0.4.10 - 2026-06-02
|
|
6
18
|
|
|
7
19
|
### 中文
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex,
|
|
|
16
16
|
|
|
17
17
|
1. **开源且接口完整** — Codex 是 OpenAI 开源的 CLI agent,并提供 `codex app-server`。FoxClaw 不是模拟终端输入,而是通过更完整的接口读取线程、切换模型、处理审批和恢复会话。
|
|
18
18
|
2. **当前编码体验强** — FoxClaw 直接读取你本机 Codex 可用的模型列表;如果你的 Codex 环境已经有 GPT-5.5,就可以在手机端选择和使用它。对高强度编程工作流来说,Codex/GPT-5.5 的体感已经是很多人优先选择它的原因。
|
|
19
|
-
3. **多账号额度切换** —
|
|
19
|
+
3. **多账号额度切换** — 免费额度、Plus/Team 试用、多账号小额度都可以纳入本地 `auth.json_*` 候选。当前账号触发用量限制时,FoxClaw 自动切换下一个可用账号、重启 app-server 并重试刚才的请求。
|
|
20
20
|
|
|
21
21
|
**适合这些场景:**
|
|
22
22
|
|
|
@@ -101,8 +101,8 @@ FoxClaw 只响应 `TG_ALLOWED_USER_ID` 的消息——把机器人拉进群不
|
|
|
101
101
|
|
|
102
102
|
**多账号管理:**
|
|
103
103
|
- Codex 账户管理:`/account`、`/quota`、`/login_device`、`/auth add <name>`
|
|
104
|
-
- 触发用量限制时自动在本地 `auth.json_*`
|
|
105
|
-
- `/auth`
|
|
104
|
+
- 触发用量限制时自动在本地 `auth.json_*` 之间切换认证
|
|
105
|
+
- `/auth` 面板分页查看、筛选、启用、禁用、搜索和切换候选账号;额度按真实窗口展示,多 bot 模式下会按账号 ID 汇总各 runtime 最近掌握的额度快照
|
|
106
106
|
|
|
107
107
|
**线程与会话:**
|
|
108
108
|
- `/threads`、`/open`、`/new`、`/where`、`/interrupt`——稳定的聊天-线程绑定
|
|
@@ -119,7 +119,7 @@ FoxClaw 只响应 `TG_ALLOWED_USER_ID` 的消息——把机器人拉进群不
|
|
|
119
119
|
|
|
120
120
|
## 多账号切换
|
|
121
121
|
|
|
122
|
-
FoxClaw
|
|
122
|
+
FoxClaw 的一大特色是自动多账号切换。当一个账号触发用量限制时,FoxClaw 会自动切换到下一个可用账号继续工作。
|
|
123
123
|
|
|
124
124
|
设置方式:
|
|
125
125
|
|
|
@@ -128,6 +128,8 @@ FoxClaw 的一大特色是自动多账号切换。当一个账号的 5 小时用
|
|
|
128
128
|
3. 用 `/auth` 查看所有候选账号状态。
|
|
129
129
|
4. 用 `/auth enable <n>` / `/auth disable <n>` 控制哪些账号参与自动轮换。
|
|
130
130
|
|
|
131
|
+
候选较多时,`/auth` 每页显示 8 个账号,并提供翻页、`全部 / 已启用 / 需关注` 筛选。也可以用 `/auth list <关键词>` 搜索文件名,或用 `/auth page <页码>` 直接跳页。额度窗口按 Codex 实际返回值展示,例如 `5h:20|7d:25` 或单月窗口 `30d:97`。
|
|
132
|
+
|
|
131
133
|
当 Codex 报告用量限制错误时,FoxClaw 会自动:
|
|
132
134
|
- 切换到下一个未失败的候选账号
|
|
133
135
|
- 重启 app-server 加载新认证
|
|
@@ -258,7 +260,7 @@ FoxClaw 会把 `codex app-server` 作为 detached 子进程启动,记录其 pi
|
|
|
258
260
|
- `/status`、`/account`、`/quota`、`/update`
|
|
259
261
|
- `/quota_nudge <credits|usage_limit> confirm`
|
|
260
262
|
- `/login_device`、`/login_cancel [id]`、`/logout confirm`
|
|
261
|
-
- `/auth [list|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]`
|
|
263
|
+
- `/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]`
|
|
262
264
|
- `/threads [query]`、`/threads archived`、`/open <n>`
|
|
263
265
|
- `/goal [objective|pause|resume|done|budget <tokens|off>|clear confirm]`
|
|
264
266
|
- `/history [limit]`、`/files <query>`、`/remote`
|
package/README_EN.md
CHANGED
|
@@ -16,7 +16,7 @@ No public server required. FoxClaw runs on your own computer, talks to `codex ap
|
|
|
16
16
|
|
|
17
17
|
1. **Open source with complete APIs** — Codex is OpenAI's open-source CLI agent and ships `codex app-server`. FoxClaw does not scrape a terminal; it uses the app-server interface to read threads, switch models, handle approvals, and resume sessions.
|
|
18
18
|
2. **Strong current coding experience** — FoxClaw reads the model list from your local Codex app-server. If your Codex environment has GPT-5.5 available, you can select and use it from your phone. For many heavy coding workflows, Codex/GPT-5.5 is already the reason to choose this stack.
|
|
19
|
-
3. **Multi-account quota rotation** — Free quota, trial Plus/Team accounts, and small account-specific allowances can all live as local `auth.json_*` candidates. When one account hits a
|
|
19
|
+
3. **Multi-account quota rotation** — Free quota, trial Plus/Team accounts, and small account-specific allowances can all live as local `auth.json_*` candidates. When one account hits a usage limit, FoxClaw switches to the next available account, restarts app-server, and retries the failed request.
|
|
20
20
|
|
|
21
21
|
**Built for these scenarios:**
|
|
22
22
|
|
|
@@ -102,7 +102,7 @@ FoxClaw accepts messages only from `TG_ALLOWED_USER_ID`. Putting the bot in a gr
|
|
|
102
102
|
**Multi-account management:**
|
|
103
103
|
- Codex account controls: `/account`, `/quota`, `/login_device`, `/auth add <name>`
|
|
104
104
|
- Automatic auth rotation across local `auth.json_*` files when a usage limit is hit — seamless account switching
|
|
105
|
-
- `/auth` panel to
|
|
105
|
+
- Paginated `/auth` panel to filter, enable, disable, search, and switch candidate accounts; quota uses observed window lengths, and multi-bot mode merges recent snapshots by account ID across runtimes
|
|
106
106
|
|
|
107
107
|
**Threads and sessions:**
|
|
108
108
|
- `/threads`, `/open`, `/new`, `/where`, `/interrupt` — sticky chat-to-thread binding
|
|
@@ -119,7 +119,7 @@ FoxClaw accepts messages only from `TG_ALLOWED_USER_ID`. Putting the bot in a gr
|
|
|
119
119
|
|
|
120
120
|
## Multi-Account Rotation
|
|
121
121
|
|
|
122
|
-
A key FoxClaw feature is automatic multi-account switching. When one account's
|
|
122
|
+
A key FoxClaw feature is automatic multi-account switching. When one account's usage limit is triggered, FoxClaw automatically switches to the next available account and continues working.
|
|
123
123
|
|
|
124
124
|
Setup:
|
|
125
125
|
|
|
@@ -128,6 +128,8 @@ Setup:
|
|
|
128
128
|
3. Use `/auth` to view all candidate account statuses.
|
|
129
129
|
4. Use `/auth enable <n>` / `/auth disable <n>` to control which accounts participate in auto-rotation.
|
|
130
130
|
|
|
131
|
+
For larger inventories, `/auth` shows 8 candidates per page with paging and `All / Enabled / Attention` filters. Use `/auth list <keyword>` to search filenames or `/auth page <n>` to jump directly. Quota windows use the values reported by Codex, for example `5h:20|7d:25` or a single monthly window such as `30d:97`.
|
|
132
|
+
|
|
131
133
|
When Codex reports a usage-limit error, FoxClaw automatically:
|
|
132
134
|
- Switches to the next non-failed candidate account
|
|
133
135
|
- Restarts app-server to load the new auth
|
|
@@ -258,7 +260,7 @@ No static Codex app-server port is required in normal installs.
|
|
|
258
260
|
- `/status`, `/account`, `/quota`, `/update`
|
|
259
261
|
- `/quota_nudge <credits|usage_limit> confirm`
|
|
260
262
|
- `/login_device`, `/login_cancel [id]`, `/logout confirm`
|
|
261
|
-
- `/auth [list|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]`
|
|
263
|
+
- `/auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]`
|
|
262
264
|
- `/threads [query]`, `/threads archived`, `/open <n>`
|
|
263
265
|
- `/goal [objective|pause|resume|done|budget <tokens|off>|clear confirm]`
|
|
264
266
|
- `/history [limit]`, `/files <query>`, `/remote`
|
|
@@ -279,6 +279,7 @@ export declare class BridgeSessionCore {
|
|
|
279
279
|
private listSkillsForScope;
|
|
280
280
|
private requireReadyBinding;
|
|
281
281
|
private handleAuthPanelActionCallback;
|
|
282
|
+
private handleAuthListViewCallback;
|
|
282
283
|
private handleAuthToggleCallback;
|
|
283
284
|
private handleAuthSwitchCallback;
|
|
284
285
|
private maybeRunPendingAuthRotation;
|
|
@@ -27,6 +27,9 @@ const DEFAULT_COLLABORATION_MODE = 'default';
|
|
|
27
27
|
const CODEX_LOCAL_USAGE_REFRESH_MS = 30 * 60_000;
|
|
28
28
|
const CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME = 'codex-local-usage.json';
|
|
29
29
|
const CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME = 'codex-auth-quota.json';
|
|
30
|
+
const CODEX_AUTH_LIST_PAGE_SIZE = 8;
|
|
31
|
+
const CODEX_AUTH_LOW_QUOTA_PERCENT = 10;
|
|
32
|
+
const CODEX_AUTH_STALE_CREDENTIAL_DAYS = 8;
|
|
30
33
|
const USER_INPUT_SUBMITTED_NOTICE_MS = 90_000;
|
|
31
34
|
const SELF_UPDATE_STATUS_POLL_MS = 1000;
|
|
32
35
|
const PLAN_IMPLEMENTATION_CODING_MESSAGE = 'Implement the plan.';
|
|
@@ -1010,6 +1013,21 @@ export class BridgeSessionCore {
|
|
|
1010
1013
|
await this.handleAuthToggleCallback(event, authToggleMatch[1], Number.parseInt(authToggleMatch[2], 10), locale);
|
|
1011
1014
|
return;
|
|
1012
1015
|
}
|
|
1016
|
+
const authPageMatch = /^auth:([a-f0-9]+):page:(prev|next)$/.exec(event.data);
|
|
1017
|
+
if (authPageMatch) {
|
|
1018
|
+
await this.handleAuthListViewCallback(event, authPageMatch[1], authPageMatch[2], locale);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const authFilterMatch = /^auth:([a-f0-9]+):filter:(all|enabled|attention)$/.exec(event.data);
|
|
1022
|
+
if (authFilterMatch) {
|
|
1023
|
+
await this.handleAuthListViewCallback(event, authFilterMatch[1], authFilterMatch[2], locale);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const authClearSearchMatch = /^auth:([a-f0-9]+):clear_search$/.exec(event.data);
|
|
1027
|
+
if (authClearSearchMatch) {
|
|
1028
|
+
await this.handleAuthListViewCallback(event, authClearSearchMatch[1], 'clear_search', locale);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1013
1031
|
const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|refresh_all_confirm|refresh_all_cancel|refresh_all)$/.exec(event.data);
|
|
1014
1032
|
if (authActionMatch) {
|
|
1015
1033
|
await this.handleAuthPanelActionCallback(event, authActionMatch[1], authActionMatch[2], locale);
|
|
@@ -3893,21 +3911,16 @@ export class BridgeSessionCore {
|
|
|
3893
3911
|
await this.handleAuthToggleCommand(scopeId, locale, args.slice(1), action === 'disable');
|
|
3894
3912
|
return;
|
|
3895
3913
|
}
|
|
3896
|
-
|
|
3914
|
+
const listRequest = parseCodexAuthListRequest(action, args);
|
|
3915
|
+
if (!listRequest) {
|
|
3897
3916
|
await this.sendMessage(scopeId, t(locale, 'usage_auth'));
|
|
3898
3917
|
return;
|
|
3899
3918
|
}
|
|
3900
3919
|
const state = await this.listCodexAuthState();
|
|
3901
3920
|
await this.refreshCurrentCodexAuthQuota(state);
|
|
3902
|
-
const record =
|
|
3903
|
-
localId: crypto.randomBytes(8).toString('hex'),
|
|
3904
|
-
chatId: scopeId,
|
|
3905
|
-
messageId: null,
|
|
3906
|
-
candidates: state.candidates,
|
|
3907
|
-
createdAt: Date.now(),
|
|
3908
|
-
};
|
|
3921
|
+
const record = createPendingAuthChoiceList(scopeId, state.candidates, listRequest);
|
|
3909
3922
|
this.pendingAuthChoiceLists.set(record.localId, record);
|
|
3910
|
-
const messageId = await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
3923
|
+
const messageId = await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
3911
3924
|
record.messageId = messageId;
|
|
3912
3925
|
}
|
|
3913
3926
|
async handleAuthRefreshAllCommand(scopeId, locale, confirmed = false) {
|
|
@@ -3918,13 +3931,7 @@ export class BridgeSessionCore {
|
|
|
3918
3931
|
if (!confirmed) {
|
|
3919
3932
|
const state = await this.listCodexAuthState();
|
|
3920
3933
|
await this.applySharedCodexAuthQuotaSnapshots(state);
|
|
3921
|
-
const record =
|
|
3922
|
-
localId: crypto.randomBytes(8).toString('hex'),
|
|
3923
|
-
chatId: scopeId,
|
|
3924
|
-
messageId: null,
|
|
3925
|
-
candidates: state.candidates,
|
|
3926
|
-
createdAt: Date.now(),
|
|
3927
|
-
};
|
|
3934
|
+
const record = createPendingAuthChoiceList(scopeId, state.candidates);
|
|
3928
3935
|
this.pendingAuthChoiceLists.set(record.localId, record);
|
|
3929
3936
|
const messageId = await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_confirm_message'), authRefreshAllConfirmKeyboard(locale, record));
|
|
3930
3937
|
record.messageId = messageId;
|
|
@@ -3934,15 +3941,9 @@ export class BridgeSessionCore {
|
|
|
3934
3941
|
const result = await this.refreshAllCodexAuthCandidates();
|
|
3935
3942
|
const state = await this.listCodexAuthState();
|
|
3936
3943
|
await this.applySharedCodexAuthQuotaSnapshots(state);
|
|
3937
|
-
const record =
|
|
3938
|
-
localId: crypto.randomBytes(8).toString('hex'),
|
|
3939
|
-
chatId: scopeId,
|
|
3940
|
-
messageId: null,
|
|
3941
|
-
candidates: state.candidates,
|
|
3942
|
-
createdAt: Date.now(),
|
|
3943
|
-
};
|
|
3944
|
+
const record = createPendingAuthChoiceList(scopeId, state.candidates);
|
|
3944
3945
|
this.pendingAuthChoiceLists.set(record.localId, record);
|
|
3945
|
-
const messageId = await this.sendMessage(scopeId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null)}`, authChoiceKeyboard(locale, record));
|
|
3946
|
+
const messageId = await this.sendMessage(scopeId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
3946
3947
|
record.messageId = messageId;
|
|
3947
3948
|
}
|
|
3948
3949
|
async handleAuthUseCommand(scopeId, locale, args) {
|
|
@@ -4435,7 +4436,7 @@ export class BridgeSessionCore {
|
|
|
4435
4436
|
record.candidates = state.candidates;
|
|
4436
4437
|
record.createdAt = Date.now();
|
|
4437
4438
|
if (record.messageId !== null) {
|
|
4438
|
-
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
4439
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
4439
4440
|
}
|
|
4440
4441
|
return;
|
|
4441
4442
|
}
|
|
@@ -4454,13 +4455,46 @@ export class BridgeSessionCore {
|
|
|
4454
4455
|
record.candidates = state.candidates;
|
|
4455
4456
|
record.createdAt = Date.now();
|
|
4456
4457
|
if (record.messageId !== null) {
|
|
4457
|
-
await this.editMessage(event.scopeId, record.messageId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null)}`, authChoiceKeyboard(locale, record));
|
|
4458
|
+
await this.editMessage(event.scopeId, record.messageId, `${formatAuthRefreshAllResult(locale, result)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
4458
4459
|
}
|
|
4459
4460
|
return;
|
|
4460
4461
|
}
|
|
4461
4462
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_reload_restarting'));
|
|
4462
4463
|
await this.handleAuthReloadCommand(event.scopeId, locale);
|
|
4463
4464
|
}
|
|
4465
|
+
async handleAuthListViewCallback(event, localId, action, locale) {
|
|
4466
|
+
const record = this.pendingAuthChoiceLists.get(localId);
|
|
4467
|
+
if (!record) {
|
|
4468
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_expired'));
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
if (record.chatId !== event.scopeId || (record.messageId !== null && record.messageId !== event.messageId)) {
|
|
4472
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_mismatch'));
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
4475
|
+
if (action === 'prev') {
|
|
4476
|
+
record.offset = Math.max(0, record.offset - record.pageSize);
|
|
4477
|
+
}
|
|
4478
|
+
else if (action === 'next') {
|
|
4479
|
+
record.offset += record.pageSize;
|
|
4480
|
+
}
|
|
4481
|
+
else if (action === 'clear_search') {
|
|
4482
|
+
record.searchTerm = null;
|
|
4483
|
+
record.offset = 0;
|
|
4484
|
+
}
|
|
4485
|
+
else {
|
|
4486
|
+
record.filter = action;
|
|
4487
|
+
record.offset = 0;
|
|
4488
|
+
}
|
|
4489
|
+
const state = await this.listCodexAuthState();
|
|
4490
|
+
record.candidates = state.candidates;
|
|
4491
|
+
record.createdAt = Date.now();
|
|
4492
|
+
clampCodexAuthListOffset(record);
|
|
4493
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
4494
|
+
if (record.messageId !== null) {
|
|
4495
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4464
4498
|
async handleAuthToggleCallback(event, localId, index, locale) {
|
|
4465
4499
|
const record = this.pendingAuthChoiceLists.get(localId);
|
|
4466
4500
|
if (!record) {
|
|
@@ -4480,9 +4514,10 @@ export class BridgeSessionCore {
|
|
|
4480
4514
|
this.store.setCodexAuthCandidateDisabled(candidate.name, disabled, this.authRuntimeId());
|
|
4481
4515
|
const state = await this.listCodexAuthState();
|
|
4482
4516
|
record.candidates = state.candidates;
|
|
4517
|
+
clampCodexAuthListOffset(record);
|
|
4483
4518
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, disabled ? 'auth_candidate_disabled_short' : 'auth_candidate_enabled_short'));
|
|
4484
4519
|
if (record.messageId !== null) {
|
|
4485
|
-
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
4520
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
4486
4521
|
}
|
|
4487
4522
|
}
|
|
4488
4523
|
async handleAuthSwitchCallback(event, localId, index, locale) {
|
|
@@ -4515,8 +4550,9 @@ export class BridgeSessionCore {
|
|
|
4515
4550
|
await this.refreshCurrentCodexAuthQuota(state);
|
|
4516
4551
|
record.candidates = state.candidates;
|
|
4517
4552
|
record.createdAt = Date.now();
|
|
4553
|
+
clampCodexAuthListOffset(record);
|
|
4518
4554
|
if (record.messageId !== null) {
|
|
4519
|
-
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
4555
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
4520
4556
|
}
|
|
4521
4557
|
}
|
|
4522
4558
|
async maybeRunPendingAuthRotation() {
|
|
@@ -5009,6 +5045,12 @@ export class BridgeSessionCore {
|
|
|
5009
5045
|
async readCodexAuthCandidateAccountIds(candidates) {
|
|
5010
5046
|
const entries = await Promise.all(candidates.map(async (candidate) => {
|
|
5011
5047
|
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
5048
|
+
candidate.credentialKind = metadata
|
|
5049
|
+
? 'chatgpt'
|
|
5050
|
+
: await isCodexApiKeyAuthCandidate(candidate.path)
|
|
5051
|
+
? 'api-key'
|
|
5052
|
+
: 'invalid';
|
|
5053
|
+
candidate.credentialLastRefreshMs = metadata?.lastRefreshMs ?? null;
|
|
5012
5054
|
return [candidate.name, metadata?.accountId ?? null];
|
|
5013
5055
|
}));
|
|
5014
5056
|
const accountIds = new Map();
|
|
@@ -5038,7 +5080,7 @@ export class BridgeSessionCore {
|
|
|
5038
5080
|
if (parsed && typeof parsed === 'object') {
|
|
5039
5081
|
for (const [name, value] of Object.entries(parsed)) {
|
|
5040
5082
|
if (isCodexAuthQuotaSnapshot(value)) {
|
|
5041
|
-
this.authQuotaSnapshots[name] = value;
|
|
5083
|
+
this.authQuotaSnapshots[name] = normalizeCodexAuthQuotaSnapshot(value);
|
|
5042
5084
|
}
|
|
5043
5085
|
}
|
|
5044
5086
|
}
|
|
@@ -7547,6 +7589,8 @@ async function listCodexAuthState(disabledNames = new Set(), explicitAuthDir = n
|
|
|
7547
7589
|
isCurrent: currentTargetPath === candidatePath,
|
|
7548
7590
|
disabled: disabledNames.has(entry.name),
|
|
7549
7591
|
mtimeMs: stat.mtimeMs,
|
|
7592
|
+
credentialKind: 'invalid',
|
|
7593
|
+
credentialLastRefreshMs: null,
|
|
7550
7594
|
quota: null,
|
|
7551
7595
|
});
|
|
7552
7596
|
}
|
|
@@ -7565,6 +7609,15 @@ function isCodexAuthCandidateName(name) {
|
|
|
7565
7609
|
}
|
|
7566
7610
|
return name.startsWith('auth.json_') || name.startsWith('auth.json.') || name.startsWith('auth.json-');
|
|
7567
7611
|
}
|
|
7612
|
+
async function isCodexApiKeyAuthCandidate(candidatePath) {
|
|
7613
|
+
try {
|
|
7614
|
+
const parsed = JSON.parse(await fs.readFile(candidatePath, 'utf8'));
|
|
7615
|
+
return typeof parsed.openai_api_key === 'string' && parsed.openai_api_key.trim().length > 0;
|
|
7616
|
+
}
|
|
7617
|
+
catch {
|
|
7618
|
+
return false;
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7568
7621
|
function codexAuthCandidateNameFromAddName(raw) {
|
|
7569
7622
|
const normalized = raw.trim();
|
|
7570
7623
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(normalized)) {
|
|
@@ -7657,7 +7710,75 @@ async function switchCodexAuth(targetPath, explicitAuthDir = null) {
|
|
|
7657
7710
|
toLabel: await authPathDisplayLabel(candidate.path),
|
|
7658
7711
|
};
|
|
7659
7712
|
}
|
|
7660
|
-
function
|
|
7713
|
+
function parseCodexAuthListRequest(action, args) {
|
|
7714
|
+
if (action === 'list') {
|
|
7715
|
+
return { searchTerm: args.slice(1).join(' ').trim() || null };
|
|
7716
|
+
}
|
|
7717
|
+
if (action === 'filter') {
|
|
7718
|
+
const filter = args[1]?.toLowerCase();
|
|
7719
|
+
return filter === 'all' || filter === 'enabled' || filter === 'attention'
|
|
7720
|
+
? { filter }
|
|
7721
|
+
: null;
|
|
7722
|
+
}
|
|
7723
|
+
if (action === 'page') {
|
|
7724
|
+
const page = Number.parseInt(args[1] ?? '', 10);
|
|
7725
|
+
return Number.isFinite(page) && page > 0
|
|
7726
|
+
? { offset: (page - 1) * CODEX_AUTH_LIST_PAGE_SIZE }
|
|
7727
|
+
: null;
|
|
7728
|
+
}
|
|
7729
|
+
return null;
|
|
7730
|
+
}
|
|
7731
|
+
function createPendingAuthChoiceList(chatId, candidates, view = {}) {
|
|
7732
|
+
const record = {
|
|
7733
|
+
localId: crypto.randomBytes(8).toString('hex'),
|
|
7734
|
+
chatId,
|
|
7735
|
+
messageId: null,
|
|
7736
|
+
candidates,
|
|
7737
|
+
createdAt: Date.now(),
|
|
7738
|
+
offset: Math.max(0, view.offset ?? 0),
|
|
7739
|
+
pageSize: CODEX_AUTH_LIST_PAGE_SIZE,
|
|
7740
|
+
filter: view.filter ?? 'all',
|
|
7741
|
+
searchTerm: view.searchTerm?.trim() || null,
|
|
7742
|
+
};
|
|
7743
|
+
clampCodexAuthListOffset(record);
|
|
7744
|
+
return record;
|
|
7745
|
+
}
|
|
7746
|
+
function clampCodexAuthListOffset(view) {
|
|
7747
|
+
const filteredCount = filterCodexAuthCandidates(view.candidates, view).length;
|
|
7748
|
+
const finalOffset = filteredCount > 0
|
|
7749
|
+
? Math.floor((filteredCount - 1) / view.pageSize) * view.pageSize
|
|
7750
|
+
: 0;
|
|
7751
|
+
view.offset = Math.min(Math.max(0, view.offset), finalOffset);
|
|
7752
|
+
}
|
|
7753
|
+
function codexAuthListPage(candidates, view) {
|
|
7754
|
+
const pageSize = Math.max(1, view?.pageSize ?? CODEX_AUTH_LIST_PAGE_SIZE);
|
|
7755
|
+
const filter = view?.filter ?? 'all';
|
|
7756
|
+
const searchTerm = view?.searchTerm?.trim() || null;
|
|
7757
|
+
const indexed = filterCodexAuthCandidates(candidates, { filter, searchTerm });
|
|
7758
|
+
const finalOffset = indexed.length > 0
|
|
7759
|
+
? Math.floor((indexed.length - 1) / pageSize) * pageSize
|
|
7760
|
+
: 0;
|
|
7761
|
+
const offset = Math.min(Math.max(0, view?.offset ?? 0), finalOffset);
|
|
7762
|
+
return {
|
|
7763
|
+
visible: indexed.slice(offset, offset + pageSize),
|
|
7764
|
+
filteredCount: indexed.length,
|
|
7765
|
+
offset,
|
|
7766
|
+
pageIndex: Math.floor(offset / pageSize),
|
|
7767
|
+
pageCount: Math.max(1, Math.ceil(indexed.length / pageSize)),
|
|
7768
|
+
filter,
|
|
7769
|
+
searchTerm,
|
|
7770
|
+
};
|
|
7771
|
+
}
|
|
7772
|
+
function filterCodexAuthCandidates(candidates, view) {
|
|
7773
|
+
const searchTerm = view.searchTerm?.trim().toLowerCase() || null;
|
|
7774
|
+
return candidates
|
|
7775
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
7776
|
+
.filter(({ candidate }) => !searchTerm || candidate.name.toLowerCase().includes(searchTerm))
|
|
7777
|
+
.filter(({ candidate }) => view.filter === 'all'
|
|
7778
|
+
|| (view.filter === 'enabled' && !candidate.disabled)
|
|
7779
|
+
|| (view.filter === 'attention' && codexAuthCandidateNeedsAttention(candidate)));
|
|
7780
|
+
}
|
|
7781
|
+
function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopyPaste = false, view = null) {
|
|
7661
7782
|
const lines = [
|
|
7662
7783
|
t(locale, 'auth_list_title'),
|
|
7663
7784
|
];
|
|
@@ -7674,24 +7795,42 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
|
|
|
7674
7795
|
}
|
|
7675
7796
|
lines.push(t(locale, 'auth_candidate_count', { value: state.candidates.length }));
|
|
7676
7797
|
lines.push(t(locale, 'auth_quota_legend'));
|
|
7677
|
-
state.candidates
|
|
7798
|
+
const page = codexAuthListPage(state.candidates, view);
|
|
7799
|
+
if (page.searchTerm) {
|
|
7800
|
+
lines.push(t(locale, 'auth_search', { value: page.searchTerm }));
|
|
7801
|
+
}
|
|
7802
|
+
if (page.filter !== 'all') {
|
|
7803
|
+
lines.push(t(locale, 'auth_filter', { value: formatCodexAuthFilter(locale, page.filter) }));
|
|
7804
|
+
}
|
|
7805
|
+
if (page.filteredCount !== state.candidates.length || page.pageCount > 1) {
|
|
7806
|
+
lines.push(t(locale, 'auth_page', {
|
|
7807
|
+
from: page.filteredCount === 0 ? 0 : page.offset + 1,
|
|
7808
|
+
to: page.offset + page.visible.length,
|
|
7809
|
+
filtered: page.filteredCount,
|
|
7810
|
+
total: state.candidates.length,
|
|
7811
|
+
page: page.pageIndex + 1,
|
|
7812
|
+
pages: page.pageCount,
|
|
7813
|
+
}));
|
|
7814
|
+
}
|
|
7815
|
+
if (page.visible.length === 0) {
|
|
7816
|
+
lines.push(t(locale, 'auth_no_matches'));
|
|
7817
|
+
}
|
|
7818
|
+
page.visible.forEach(({ candidate, index }) => {
|
|
7678
7819
|
const marker = candidate.isCurrent ? ' *' : '';
|
|
7679
|
-
|
|
7680
|
-
? t(locale, 'auth_candidate_status_disabled')
|
|
7681
|
-
: t(locale, 'auth_candidate_status_enabled');
|
|
7682
|
-
lines.push(`${index + 1}. ${formatAuthQuotaPrefix(candidate.quota)}|${candidate.name}${marker} [${status}]`);
|
|
7820
|
+
lines.push(`${index + 1}. ${formatAuthQuotaPrefix(locale, candidate.quota)}|${candidate.name}${marker} ${formatCodexAuthCandidateStatus(locale, candidate)}`);
|
|
7683
7821
|
});
|
|
7684
7822
|
if (includeWeixinCopyPaste) {
|
|
7685
|
-
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...
|
|
7823
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...page.visible.map(({ index }) => `/auth use ${index + 1}`), ...page.visible.map(({ candidate, index }) => candidate.disabled
|
|
7686
7824
|
? `/auth enable ${index + 1}`
|
|
7687
|
-
: `/auth disable ${index + 1}`), '/login_device', '/auth reload', '/permissions');
|
|
7825
|
+
: `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth reload', '/permissions');
|
|
7688
7826
|
}
|
|
7689
7827
|
return lines.join('\n');
|
|
7690
7828
|
}
|
|
7691
7829
|
function authChoiceKeyboard(locale, record) {
|
|
7692
|
-
const
|
|
7830
|
+
const page = codexAuthListPage(record.candidates, record);
|
|
7831
|
+
const rows = page.visible.map(({ candidate, index }) => [
|
|
7693
7832
|
{
|
|
7694
|
-
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaPrefix(candidate.quota)}|${candidate.name}${candidate.disabled ? ' · off' : ''}`),
|
|
7833
|
+
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaPrefix(locale, candidate.quota)}|${candidate.name}${candidate.disabled ? ' · off' : ''}`),
|
|
7695
7834
|
callback_data: `auth:${record.localId}:${index}`,
|
|
7696
7835
|
},
|
|
7697
7836
|
{
|
|
@@ -7699,6 +7838,24 @@ function authChoiceKeyboard(locale, record) {
|
|
|
7699
7838
|
callback_data: `auth:${record.localId}:toggle:${index}`,
|
|
7700
7839
|
},
|
|
7701
7840
|
]);
|
|
7841
|
+
const navigationRow = [];
|
|
7842
|
+
if (page.offset > 0) {
|
|
7843
|
+
navigationRow.push({ text: t(locale, 'button_prev_page'), callback_data: `auth:${record.localId}:page:prev` });
|
|
7844
|
+
}
|
|
7845
|
+
if (page.offset + page.visible.length < page.filteredCount) {
|
|
7846
|
+
navigationRow.push({ text: t(locale, 'button_next_page'), callback_data: `auth:${record.localId}:page:next` });
|
|
7847
|
+
}
|
|
7848
|
+
if (navigationRow.length > 0) {
|
|
7849
|
+
rows.push(navigationRow);
|
|
7850
|
+
}
|
|
7851
|
+
rows.push([
|
|
7852
|
+
authFilterButton(locale, record, 'all'),
|
|
7853
|
+
authFilterButton(locale, record, 'enabled'),
|
|
7854
|
+
authFilterButton(locale, record, 'attention'),
|
|
7855
|
+
]);
|
|
7856
|
+
if (record.searchTerm) {
|
|
7857
|
+
rows.push([{ text: t(locale, 'button_clear_filter'), callback_data: `auth:${record.localId}:clear_search` }]);
|
|
7858
|
+
}
|
|
7702
7859
|
rows.push([
|
|
7703
7860
|
{ text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
|
|
7704
7861
|
{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
|
|
@@ -7706,6 +7863,66 @@ function authChoiceKeyboard(locale, record) {
|
|
|
7706
7863
|
rows.push([{ text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` }]);
|
|
7707
7864
|
return rows;
|
|
7708
7865
|
}
|
|
7866
|
+
function authFilterButton(locale, record, filter) {
|
|
7867
|
+
const label = t(locale, `button_auth_filter_${filter}`);
|
|
7868
|
+
return {
|
|
7869
|
+
text: record.filter === filter ? `☑️ ${label}` : label,
|
|
7870
|
+
callback_data: `auth:${record.localId}:filter:${filter}`,
|
|
7871
|
+
};
|
|
7872
|
+
}
|
|
7873
|
+
function codexAuthCandidateHealth(candidate) {
|
|
7874
|
+
if (candidate.disabled) {
|
|
7875
|
+
return 'disabled';
|
|
7876
|
+
}
|
|
7877
|
+
if (candidate.credentialKind === 'api-key') {
|
|
7878
|
+
return 'api-key';
|
|
7879
|
+
}
|
|
7880
|
+
if (candidate.credentialKind === 'invalid') {
|
|
7881
|
+
return 'invalid';
|
|
7882
|
+
}
|
|
7883
|
+
const remaining = [
|
|
7884
|
+
candidate.quota?.primaryRemainingPercent,
|
|
7885
|
+
candidate.quota?.secondaryRemainingPercent,
|
|
7886
|
+
].filter((value) => typeof value === 'number' && Number.isFinite(value));
|
|
7887
|
+
if (remaining.some(value => value <= 0)) {
|
|
7888
|
+
return 'exhausted';
|
|
7889
|
+
}
|
|
7890
|
+
if (remaining.some(value => value <= CODEX_AUTH_LOW_QUOTA_PERCENT)) {
|
|
7891
|
+
return 'low';
|
|
7892
|
+
}
|
|
7893
|
+
if (candidate.credentialLastRefreshMs !== null
|
|
7894
|
+
&& candidate.credentialLastRefreshMs < Date.now() - CODEX_AUTH_STALE_CREDENTIAL_DAYS * 24 * 60 * 60_000) {
|
|
7895
|
+
return 'stale';
|
|
7896
|
+
}
|
|
7897
|
+
if (remaining.length === 0) {
|
|
7898
|
+
return 'unknown';
|
|
7899
|
+
}
|
|
7900
|
+
return 'ready';
|
|
7901
|
+
}
|
|
7902
|
+
function codexAuthCandidateNeedsAttention(candidate) {
|
|
7903
|
+
const health = codexAuthCandidateHealth(candidate);
|
|
7904
|
+
return health === 'stale'
|
|
7905
|
+
|| health === 'unknown'
|
|
7906
|
+
|| health === 'low'
|
|
7907
|
+
|| health === 'exhausted'
|
|
7908
|
+
|| health === 'invalid';
|
|
7909
|
+
}
|
|
7910
|
+
function formatCodexAuthCandidateStatus(locale, candidate) {
|
|
7911
|
+
const details = [];
|
|
7912
|
+
if (candidate.quota?.planType) {
|
|
7913
|
+
details.push(formatPlanTypeLabel(candidate.quota.planType));
|
|
7914
|
+
}
|
|
7915
|
+
details.push(t(locale, `auth_candidate_health_${codexAuthCandidateHealth(candidate)}`));
|
|
7916
|
+
if (candidate.credentialLastRefreshMs !== null) {
|
|
7917
|
+
details.push(t(locale, 'auth_candidate_last_refresh', {
|
|
7918
|
+
value: formatCompactAge(locale, Date.now() - candidate.credentialLastRefreshMs),
|
|
7919
|
+
}));
|
|
7920
|
+
}
|
|
7921
|
+
return `[${details.join(' · ')}]`;
|
|
7922
|
+
}
|
|
7923
|
+
function formatCodexAuthFilter(locale, filter) {
|
|
7924
|
+
return t(locale, `auth_filter_${filter}`);
|
|
7925
|
+
}
|
|
7709
7926
|
function authRefreshAllConfirmKeyboard(locale, record) {
|
|
7710
7927
|
return [
|
|
7711
7928
|
[{ text: t(locale, 'button_auth_refresh_all_confirm'), callback_data: `auth:${record.localId}:refresh_all_confirm` }],
|
|
@@ -8249,6 +8466,33 @@ function formatRateLimitWindowLabel(locale, window, fallback) {
|
|
|
8249
8466
|
}
|
|
8250
8467
|
return locale === 'zh' ? `${formatCompactNumber(minutes)}分钟` : `${formatCompactNumber(minutes)}m window`;
|
|
8251
8468
|
}
|
|
8469
|
+
function formatCompactRateLimitWindowLabel(locale, minutes, fallback) {
|
|
8470
|
+
if (!minutes || minutes <= 0) {
|
|
8471
|
+
return fallback === 'primary'
|
|
8472
|
+
? (locale === 'zh' ? '短' : 'P')
|
|
8473
|
+
: (locale === 'zh' ? '长' : 'S');
|
|
8474
|
+
}
|
|
8475
|
+
if (minutes % 1440 === 0) {
|
|
8476
|
+
return `${formatCompactNumber(minutes / 1440)}d`;
|
|
8477
|
+
}
|
|
8478
|
+
if (minutes % 60 === 0) {
|
|
8479
|
+
return `${formatCompactNumber(minutes / 60)}h`;
|
|
8480
|
+
}
|
|
8481
|
+
return `${formatCompactNumber(minutes)}m`;
|
|
8482
|
+
}
|
|
8483
|
+
function formatCompactAge(locale, ageMs) {
|
|
8484
|
+
const minutes = Math.max(0, ageMs) / 60_000;
|
|
8485
|
+
if (minutes >= 1440) {
|
|
8486
|
+
const days = Math.floor(minutes / 1440);
|
|
8487
|
+
return locale === 'zh' ? `${days}天前` : `${days}d ago`;
|
|
8488
|
+
}
|
|
8489
|
+
if (minutes >= 60) {
|
|
8490
|
+
const hours = Math.floor(minutes / 60);
|
|
8491
|
+
return locale === 'zh' ? `${hours}小时前` : `${hours}h ago`;
|
|
8492
|
+
}
|
|
8493
|
+
const roundedMinutes = Math.floor(minutes);
|
|
8494
|
+
return locale === 'zh' ? `${roundedMinutes}分钟前` : `${roundedMinutes}m ago`;
|
|
8495
|
+
}
|
|
8252
8496
|
function formatUsagePercent(value) {
|
|
8253
8497
|
if (!Number.isFinite(value)) {
|
|
8254
8498
|
return '?';
|
|
@@ -8263,7 +8507,10 @@ function authQuotaSnapshotFromRateLimit(snapshot, accountId = null) {
|
|
|
8263
8507
|
return {
|
|
8264
8508
|
capturedAtMs: Date.now(),
|
|
8265
8509
|
accountId,
|
|
8510
|
+
planType: snapshot.planType,
|
|
8511
|
+
primaryWindowDurationMins: snapshot.primary?.windowDurationMins ?? null,
|
|
8266
8512
|
primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
|
|
8513
|
+
secondaryWindowDurationMins: snapshot.secondary?.windowDurationMins ?? null,
|
|
8267
8514
|
secondaryRemainingPercent: snapshot.secondary ? remainingUsagePercent(snapshot.secondary.usedPercent) : null,
|
|
8268
8515
|
};
|
|
8269
8516
|
}
|
|
@@ -8271,7 +8518,10 @@ function codexAuthQuotaSnapshotFromRecord(record) {
|
|
|
8271
8518
|
return {
|
|
8272
8519
|
capturedAtMs: record.capturedAtMs,
|
|
8273
8520
|
accountId: record.accountId,
|
|
8521
|
+
planType: record.planType,
|
|
8522
|
+
primaryWindowDurationMins: record.primaryWindowDurationMins,
|
|
8274
8523
|
primaryRemainingPercent: record.primaryRemainingPercent,
|
|
8524
|
+
secondaryWindowDurationMins: record.secondaryWindowDurationMins,
|
|
8275
8525
|
secondaryRemainingPercent: record.secondaryRemainingPercent,
|
|
8276
8526
|
};
|
|
8277
8527
|
}
|
|
@@ -8282,25 +8532,19 @@ function mergeCodexAuthQuotaSnapshots(current, incoming) {
|
|
|
8282
8532
|
if (!incoming) {
|
|
8283
8533
|
return current;
|
|
8284
8534
|
}
|
|
8535
|
+
const freshest = incoming.capturedAtMs >= current.capturedAtMs ? incoming : current;
|
|
8536
|
+
const older = freshest === incoming ? current : incoming;
|
|
8285
8537
|
return {
|
|
8286
|
-
|
|
8287
|
-
accountId:
|
|
8288
|
-
primaryRemainingPercent: fresherNullableQuotaValue(current.primaryRemainingPercent, current.capturedAtMs, incoming.primaryRemainingPercent, incoming.capturedAtMs),
|
|
8289
|
-
secondaryRemainingPercent: fresherNullableQuotaValue(current.secondaryRemainingPercent, current.capturedAtMs, incoming.secondaryRemainingPercent, incoming.capturedAtMs),
|
|
8538
|
+
...freshest,
|
|
8539
|
+
accountId: freshest.accountId ?? older.accountId ?? null,
|
|
8290
8540
|
};
|
|
8291
8541
|
}
|
|
8292
|
-
function fresherNullableQuotaValue(currentValue, currentCapturedAtMs, incomingValue, incomingCapturedAtMs) {
|
|
8293
|
-
if (currentValue === null) {
|
|
8294
|
-
return incomingValue;
|
|
8295
|
-
}
|
|
8296
|
-
if (incomingValue === null) {
|
|
8297
|
-
return currentValue;
|
|
8298
|
-
}
|
|
8299
|
-
return incomingCapturedAtMs >= currentCapturedAtMs ? incomingValue : currentValue;
|
|
8300
|
-
}
|
|
8301
8542
|
function isFiniteCodexAuthQuotaSnapshotRecord(record) {
|
|
8302
8543
|
return Number.isFinite(record.capturedAtMs)
|
|
8544
|
+
&& isNullableString(record.planType)
|
|
8545
|
+
&& isNullableFiniteNumber(record.primaryWindowDurationMins)
|
|
8303
8546
|
&& isNullableFiniteNumber(record.primaryRemainingPercent)
|
|
8547
|
+
&& isNullableFiniteNumber(record.secondaryWindowDurationMins)
|
|
8304
8548
|
&& isNullableFiniteNumber(record.secondaryRemainingPercent);
|
|
8305
8549
|
}
|
|
8306
8550
|
function remainingUsagePercent(usedPercent) {
|
|
@@ -8309,13 +8553,18 @@ function remainingUsagePercent(usedPercent) {
|
|
|
8309
8553
|
}
|
|
8310
8554
|
return Math.max(0, Math.min(100, 100 - usedPercent));
|
|
8311
8555
|
}
|
|
8312
|
-
function formatAuthQuotaPrefix(snapshot) {
|
|
8556
|
+
function formatAuthQuotaPrefix(locale, snapshot) {
|
|
8313
8557
|
if (!snapshot) {
|
|
8314
|
-
return '
|
|
8558
|
+
return '--';
|
|
8315
8559
|
}
|
|
8316
|
-
const
|
|
8317
|
-
|
|
8318
|
-
|
|
8560
|
+
const windows = [
|
|
8561
|
+
[snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, 'primary'],
|
|
8562
|
+
[snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, 'secondary'],
|
|
8563
|
+
];
|
|
8564
|
+
const values = windows
|
|
8565
|
+
.filter(([duration, remaining]) => duration !== null || remaining !== null)
|
|
8566
|
+
.map(([duration, remaining, fallback]) => (`${formatCompactRateLimitWindowLabel(locale, duration, fallback)}:${remaining === null ? '--' : formatUsagePercent(remaining)}`));
|
|
8567
|
+
return values.length > 0 ? values.join('|') : '--';
|
|
8319
8568
|
}
|
|
8320
8569
|
function isCodexAuthQuotaSnapshot(value) {
|
|
8321
8570
|
if (!value || typeof value !== 'object') {
|
|
@@ -8325,12 +8574,29 @@ function isCodexAuthQuotaSnapshot(value) {
|
|
|
8325
8574
|
return typeof snapshot.capturedAtMs === 'number'
|
|
8326
8575
|
&& Number.isFinite(snapshot.capturedAtMs)
|
|
8327
8576
|
&& (snapshot.accountId === undefined || snapshot.accountId === null || typeof snapshot.accountId === 'string')
|
|
8577
|
+
&& (snapshot.planType === undefined || isNullableString(snapshot.planType))
|
|
8578
|
+
&& (snapshot.primaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.primaryWindowDurationMins))
|
|
8328
8579
|
&& isNullableFiniteNumber(snapshot.primaryRemainingPercent)
|
|
8580
|
+
&& (snapshot.secondaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.secondaryWindowDurationMins))
|
|
8329
8581
|
&& isNullableFiniteNumber(snapshot.secondaryRemainingPercent);
|
|
8330
8582
|
}
|
|
8583
|
+
function normalizeCodexAuthQuotaSnapshot(snapshot) {
|
|
8584
|
+
return {
|
|
8585
|
+
capturedAtMs: snapshot.capturedAtMs,
|
|
8586
|
+
accountId: snapshot.accountId ?? null,
|
|
8587
|
+
planType: snapshot.planType ?? null,
|
|
8588
|
+
primaryWindowDurationMins: snapshot.primaryWindowDurationMins ?? null,
|
|
8589
|
+
primaryRemainingPercent: snapshot.primaryRemainingPercent,
|
|
8590
|
+
secondaryWindowDurationMins: snapshot.secondaryWindowDurationMins ?? null,
|
|
8591
|
+
secondaryRemainingPercent: snapshot.secondaryRemainingPercent,
|
|
8592
|
+
};
|
|
8593
|
+
}
|
|
8331
8594
|
function isNullableFiniteNumber(value) {
|
|
8332
8595
|
return value === null || (typeof value === 'number' && Number.isFinite(value));
|
|
8333
8596
|
}
|
|
8597
|
+
function isNullableString(value) {
|
|
8598
|
+
return value === null || typeof value === 'string';
|
|
8599
|
+
}
|
|
8334
8600
|
function formatCompactNumber(value) {
|
|
8335
8601
|
return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, '');
|
|
8336
8602
|
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -137,16 +137,32 @@ declare const MESSAGES: {
|
|
|
137
137
|
readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
|
|
138
138
|
readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
|
|
139
139
|
readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
|
|
140
|
-
readonly usage_auth: "Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]";
|
|
140
|
+
readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]";
|
|
141
141
|
readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
|
|
142
142
|
readonly auth_list_title: "Codex auth files:";
|
|
143
143
|
readonly auth_bot: "Bot runtime: {value}";
|
|
144
144
|
readonly auth_current: "Current auth: {value}";
|
|
145
145
|
readonly auth_dir: "Auth dir: {value}";
|
|
146
146
|
readonly auth_candidate_count: "Candidates: {value}";
|
|
147
|
-
readonly auth_quota_legend: "Quota remaining:
|
|
147
|
+
readonly auth_quota_legend: "Quota remaining: window:percent|auth";
|
|
148
|
+
readonly auth_search: "Search: {value}";
|
|
149
|
+
readonly auth_filter: "Filter: {value}";
|
|
150
|
+
readonly auth_filter_all: "all";
|
|
151
|
+
readonly auth_filter_enabled: "enabled";
|
|
152
|
+
readonly auth_filter_attention: "attention";
|
|
153
|
+
readonly auth_page: "Showing {from}-{to} of {filtered} matched candidates ({total} total), page {page}/{pages}";
|
|
154
|
+
readonly auth_no_matches: "No auth candidates match this view.";
|
|
148
155
|
readonly auth_candidate_status_enabled: "enabled";
|
|
149
156
|
readonly auth_candidate_status_disabled: "disabled";
|
|
157
|
+
readonly auth_candidate_health_disabled: "disabled";
|
|
158
|
+
readonly auth_candidate_health_ready: "ready";
|
|
159
|
+
readonly auth_candidate_health_low: "low quota";
|
|
160
|
+
readonly auth_candidate_health_exhausted: "quota exhausted";
|
|
161
|
+
readonly auth_candidate_health_stale: "not recently refreshed";
|
|
162
|
+
readonly auth_candidate_health_unknown: "quota unknown";
|
|
163
|
+
readonly 'auth_candidate_health_api-key': "API key";
|
|
164
|
+
readonly auth_candidate_health_invalid: "invalid auth file";
|
|
165
|
+
readonly auth_candidate_last_refresh: "refreshed {value}";
|
|
150
166
|
readonly auth_candidate_enabled: "Enabled auth candidate for auto-rotation: {value}";
|
|
151
167
|
readonly auth_candidate_disabled: "Disabled auth candidate for auto-rotation: {value}";
|
|
152
168
|
readonly auth_candidate_enabled_short: "Auth enabled";
|
|
@@ -185,6 +201,9 @@ declare const MESSAGES: {
|
|
|
185
201
|
readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
|
|
186
202
|
readonly button_auth_enable: "✅";
|
|
187
203
|
readonly button_auth_disable: "⏸️";
|
|
204
|
+
readonly button_auth_filter_all: "All";
|
|
205
|
+
readonly button_auth_filter_enabled: "Enabled";
|
|
206
|
+
readonly button_auth_filter_attention: "Attention";
|
|
188
207
|
readonly another_turn_running: "Another turn is already running. Use /interrupt, /takeover, /queue, or wait.";
|
|
189
208
|
readonly working: "Working...";
|
|
190
209
|
readonly usage_open: "Usage: /open <n>";
|
|
@@ -723,16 +742,32 @@ declare const MESSAGES: {
|
|
|
723
742
|
readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
|
|
724
743
|
readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
|
|
725
744
|
readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
|
|
726
|
-
readonly usage_auth: "用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]";
|
|
745
|
+
readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]";
|
|
727
746
|
readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
|
|
728
747
|
readonly auth_list_title: "Codex auth 文件:";
|
|
729
748
|
readonly auth_bot: "Bot runtime:{value}";
|
|
730
749
|
readonly auth_current: "当前 auth:{value}";
|
|
731
750
|
readonly auth_dir: "Auth 目录:{value}";
|
|
732
751
|
readonly auth_candidate_count: "候选数量:{value}";
|
|
733
|
-
readonly auth_quota_legend: "
|
|
752
|
+
readonly auth_quota_legend: "额度剩余:窗口:百分比|auth";
|
|
753
|
+
readonly auth_search: "搜索:{value}";
|
|
754
|
+
readonly auth_filter: "筛选:{value}";
|
|
755
|
+
readonly auth_filter_all: "全部";
|
|
756
|
+
readonly auth_filter_enabled: "已启用";
|
|
757
|
+
readonly auth_filter_attention: "需关注";
|
|
758
|
+
readonly auth_page: "当前显示 {from}-{to},匹配 {filtered} / 总计 {total},第 {page}/{pages} 页";
|
|
759
|
+
readonly auth_no_matches: "当前视图没有匹配的 auth 候选。";
|
|
734
760
|
readonly auth_candidate_status_enabled: "启用";
|
|
735
761
|
readonly auth_candidate_status_disabled: "禁用";
|
|
762
|
+
readonly auth_candidate_health_disabled: "已禁用";
|
|
763
|
+
readonly auth_candidate_health_ready: "正常";
|
|
764
|
+
readonly auth_candidate_health_low: "额度偏低";
|
|
765
|
+
readonly auth_candidate_health_exhausted: "额度耗尽";
|
|
766
|
+
readonly auth_candidate_health_stale: "长期未刷新";
|
|
767
|
+
readonly auth_candidate_health_unknown: "额度未知";
|
|
768
|
+
readonly 'auth_candidate_health_api-key': "API key";
|
|
769
|
+
readonly auth_candidate_health_invalid: "auth 文件无效";
|
|
770
|
+
readonly auth_candidate_last_refresh: "刷新于 {value}";
|
|
736
771
|
readonly auth_candidate_enabled: "已启用 auth 候选自动轮换:{value}";
|
|
737
772
|
readonly auth_candidate_disabled: "已禁用 auth 候选自动轮换:{value}";
|
|
738
773
|
readonly auth_candidate_enabled_short: "auth 已启用";
|
|
@@ -771,6 +806,9 @@ declare const MESSAGES: {
|
|
|
771
806
|
readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
|
|
772
807
|
readonly button_auth_enable: "✅";
|
|
773
808
|
readonly button_auth_disable: "⏸️";
|
|
809
|
+
readonly button_auth_filter_all: "全部";
|
|
810
|
+
readonly button_auth_filter_enabled: "已启用";
|
|
811
|
+
readonly button_auth_filter_attention: "需关注";
|
|
774
812
|
readonly another_turn_running: "已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。";
|
|
775
813
|
readonly working: "处理中...";
|
|
776
814
|
readonly usage_open: "用法:/open <编号>";
|
package/dist/i18n.js
CHANGED
|
@@ -135,16 +135,32 @@ const MESSAGES = {
|
|
|
135
135
|
auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
|
|
136
136
|
auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
|
|
137
137
|
auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
|
|
138
|
-
usage_auth: 'Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]',
|
|
138
|
+
usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]',
|
|
139
139
|
usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
|
|
140
140
|
auth_list_title: 'Codex auth files:',
|
|
141
141
|
auth_bot: 'Bot runtime: {value}',
|
|
142
142
|
auth_current: 'Current auth: {value}',
|
|
143
143
|
auth_dir: 'Auth dir: {value}',
|
|
144
144
|
auth_candidate_count: 'Candidates: {value}',
|
|
145
|
-
auth_quota_legend: 'Quota remaining:
|
|
145
|
+
auth_quota_legend: 'Quota remaining: window:percent|auth',
|
|
146
|
+
auth_search: 'Search: {value}',
|
|
147
|
+
auth_filter: 'Filter: {value}',
|
|
148
|
+
auth_filter_all: 'all',
|
|
149
|
+
auth_filter_enabled: 'enabled',
|
|
150
|
+
auth_filter_attention: 'attention',
|
|
151
|
+
auth_page: 'Showing {from}-{to} of {filtered} matched candidates ({total} total), page {page}/{pages}',
|
|
152
|
+
auth_no_matches: 'No auth candidates match this view.',
|
|
146
153
|
auth_candidate_status_enabled: 'enabled',
|
|
147
154
|
auth_candidate_status_disabled: 'disabled',
|
|
155
|
+
auth_candidate_health_disabled: 'disabled',
|
|
156
|
+
auth_candidate_health_ready: 'ready',
|
|
157
|
+
auth_candidate_health_low: 'low quota',
|
|
158
|
+
auth_candidate_health_exhausted: 'quota exhausted',
|
|
159
|
+
auth_candidate_health_stale: 'not recently refreshed',
|
|
160
|
+
auth_candidate_health_unknown: 'quota unknown',
|
|
161
|
+
'auth_candidate_health_api-key': 'API key',
|
|
162
|
+
auth_candidate_health_invalid: 'invalid auth file',
|
|
163
|
+
auth_candidate_last_refresh: 'refreshed {value}',
|
|
148
164
|
auth_candidate_enabled: 'Enabled auth candidate for auto-rotation: {value}',
|
|
149
165
|
auth_candidate_disabled: 'Disabled auth candidate for auto-rotation: {value}',
|
|
150
166
|
auth_candidate_enabled_short: 'Auth enabled',
|
|
@@ -183,6 +199,9 @@ const MESSAGES = {
|
|
|
183
199
|
button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
|
|
184
200
|
button_auth_enable: '✅',
|
|
185
201
|
button_auth_disable: '⏸️',
|
|
202
|
+
button_auth_filter_all: 'All',
|
|
203
|
+
button_auth_filter_enabled: 'Enabled',
|
|
204
|
+
button_auth_filter_attention: 'Attention',
|
|
186
205
|
another_turn_running: 'Another turn is already running. Use /interrupt, /takeover, /queue, or wait.',
|
|
187
206
|
working: 'Working...',
|
|
188
207
|
usage_open: 'Usage: /open <n>',
|
|
@@ -721,16 +740,32 @@ const MESSAGES = {
|
|
|
721
740
|
auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
|
|
722
741
|
auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
|
|
723
742
|
auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
|
|
724
|
-
usage_auth: '用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]',
|
|
743
|
+
usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]',
|
|
725
744
|
usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
|
|
726
745
|
auth_list_title: 'Codex auth 文件:',
|
|
727
746
|
auth_bot: 'Bot runtime:{value}',
|
|
728
747
|
auth_current: '当前 auth:{value}',
|
|
729
748
|
auth_dir: 'Auth 目录:{value}',
|
|
730
749
|
auth_candidate_count: '候选数量:{value}',
|
|
731
|
-
auth_quota_legend: '
|
|
750
|
+
auth_quota_legend: '额度剩余:窗口:百分比|auth',
|
|
751
|
+
auth_search: '搜索:{value}',
|
|
752
|
+
auth_filter: '筛选:{value}',
|
|
753
|
+
auth_filter_all: '全部',
|
|
754
|
+
auth_filter_enabled: '已启用',
|
|
755
|
+
auth_filter_attention: '需关注',
|
|
756
|
+
auth_page: '当前显示 {from}-{to},匹配 {filtered} / 总计 {total},第 {page}/{pages} 页',
|
|
757
|
+
auth_no_matches: '当前视图没有匹配的 auth 候选。',
|
|
732
758
|
auth_candidate_status_enabled: '启用',
|
|
733
759
|
auth_candidate_status_disabled: '禁用',
|
|
760
|
+
auth_candidate_health_disabled: '已禁用',
|
|
761
|
+
auth_candidate_health_ready: '正常',
|
|
762
|
+
auth_candidate_health_low: '额度偏低',
|
|
763
|
+
auth_candidate_health_exhausted: '额度耗尽',
|
|
764
|
+
auth_candidate_health_stale: '长期未刷新',
|
|
765
|
+
auth_candidate_health_unknown: '额度未知',
|
|
766
|
+
'auth_candidate_health_api-key': 'API key',
|
|
767
|
+
auth_candidate_health_invalid: 'auth 文件无效',
|
|
768
|
+
auth_candidate_last_refresh: '刷新于 {value}',
|
|
734
769
|
auth_candidate_enabled: '已启用 auth 候选自动轮换:{value}',
|
|
735
770
|
auth_candidate_disabled: '已禁用 auth 候选自动轮换:{value}',
|
|
736
771
|
auth_candidate_enabled_short: 'auth 已启用',
|
|
@@ -769,6 +804,9 @@ const MESSAGES = {
|
|
|
769
804
|
button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
|
|
770
805
|
button_auth_enable: '✅',
|
|
771
806
|
button_auth_disable: '⏸️',
|
|
807
|
+
button_auth_filter_all: '全部',
|
|
808
|
+
button_auth_filter_enabled: '已启用',
|
|
809
|
+
button_auth_filter_attention: '需关注',
|
|
772
810
|
another_turn_running: '已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。',
|
|
773
811
|
working: '处理中...',
|
|
774
812
|
usage_open: '用法:/open <编号>',
|
package/dist/store/database.d.ts
CHANGED
|
@@ -29,7 +29,10 @@ export interface CodexAuthQuotaSnapshotRecord {
|
|
|
29
29
|
candidateName: string;
|
|
30
30
|
accountId: string;
|
|
31
31
|
capturedAtMs: number;
|
|
32
|
+
planType: string | null;
|
|
33
|
+
primaryWindowDurationMins: number | null;
|
|
32
34
|
primaryRemainingPercent: number | null;
|
|
35
|
+
secondaryWindowDurationMins: number | null;
|
|
33
36
|
secondaryRemainingPercent: number | null;
|
|
34
37
|
updatedAt: number;
|
|
35
38
|
}
|
|
@@ -86,7 +89,7 @@ export declare class BridgeStore {
|
|
|
86
89
|
setWeixinContextToken(scopeId: string, contextToken: string): void;
|
|
87
90
|
listDisabledCodexAuthCandidateNames(runtimeId?: string): Set<string>;
|
|
88
91
|
setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
|
|
89
|
-
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'primaryRemainingPercent' | 'secondaryRemainingPercent'>): void;
|
|
92
|
+
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
|
|
90
93
|
listCodexAuthQuotaSnapshots(accountIds: string[]): CodexAuthQuotaSnapshotRecord[];
|
|
91
94
|
private ensureColumn;
|
|
92
95
|
}
|
package/dist/store/database.js
CHANGED
|
@@ -128,7 +128,10 @@ export class BridgeStore {
|
|
|
128
128
|
candidate_name TEXT NOT NULL,
|
|
129
129
|
account_id TEXT NOT NULL,
|
|
130
130
|
captured_at_ms INTEGER NOT NULL,
|
|
131
|
+
plan_type TEXT,
|
|
132
|
+
primary_window_duration_mins REAL,
|
|
131
133
|
primary_remaining_percent REAL,
|
|
134
|
+
secondary_window_duration_mins REAL,
|
|
132
135
|
secondary_remaining_percent REAL,
|
|
133
136
|
updated_at INTEGER NOT NULL,
|
|
134
137
|
PRIMARY KEY (runtime_id, candidate_name)
|
|
@@ -148,6 +151,9 @@ export class BridgeStore {
|
|
|
148
151
|
this.ensureColumn('pending_approvals', 'payload_json', 'TEXT');
|
|
149
152
|
this.ensureColumn('pending_user_inputs', 'status', "TEXT NOT NULL DEFAULT 'pending'");
|
|
150
153
|
this.ensureColumn('pending_user_inputs', 'submitted_at', 'INTEGER');
|
|
154
|
+
this.ensureColumn('codex_auth_quota_snapshots', 'plan_type', 'TEXT');
|
|
155
|
+
this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
|
|
156
|
+
this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
|
|
151
157
|
migrateLegacyBridgeScopeIds(this.db);
|
|
152
158
|
}
|
|
153
159
|
getTelegramOffset(botKey) {
|
|
@@ -528,18 +534,24 @@ export class BridgeStore {
|
|
|
528
534
|
candidate_name,
|
|
529
535
|
account_id,
|
|
530
536
|
captured_at_ms,
|
|
537
|
+
plan_type,
|
|
538
|
+
primary_window_duration_mins,
|
|
531
539
|
primary_remaining_percent,
|
|
540
|
+
secondary_window_duration_mins,
|
|
532
541
|
secondary_remaining_percent,
|
|
533
542
|
updated_at
|
|
534
543
|
)
|
|
535
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
544
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
536
545
|
ON CONFLICT(runtime_id, candidate_name) DO UPDATE SET
|
|
537
546
|
account_id = excluded.account_id,
|
|
538
547
|
captured_at_ms = excluded.captured_at_ms,
|
|
548
|
+
plan_type = excluded.plan_type,
|
|
549
|
+
primary_window_duration_mins = excluded.primary_window_duration_mins,
|
|
539
550
|
primary_remaining_percent = excluded.primary_remaining_percent,
|
|
551
|
+
secondary_window_duration_mins = excluded.secondary_window_duration_mins,
|
|
540
552
|
secondary_remaining_percent = excluded.secondary_remaining_percent,
|
|
541
553
|
updated_at = excluded.updated_at
|
|
542
|
-
`).run(runtimeId, candidateName, accountId, snapshot.capturedAtMs, snapshot.primaryRemainingPercent, snapshot.secondaryRemainingPercent, Date.now());
|
|
554
|
+
`).run(runtimeId, candidateName, accountId, snapshot.capturedAtMs, snapshot.planType, snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, Date.now());
|
|
543
555
|
}
|
|
544
556
|
listCodexAuthQuotaSnapshots(accountIds) {
|
|
545
557
|
const uniqueAccountIds = [...new Set(accountIds.filter(Boolean))];
|
|
@@ -553,7 +565,10 @@ export class BridgeStore {
|
|
|
553
565
|
candidate_name,
|
|
554
566
|
account_id,
|
|
555
567
|
captured_at_ms,
|
|
568
|
+
plan_type,
|
|
569
|
+
primary_window_duration_mins,
|
|
556
570
|
primary_remaining_percent,
|
|
571
|
+
secondary_window_duration_mins,
|
|
557
572
|
secondary_remaining_percent,
|
|
558
573
|
updated_at
|
|
559
574
|
FROM codex_auth_quota_snapshots
|
|
@@ -564,7 +579,10 @@ export class BridgeStore {
|
|
|
564
579
|
candidateName: String(row.candidate_name),
|
|
565
580
|
accountId: String(row.account_id),
|
|
566
581
|
capturedAtMs: Number(row.captured_at_ms),
|
|
582
|
+
planType: nullableString(row.plan_type),
|
|
583
|
+
primaryWindowDurationMins: nullableNumber(row.primary_window_duration_mins),
|
|
567
584
|
primaryRemainingPercent: nullableNumber(row.primary_remaining_percent),
|
|
585
|
+
secondaryWindowDurationMins: nullableNumber(row.secondary_window_duration_mins),
|
|
568
586
|
secondaryRemainingPercent: nullableNumber(row.secondary_remaining_percent),
|
|
569
587
|
updatedAt: Number(row.updated_at),
|
|
570
588
|
}));
|
|
@@ -584,6 +602,9 @@ function nullableNumber(value) {
|
|
|
584
602
|
const numberValue = Number(value);
|
|
585
603
|
return Number.isFinite(numberValue) ? numberValue : null;
|
|
586
604
|
}
|
|
605
|
+
function nullableString(value) {
|
|
606
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
607
|
+
}
|
|
587
608
|
function normalizeCollaborationMode(value) {
|
|
588
609
|
return value === 'default' || value === 'plan' ? value : null;
|
|
589
610
|
}
|
package/docs/user-manual.md
CHANGED
|
@@ -389,7 +389,9 @@ If the login is cancelled or fails, FoxClaw tries to restore the previous auth t
|
|
|
389
389
|
|
|
390
390
|
### 6.3 The `/auth` Panel
|
|
391
391
|
|
|
392
|
-
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. In multi-bot mode the panel names the `@botname` runtime being managed, because private chats, groups, and topics on one bot share that bot's current auth. The
|
|
392
|
+
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. In multi-bot mode the panel names the `@botname` runtime being managed, because private chats, groups, and topics on one bot share that bot's current auth. The panel shows 8 candidates per page and supports paging, `All / Enabled / Attention` filters, and `/auth list <keyword>` filename search for large local inventories. `/auth use <n>` always uses the full-list candidate number, independent of panel paging.
|
|
393
|
+
|
|
394
|
+
Each filename is prefixed with observed `window:remaining-percent` values. For example, a Plus account may show `5h:20|7d:25`, while an account with one monthly window may show `30d:97`. The current auth quota is refreshed when the panel opens; other candidates are not switched merely to query quota. When multiple bot runtimes have recently used the same ChatGPT account, FoxClaw combines their cached quota snapshots by verified account ID, so one bot's `/auth` panel can show quota information learned by another bot without mixing different accounts.
|
|
393
395
|
|
|
394
396
|
Approximation:
|
|
395
397
|
|
|
@@ -398,23 +400,28 @@ Codex auth
|
|
|
398
400
|
Current: auth.json_personal
|
|
399
401
|
Auth dir: /home/alice/.codex
|
|
400
402
|
Candidates: 2
|
|
401
|
-
Quota remaining:
|
|
402
|
-
1. 20|25|auth.json_personal * [
|
|
403
|
-
2.
|
|
403
|
+
Quota remaining: window:percent|auth
|
|
404
|
+
1. 5h:20|7d:25|auth.json_personal * [Plus · ready · refreshed 2h ago]
|
|
405
|
+
2. --|auth.json_team [quota unknown]
|
|
404
406
|
|
|
405
|
-
[✅ 20|25|auth.json_personal] [✅]
|
|
406
|
-
[🔐
|
|
407
|
+
[✅ 5h:20|7d:25|auth.json_personal] [✅]
|
|
408
|
+
[🔐 --|auth.json_team] [✅]
|
|
409
|
+
[☑️ All] [Enabled] [Attention]
|
|
407
410
|
[🛡️ Access] [🔑 Login]
|
|
408
411
|
[🔄 Reload auth]
|
|
409
412
|
```
|
|
410
413
|
|
|
411
|
-
The right-side `✅` / `⏸️` button
|
|
414
|
+
The right-side `✅` / `⏸️` button controls whether the candidate participates in auto-rotation. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. Tapping a candidate switches auth, restarts that runtime, and refreshes the same panel with its buttons intact so you can switch again immediately. `--` means no quota snapshot has been observed for that candidate yet. Health summaries distinguish ready, low quota, quota exhausted, quota unknown, not recently refreshed, API key, and invalid auth file states.
|
|
412
415
|
|
|
413
416
|
`/auth refresh all` is a command-only maintenance action because ChatGPT refresh tokens are rotated. It is allowed only when every Telegram runtime, the Weixin runtime, approvals, inputs, logins, and auth mirroring are idle. The command first shows a risk confirmation: if OpenAI/Codex consumes an old refresh token but the new token cannot be saved because of network, process, or disk failure, that candidate may require device login or phone verification again. After confirmation, FoxClaw visits every ChatGPT candidate, asks Codex to force-refresh tokens with `account/read refreshToken=true`, verifies the result through the usage endpoint, mirrors successful candidates, restores the original current auth, and shows a summary.
|
|
414
417
|
|
|
418
|
+
OpenAI does not publish a fixed ChatGPT refresh-token lifetime or an old-token replay grace period. Codex refreshes automatically when an access token approaches expiry; when it cannot parse the access-token `exp`, current Codex uses a `last_refresh` fallback of about 8 days. The panel labels candidates without a refresh record in that interval as `not recently refreshed`, but this is only a maintenance hint. It does not prove expiry and does not trigger bulk keepalive refreshes. Do not use `/auth refresh all` as a routine keepalive command.
|
|
419
|
+
|
|
415
420
|
Equivalent commands:
|
|
416
421
|
|
|
417
|
-
- `/auth` or `/auth list`: show candidates.
|
|
422
|
+
- `/auth` or `/auth list [keyword]`: show candidates, optionally filtered by filename.
|
|
423
|
+
- `/auth filter <all|enabled|attention>`: show all, enabled, or attention-needed candidates.
|
|
424
|
+
- `/auth page <n>`: open a specific page directly.
|
|
418
425
|
- `/auth use <n>`: switch to candidate n and restart app-server.
|
|
419
426
|
- `/auth enable <n>`: let candidate n participate in auto-rotation.
|
|
420
427
|
- `/auth disable <n>`: skip candidate n during auto-rotation.
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -389,7 +389,9 @@ cp -L ~/.codex/auth.json ~/.codex/auth.json_personal
|
|
|
389
389
|
|
|
390
390
|
### 6.3 `/auth` 面板
|
|
391
391
|
|
|
392
|
-
`/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。多 bot 模式中,面板顶部还会显示当前正在管理的 `@botname`,因为该 bot 内的私聊、群聊和话题共享同一个当前 auth
|
|
392
|
+
`/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。多 bot 模式中,面板顶部还会显示当前正在管理的 `@botname`,因为该 bot 内的私聊、群聊和话题共享同一个当前 auth。面板每页显示 8 个候选,支持翻页、`全部 / 已启用 / 需关注` 筛选和 `/auth list <关键词>` 文件名搜索,适合管理较大的本地候选清单。命令 `/auth use <n>` 的编号始终对应完整候选列表,不会因为分页变化。
|
|
393
|
+
|
|
394
|
+
每个候选名前的 `窗口:剩余百分比` 来自最近一次观察到的真实额度窗口,例如 Plus 账号可能显示 `5h:20|7d:25`,只有一个月度窗口的账号可能显示 `30d:97`。当前 auth 会在打开面板时刷新额度;其他候选不会为了查询额度被自动切换。如果多个 bot runtime 最近使用过同一个 ChatGPT 账号,FoxClaw 会按已验证的账号 ID 合并它们缓存到的额度快照,因此一个 bot 的 `/auth` 面板可以显示另一个 bot 掌握到的额度信息,同时不会把不同账号混在一起。
|
|
393
395
|
|
|
394
396
|
示意:
|
|
395
397
|
|
|
@@ -398,23 +400,28 @@ Codex auth
|
|
|
398
400
|
Current: auth.json_personal
|
|
399
401
|
Auth dir: /home/alice/.codex
|
|
400
402
|
Candidates: 2
|
|
401
|
-
|
|
402
|
-
1. 20|25|auth.json_personal * [
|
|
403
|
-
2.
|
|
403
|
+
额度剩余:窗口:百分比|auth
|
|
404
|
+
1. 5h:20|7d:25|auth.json_personal * [Plus · 正常 · 刷新于 2小时前]
|
|
405
|
+
2. --|auth.json_team [额度未知]
|
|
404
406
|
|
|
405
|
-
[✅ 20|25|auth.json_personal] [✅]
|
|
406
|
-
[🔐
|
|
407
|
+
[✅ 5h:20|7d:25|auth.json_personal] [✅]
|
|
408
|
+
[🔐 --|auth.json_team] [✅]
|
|
409
|
+
[☑️ 全部] [已启用] [需关注]
|
|
407
410
|
[🛡️ Access] [🔑 设备登录]
|
|
408
411
|
[🔄 Reload auth]
|
|
409
412
|
```
|
|
410
413
|
|
|
411
|
-
右侧 `✅` / `⏸️`
|
|
414
|
+
右侧 `✅` / `⏸️` 表示当前是否参与自动轮转。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--` 表示该候选还没有额度历史快照。健康摘要会区分正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key 和无效 auth 文件。
|
|
412
415
|
|
|
413
416
|
`/auth refresh all` 是仅命令入口的维护操作,因为 ChatGPT refresh token 会被轮换。只有所有 Telegram runtime、微信 runtime、审批、待输入、登录流程和 auth 镜像写入都空闲时才允许执行。命令会先显示风险确认:如果 OpenAI/Codex 已经消费旧 refresh token,但因为网络、进程或磁盘故障导致新 token 没能成功保存,该候选可能需要重新设备登录,甚至重新手机号验证。确认后,它会逐个访问 ChatGPT 候选,让 Codex 通过 `account/read refreshToken=true` 强制刷新 token,再用 usage 接口验证,成功后镜像到其他 bot home,最后恢复原本的当前 auth 并显示摘要。
|
|
414
417
|
|
|
418
|
+
OpenAI 没有公开 ChatGPT refresh token 的固定有效期或旧 token 重放宽限期。Codex 会在 access token 临近到期时自动刷新;如果 access token 里无法解析 `exp`,Codex 当前使用 `last_refresh` 超过约 8 天作为兜底刷新条件。面板把超过 8 天没有刷新记录的候选标为“长期未刷新”,但这只是维护提醒,不代表 refresh token 已经过期,也不会触发批量保活刷新。不要把 `/auth refresh all` 当作日常保活命令。
|
|
419
|
+
|
|
415
420
|
命令等价用法:
|
|
416
421
|
|
|
417
|
-
- `/auth` 或 `/auth list
|
|
422
|
+
- `/auth` 或 `/auth list [关键词]`:查看候选,可按文件名搜索。
|
|
423
|
+
- `/auth filter <all|enabled|attention>`:筛选全部、已启用或需关注候选。
|
|
424
|
+
- `/auth page <页码>`:直接查看指定页。
|
|
418
425
|
- `/auth use <n>`:切到第 n 个候选并重启 app-server。
|
|
419
426
|
- `/auth enable <n>`:让第 n 个候选参与自动轮转。
|
|
420
427
|
- `/auth disable <n>`:禁用第 n 个候选,自动轮转会跳过它。
|
package/package.json
CHANGED
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -67,6 +67,8 @@ Telegram behavior:
|
|
|
67
67
|
- when `TG_BOT_TOKENS` contains multiple bots, one FoxClaw service starts independent Codex app-servers and auth selections per bot
|
|
68
68
|
- isolated Telegram runtimes use independent `CODEX_HOME` directories and file-backed credentials; they do not share Codex sessions
|
|
69
69
|
- refreshed ChatGPT candidates are mirrored only after online usage-endpoint validation; before auth switch or reload, a runtime restores any newer same-account credential found in another Codex home
|
|
70
|
+
- `/auth` paginates large candidate inventories at 8 rows per page, supports all/enabled/attention filters and filename search, and renders the actual observed quota windows rather than assuming every account has 5-hour and 7-day windows
|
|
71
|
+
- treat the `/auth` `not recently refreshed` state as a maintenance hint only: OpenAI does not publish a fixed ChatGPT refresh-token lifetime or replay grace period, and FoxClaw must not add routine bulk keepalive refreshes
|
|
70
72
|
- `/auth refresh all` remains a command-only maintenance action, not a panel button; it requires explicit token-rotation risk confirmation, then force-refreshes every ChatGPT candidate through Codex `account/read refreshToken=true`, validates usage, mirrors successful candidates, and restores the original current auth while every runtime is globally idle
|
|
71
73
|
- if `TG_BOT_TOKEN` is also set to one exact token from `TG_BOT_TOKENS`, that bot uses the default/shared-terminal runtime instead of an isolated Telegram home
|
|
72
74
|
- if Weixin is enabled alongside multiple Telegram bots, it remains on the default Codex runtime instead of borrowing a Telegram bot runtime
|