@foxden-app/foxclaw 0.5.14 → 0.5.16
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 +22 -0
- package/dist/auth/mirror.d.ts +1 -0
- package/dist/auth/mirror.js +39 -0
- package/dist/controller/controller.d.ts +7 -0
- package/dist/controller/controller.js +232 -17
- package/dist/i18n.d.ts +32 -0
- package/dist/i18n.js +32 -0
- package/dist/main.js +2 -0
- package/dist/store/database.d.ts +8 -1
- package/dist/store/database.js +61 -12
- package/docs/user-manual.md +3 -1
- package/docs/zh/user-manual.md +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.5.16 - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- FoxClaw 重启后会把仍在运行的桥接自有 Codex turn 恢复为可继续操作的活动态,状态卡继续刷新,后续 Telegram 输入会继续 steer 或按聊天设置排队,不再退化成需要用户重新发消息的只读观察态。
|
|
9
|
+
- `/watch` 产生的观察态 turn 会在状态卡中持久化只读标记,重启恢复后仍保持只读,避免把旁观线程误恢复成可操作任务。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- After a FoxClaw restart, bridge-owned live Codex turns are restored as actionable active turns: their status cards keep updating, and later Telegram messages keep steering or queueing according to the chat setting instead of degrading into read-only watch mode.
|
|
13
|
+
- `/watch`-created observed turns now persist their read-only marker, so restart recovery keeps watched threads read-only and does not accidentally promote them into actionable tasks.
|
|
14
|
+
|
|
15
|
+
## 0.5.15 - 2026-06-08
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- `/auth` 现在会把已确认不可用、并且本机/跨节点同步恢复失败的候选标记为“需要登录修复”,用 `?` 按钮显示,并从自动轮换、主动刷新和 enabled 视图中排除。
|
|
19
|
+
- 点击 `?` 会进入修复菜单,可选择“登录修复”对该候选执行设备码登录,成功后清除修复状态并重新参与轮换;也可选择“删除”,从 canonical 和所有本机 bot runtime 中删除该候选并清理额度缓存。
|
|
20
|
+
- 删除 auth 候选现在走 auth mirror 统一删除,避免只删一个 runtime 后又被其他 runtime 或 canonical 副本恢复。
|
|
21
|
+
|
|
22
|
+
### English
|
|
23
|
+
- `/auth` now marks candidates that have been proven unusable and could not be recovered through local/cross-node sync as “needs login repair”, shows a `?` action, and excludes them from auto-rotation, proactive refresh, and the enabled filter.
|
|
24
|
+
- Tapping `?` opens a repair menu: Login repair runs device-code login for that candidate and clears the repair state on success; Delete removes the candidate from canonical storage and all local bot runtimes while clearing quota cache.
|
|
25
|
+
- Auth candidate deletion now flows through the auth mirror so deleting a candidate from one runtime is not undone by another runtime or canonical copy.
|
|
26
|
+
|
|
5
27
|
## 0.5.14 - 2026-06-08
|
|
6
28
|
|
|
7
29
|
### 中文
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export declare class AuthCandidateMirror {
|
|
|
84
84
|
readRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorCandidateRecord | null>;
|
|
85
85
|
listNewestCandidates(): Promise<AuthMirrorCandidateRecord[]>;
|
|
86
86
|
syncRuntimeCandidate(runtimeId: string, candidateName: string): Promise<boolean>;
|
|
87
|
+
deleteCandidate(candidateName: string): Promise<boolean>;
|
|
87
88
|
syncAllRuntimeCandidates(): Promise<AuthMirrorSyncAllResult>;
|
|
88
89
|
recoverRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorRecovery | null>;
|
|
89
90
|
private scan;
|
package/dist/auth/mirror.js
CHANGED
|
@@ -126,6 +126,27 @@ export class AuthCandidateMirror {
|
|
|
126
126
|
return false;
|
|
127
127
|
return this.withActivity(() => this.propagateValidatedCandidate(runtime, candidateName));
|
|
128
128
|
}
|
|
129
|
+
async deleteCandidate(candidateName) {
|
|
130
|
+
if (!isAuthCandidateName(candidateName))
|
|
131
|
+
return false;
|
|
132
|
+
return this.withActivity(async () => {
|
|
133
|
+
const directories = [
|
|
134
|
+
this.canonicalDir,
|
|
135
|
+
...this.runtimes.map(runtime => runtime.authDir),
|
|
136
|
+
];
|
|
137
|
+
for (const authDir of directories) {
|
|
138
|
+
await removeAuthCandidate(authDir, candidateName);
|
|
139
|
+
}
|
|
140
|
+
this.lastSyncedRefresh.delete(candidateName);
|
|
141
|
+
for (const key of [...this.lastValidationFailures.keys()]) {
|
|
142
|
+
if (key.endsWith(`:${candidateName}`)) {
|
|
143
|
+
this.lastValidationFailures.delete(key);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.logger.info('auth.mirror.deleted', { candidateName });
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
129
150
|
async syncAllRuntimeCandidates() {
|
|
130
151
|
return this.withActivity(async () => {
|
|
131
152
|
let synced = 0;
|
|
@@ -687,6 +708,24 @@ async function pointAuthSymlink(dir, candidateName) {
|
|
|
687
708
|
await fs.symlink(path.join(dir, candidateName), temporary);
|
|
688
709
|
await fs.rename(temporary, path.join(dir, 'auth.json'));
|
|
689
710
|
}
|
|
711
|
+
async function removeAuthCandidate(dir, candidateName) {
|
|
712
|
+
const candidatePath = path.join(dir, candidateName);
|
|
713
|
+
const authPath = path.join(dir, 'auth.json');
|
|
714
|
+
const currentTarget = await resolveCurrentAuthCandidatePath(authPath);
|
|
715
|
+
await fs.rm(candidatePath, { force: true }).catch(() => undefined);
|
|
716
|
+
if (currentTarget === candidatePath) {
|
|
717
|
+
await fs.rm(authPath, { force: true }).catch(() => undefined);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function resolveCurrentAuthCandidatePath(authPath) {
|
|
721
|
+
const stat = await fs.lstat(authPath).catch(() => null);
|
|
722
|
+
if (!stat)
|
|
723
|
+
return null;
|
|
724
|
+
if (stat.isSymbolicLink()) {
|
|
725
|
+
return resolveFinalPath(authPath);
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
690
729
|
async function removeValidationTempFiles(dir) {
|
|
691
730
|
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
692
731
|
await Promise.all(entries
|
|
@@ -9,6 +9,7 @@ import type { SelfUpdateRuntime, SelfUpdateStatus } from '../update.js';
|
|
|
9
9
|
export interface CoreCoordinator {
|
|
10
10
|
canSelfUpdate?: () => boolean;
|
|
11
11
|
authCandidateUpdated?: (runtimeId: string, candidateName: string) => Promise<void>;
|
|
12
|
+
authCandidateDeleted?: (runtimeId: string, candidateName: string) => Promise<void>;
|
|
12
13
|
recoverAuthCandidate?: (runtimeId: string, candidateName: string, options?: {
|
|
13
14
|
crossNode?: boolean;
|
|
14
15
|
}) => Promise<boolean>;
|
|
@@ -295,6 +296,8 @@ export declare class BridgeSessionCore {
|
|
|
295
296
|
private handleAuthToggleCommand;
|
|
296
297
|
private findLatestAuthChoiceList;
|
|
297
298
|
private handleAuthAddCommand;
|
|
299
|
+
private startAuthRepairLogin;
|
|
300
|
+
private deleteCodexAuthCandidate;
|
|
298
301
|
private handleAccountCommand;
|
|
299
302
|
private handleQuotaCommand;
|
|
300
303
|
private handleQuotaNudgeCommand;
|
|
@@ -331,6 +334,8 @@ export declare class BridgeSessionCore {
|
|
|
331
334
|
private requireReadyBinding;
|
|
332
335
|
private handleAuthPanelActionCallback;
|
|
333
336
|
private handleAuthListViewCallback;
|
|
337
|
+
private handleAuthRepairMenuCallback;
|
|
338
|
+
private handleAuthRepairActionCallback;
|
|
334
339
|
private handleAuthToggleCallback;
|
|
335
340
|
private handleAuthSwitchCallback;
|
|
336
341
|
private maybeRunPendingAuthRotation;
|
|
@@ -342,6 +347,8 @@ export declare class BridgeSessionCore {
|
|
|
342
347
|
private codexAuthSwitchParams;
|
|
343
348
|
private switchCodexAuthAndRestart;
|
|
344
349
|
private recoverCodexAuthCandidate;
|
|
350
|
+
private markCodexAuthCandidateNeedsRepair;
|
|
351
|
+
private markCodexAuthCandidateActive;
|
|
345
352
|
private syncCodexAuthCandidate;
|
|
346
353
|
private refreshAllCodexAuthCandidates;
|
|
347
354
|
private refreshCodexAuthCandidates;
|
|
@@ -1045,6 +1045,16 @@ export class BridgeSessionCore {
|
|
|
1045
1045
|
await this.handleSettingsCallback(event, settingsMatch[1], settingsMatch[2], locale);
|
|
1046
1046
|
return;
|
|
1047
1047
|
}
|
|
1048
|
+
const authRepairActionMatch = /^auth:([a-f0-9]+):repair_(login|delete|cancel):(\d+)$/.exec(event.data);
|
|
1049
|
+
if (authRepairActionMatch) {
|
|
1050
|
+
await this.handleAuthRepairActionCallback(event, authRepairActionMatch[1], authRepairActionMatch[2], Number.parseInt(authRepairActionMatch[3], 10), locale);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const authRepairMatch = /^auth:([a-f0-9]+):repair:(\d+)$/.exec(event.data);
|
|
1054
|
+
if (authRepairMatch) {
|
|
1055
|
+
await this.handleAuthRepairMenuCallback(event, authRepairMatch[1], Number.parseInt(authRepairMatch[2], 10), locale);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1048
1058
|
const authToggleMatch = /^auth:([a-f0-9]+):toggle:(\d+)$/.exec(event.data);
|
|
1049
1059
|
if (authToggleMatch) {
|
|
1050
1060
|
await this.handleAuthToggleCallback(event, authToggleMatch[1], Number.parseInt(authToggleMatch[2], 10), locale);
|
|
@@ -1657,8 +1667,11 @@ export class BridgeSessionCore {
|
|
|
1657
1667
|
if (!success) {
|
|
1658
1668
|
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
1659
1669
|
await this.sendMessage(scopeId, [
|
|
1660
|
-
t(locale, '
|
|
1661
|
-
|
|
1670
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_failed' : 'auth_add_failed', {
|
|
1671
|
+
value: pendingAuthAdd.name,
|
|
1672
|
+
error: params?.error ?? t(locale, 'unknown'),
|
|
1673
|
+
}),
|
|
1674
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_reverted' : 'auth_add_reverted'),
|
|
1662
1675
|
].join('\n'));
|
|
1663
1676
|
return;
|
|
1664
1677
|
}
|
|
@@ -1666,12 +1679,26 @@ export class BridgeSessionCore {
|
|
|
1666
1679
|
if (!stat?.isFile()) {
|
|
1667
1680
|
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
1668
1681
|
await this.sendMessage(scopeId, [
|
|
1669
|
-
t(locale, 'auth_add_missing_file', { value: pendingAuthAdd.name }),
|
|
1670
|
-
t(locale, 'auth_add_reverted'),
|
|
1682
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_missing_file' : 'auth_add_missing_file', { value: pendingAuthAdd.name }),
|
|
1683
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_reverted' : 'auth_add_reverted'),
|
|
1671
1684
|
].join('\n'));
|
|
1672
1685
|
return;
|
|
1673
1686
|
}
|
|
1674
|
-
|
|
1687
|
+
if (pendingAuthAdd.mode === 'repair') {
|
|
1688
|
+
const metadata = await readChatGptAuthMetadata(pendingAuthAdd.path);
|
|
1689
|
+
if (!metadata || !chatGptAuthMetadataMatchesCandidateName(pendingAuthAdd.name, metadata)) {
|
|
1690
|
+
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
1691
|
+
await this.sendMessage(scopeId, [
|
|
1692
|
+
t(locale, 'auth_repair_identity_mismatch', { value: pendingAuthAdd.name }),
|
|
1693
|
+
t(locale, 'auth_repair_reverted'),
|
|
1694
|
+
].join('\n'));
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
this.markCodexAuthCandidateActive(pendingAuthAdd.name);
|
|
1698
|
+
this.store.setCodexAuthCandidateDisabled(pendingAuthAdd.name, false);
|
|
1699
|
+
this.store.setCodexAuthCandidateDisabled(pendingAuthAdd.name, false, this.authRuntimeId());
|
|
1700
|
+
}
|
|
1701
|
+
const lines = [t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_done' : 'auth_add_done', { value: pendingAuthAdd.name })];
|
|
1675
1702
|
try {
|
|
1676
1703
|
await this.coordinator?.authCandidateUpdated?.(this.authRuntimeId(), pendingAuthAdd.name);
|
|
1677
1704
|
}
|
|
@@ -1692,6 +1719,7 @@ export class BridgeSessionCore {
|
|
|
1692
1719
|
}
|
|
1693
1720
|
const currentCandidate = (await this.listCodexAuthState()).candidates.find(candidate => candidate.isCurrent) ?? null;
|
|
1694
1721
|
if (currentCandidate) {
|
|
1722
|
+
this.markCodexAuthCandidateActive(currentCandidate.name);
|
|
1695
1723
|
await this.syncCodexAuthCandidate(currentCandidate.name);
|
|
1696
1724
|
}
|
|
1697
1725
|
await this.sendMessage(scopeId, t(locale, 'login_completed'));
|
|
@@ -2834,6 +2862,7 @@ export class BridgeSessionCore {
|
|
|
2834
2862
|
scopeId,
|
|
2835
2863
|
threadId,
|
|
2836
2864
|
messageId: previewMessageId,
|
|
2865
|
+
isObserved: active.isObserved,
|
|
2837
2866
|
});
|
|
2838
2867
|
}
|
|
2839
2868
|
this.updateStatus();
|
|
@@ -4340,6 +4369,7 @@ export class BridgeSessionCore {
|
|
|
4340
4369
|
}
|
|
4341
4370
|
const state = await this.listCodexAuthState();
|
|
4342
4371
|
const dueCandidates = state.candidates.filter(candidate => !candidate.disabled
|
|
4372
|
+
&& candidate.state !== 'needs_repair'
|
|
4343
4373
|
&& candidate.credentialKind === 'chatgpt'
|
|
4344
4374
|
&& candidate.credentialLastRefreshMs !== null
|
|
4345
4375
|
&& candidate.credentialLastRefreshMs <= Date.now() - CODEX_AUTH_PROACTIVE_REFRESH_DAYS * 24 * 60 * 60_000);
|
|
@@ -4553,6 +4583,10 @@ export class BridgeSessionCore {
|
|
|
4553
4583
|
await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null));
|
|
4554
4584
|
return;
|
|
4555
4585
|
}
|
|
4586
|
+
if (candidate.state === 'needs_repair') {
|
|
4587
|
+
await this.sendMessage(scopeId, t(locale, 'auth_candidate_needs_repair', { value: candidate.name }));
|
|
4588
|
+
return;
|
|
4589
|
+
}
|
|
4556
4590
|
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
4557
4591
|
await this.sendMessage(scopeId, t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel)));
|
|
4558
4592
|
await this.switchCodexAuthAndRestart(scopeId, locale, candidate, false);
|
|
@@ -4625,6 +4659,7 @@ export class BridgeSessionCore {
|
|
|
4625
4659
|
name: candidateName,
|
|
4626
4660
|
path: targetPath,
|
|
4627
4661
|
previousTargetPath: state.currentTargetPath,
|
|
4662
|
+
mode: 'add',
|
|
4628
4663
|
createdAt: Date.now(),
|
|
4629
4664
|
});
|
|
4630
4665
|
await this.sendMessage(scopeId, [
|
|
@@ -4641,6 +4676,85 @@ export class BridgeSessionCore {
|
|
|
4641
4676
|
throw error;
|
|
4642
4677
|
}
|
|
4643
4678
|
}
|
|
4679
|
+
async startAuthRepairLogin(scopeId, locale, candidate) {
|
|
4680
|
+
const state = await this.listCodexAuthState();
|
|
4681
|
+
const target = state.candidates.find(entry => entry.name === candidate.name) ?? null;
|
|
4682
|
+
if (!target) {
|
|
4683
|
+
await this.sendMessage(scopeId, t(locale, 'auth_choice_expired'));
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
await pointCodexAuthAtTarget(state.authDir, state.authPath, target.path);
|
|
4687
|
+
this.pendingTurnErrors.clear();
|
|
4688
|
+
this.attachedThreads.clear();
|
|
4689
|
+
try {
|
|
4690
|
+
await this.app.restart();
|
|
4691
|
+
const login = await this.app.startDeviceLogin();
|
|
4692
|
+
const oldLoginId = this.pendingLoginsByScope.get(scopeId);
|
|
4693
|
+
if (oldLoginId) {
|
|
4694
|
+
this.pendingLoginScopesById.delete(oldLoginId);
|
|
4695
|
+
this.pendingAuthAddsByLoginId.delete(oldLoginId);
|
|
4696
|
+
}
|
|
4697
|
+
this.pendingLoginsByScope.set(scopeId, login.loginId);
|
|
4698
|
+
this.pendingLoginScopesById.set(login.loginId, scopeId);
|
|
4699
|
+
this.pendingAuthAddsByLoginId.set(login.loginId, {
|
|
4700
|
+
loginId: login.loginId,
|
|
4701
|
+
scopeId,
|
|
4702
|
+
name: target.name,
|
|
4703
|
+
path: target.path,
|
|
4704
|
+
previousTargetPath: state.currentTargetPath,
|
|
4705
|
+
mode: 'repair',
|
|
4706
|
+
createdAt: Date.now(),
|
|
4707
|
+
});
|
|
4708
|
+
await this.sendMessage(scopeId, [
|
|
4709
|
+
t(locale, 'auth_repair_started', { value: target.name }),
|
|
4710
|
+
t(locale, 'login_device_prereq'),
|
|
4711
|
+
t(locale, 'login_url', { value: login.verificationUrl }),
|
|
4712
|
+
t(locale, 'login_code', { value: login.userCode }),
|
|
4713
|
+
t(locale, 'login_id', { value: login.loginId }),
|
|
4714
|
+
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
4715
|
+
].join('\n'));
|
|
4716
|
+
}
|
|
4717
|
+
catch (error) {
|
|
4718
|
+
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, state.currentTargetPath);
|
|
4719
|
+
throw error;
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
async deleteCodexAuthCandidate(candidate) {
|
|
4723
|
+
const wasCurrent = candidate.isCurrent;
|
|
4724
|
+
const authDir = this.resolveAuthDir();
|
|
4725
|
+
const authPath = path.join(authDir, 'auth.json');
|
|
4726
|
+
let deletedByCoordinator = false;
|
|
4727
|
+
try {
|
|
4728
|
+
await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name);
|
|
4729
|
+
deletedByCoordinator = Boolean(this.coordinator?.authCandidateDeleted);
|
|
4730
|
+
}
|
|
4731
|
+
catch (error) {
|
|
4732
|
+
this.logger.warn('codex.auth_candidate_delete_sync_failed', {
|
|
4733
|
+
candidate: candidate.name,
|
|
4734
|
+
runtimeId: this.authRuntimeId(),
|
|
4735
|
+
error: toErrorMeta(error),
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
if (!deletedByCoordinator) {
|
|
4739
|
+
await fs.rm(candidate.path, { force: true }).catch(() => undefined);
|
|
4740
|
+
if (wasCurrent) {
|
|
4741
|
+
await fs.rm(authPath, { force: true }).catch(() => undefined);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
this.store.deleteCodexAuthCandidate(candidate.name);
|
|
4745
|
+
this.authRotationFailedTargets.delete(candidate.path);
|
|
4746
|
+
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4747
|
+
if (Object.prototype.hasOwnProperty.call(snapshots, candidate.name)) {
|
|
4748
|
+
delete snapshots[candidate.name];
|
|
4749
|
+
await this.writeCodexAuthQuotaSnapshots();
|
|
4750
|
+
}
|
|
4751
|
+
if (wasCurrent) {
|
|
4752
|
+
this.pendingTurnErrors.clear();
|
|
4753
|
+
this.attachedThreads.clear();
|
|
4754
|
+
await this.app.restart();
|
|
4755
|
+
}
|
|
4756
|
+
return wasCurrent;
|
|
4757
|
+
}
|
|
4644
4758
|
async handleAccountCommand(scopeId, locale) {
|
|
4645
4759
|
const account = await this.app.readAccount();
|
|
4646
4760
|
const lines = [
|
|
@@ -5126,6 +5240,74 @@ export class BridgeSessionCore {
|
|
|
5126
5240
|
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
5127
5241
|
}
|
|
5128
5242
|
}
|
|
5243
|
+
async handleAuthRepairMenuCallback(event, localId, index, locale) {
|
|
5244
|
+
const record = this.pendingAuthChoiceLists.get(localId);
|
|
5245
|
+
if (!record) {
|
|
5246
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_expired'));
|
|
5247
|
+
return;
|
|
5248
|
+
}
|
|
5249
|
+
if (record.chatId !== event.scopeId || (record.messageId !== null && record.messageId !== event.messageId)) {
|
|
5250
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_mismatch'));
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
const candidate = record.candidates[index];
|
|
5254
|
+
if (!candidate) {
|
|
5255
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
5256
|
+
return;
|
|
5257
|
+
}
|
|
5258
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_repair_actions_short'));
|
|
5259
|
+
if (record.messageId !== null) {
|
|
5260
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_actions_message', { value: formatCodexAuthCandidateDisplayName(candidate.name) }), authRepairKeyboard(locale, record, index));
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
async handleAuthRepairActionCallback(event, localId, action, index, locale) {
|
|
5264
|
+
const record = this.pendingAuthChoiceLists.get(localId);
|
|
5265
|
+
if (!record) {
|
|
5266
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_expired'));
|
|
5267
|
+
return;
|
|
5268
|
+
}
|
|
5269
|
+
if (record.chatId !== event.scopeId || (record.messageId !== null && record.messageId !== event.messageId)) {
|
|
5270
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_mismatch'));
|
|
5271
|
+
return;
|
|
5272
|
+
}
|
|
5273
|
+
const candidate = record.candidates[index];
|
|
5274
|
+
if (!candidate) {
|
|
5275
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
5276
|
+
return;
|
|
5277
|
+
}
|
|
5278
|
+
if (action === 'cancel') {
|
|
5279
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
5280
|
+
const state = await this.listCodexAuthState();
|
|
5281
|
+
record.candidates = state.candidates;
|
|
5282
|
+
record.createdAt = Date.now();
|
|
5283
|
+
clampCodexAuthListOffset(record);
|
|
5284
|
+
if (record.messageId !== null) {
|
|
5285
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
5286
|
+
}
|
|
5287
|
+
return;
|
|
5288
|
+
}
|
|
5289
|
+
if (this.hasLocalBlockingActivity()) {
|
|
5290
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_reload_blocked_active'));
|
|
5291
|
+
return;
|
|
5292
|
+
}
|
|
5293
|
+
if (action === 'login') {
|
|
5294
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'login_device_started'));
|
|
5295
|
+
if (record.messageId !== null) {
|
|
5296
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_login_preparing', { value: candidate.name }), []);
|
|
5297
|
+
}
|
|
5298
|
+
await this.startAuthRepairLogin(event.scopeId, locale, candidate);
|
|
5299
|
+
return;
|
|
5300
|
+
}
|
|
5301
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_candidate_deleted_short'));
|
|
5302
|
+
const restarted = await this.deleteCodexAuthCandidate(candidate);
|
|
5303
|
+
const state = await this.listCodexAuthState();
|
|
5304
|
+
record.candidates = state.candidates;
|
|
5305
|
+
record.createdAt = Date.now();
|
|
5306
|
+
clampCodexAuthListOffset(record);
|
|
5307
|
+
if (record.messageId !== null) {
|
|
5308
|
+
await this.editMessage(event.scopeId, record.messageId, `${t(locale, 'auth_candidate_deleted', { value: candidate.name })}${restarted ? `\n${t(locale, 'auth_delete_current_restarted')}` : ''}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5129
5311
|
async handleAuthToggleCallback(event, localId, index, locale) {
|
|
5130
5312
|
const record = this.pendingAuthChoiceLists.get(localId);
|
|
5131
5313
|
if (!record) {
|
|
@@ -5170,6 +5352,10 @@ export class BridgeSessionCore {
|
|
|
5170
5352
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
5171
5353
|
return;
|
|
5172
5354
|
}
|
|
5355
|
+
if (candidate.state === 'needs_repair') {
|
|
5356
|
+
await this.handleAuthRepairMenuCallback(event, localId, index, locale);
|
|
5357
|
+
return;
|
|
5358
|
+
}
|
|
5173
5359
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_recorded'));
|
|
5174
5360
|
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
5175
5361
|
const switchingMessage = t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel));
|
|
@@ -5217,6 +5403,7 @@ export class BridgeSessionCore {
|
|
|
5217
5403
|
}
|
|
5218
5404
|
return false;
|
|
5219
5405
|
}
|
|
5406
|
+
this.markCodexAuthCandidateNeedsRepair(current.name);
|
|
5220
5407
|
}
|
|
5221
5408
|
const selection = await this.selectNextCodexAuthCandidate(failedTargets);
|
|
5222
5409
|
if (!selection) {
|
|
@@ -5291,7 +5478,9 @@ export class BridgeSessionCore {
|
|
|
5291
5478
|
if (state.currentTargetPath) {
|
|
5292
5479
|
failedTargets.add(state.currentTargetPath);
|
|
5293
5480
|
}
|
|
5294
|
-
const candidates = state.candidates.filter(candidate => !candidate.disabled
|
|
5481
|
+
const candidates = state.candidates.filter(candidate => !candidate.disabled
|
|
5482
|
+
&& candidate.state !== 'needs_repair'
|
|
5483
|
+
&& !failedTargets.has(candidate.path));
|
|
5295
5484
|
if (candidates.length === 0) {
|
|
5296
5485
|
return null;
|
|
5297
5486
|
}
|
|
@@ -5300,7 +5489,10 @@ export class BridgeSessionCore {
|
|
|
5300
5489
|
: -1;
|
|
5301
5490
|
for (let offset = 1; offset <= state.candidates.length; offset += 1) {
|
|
5302
5491
|
const candidate = state.candidates[(currentIndex + offset + state.candidates.length) % state.candidates.length];
|
|
5303
|
-
if (candidate
|
|
5492
|
+
if (candidate
|
|
5493
|
+
&& !candidate.disabled
|
|
5494
|
+
&& candidate.state !== 'needs_repair'
|
|
5495
|
+
&& !failedTargets.has(candidate.path)) {
|
|
5304
5496
|
return { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) };
|
|
5305
5497
|
}
|
|
5306
5498
|
}
|
|
@@ -5308,7 +5500,7 @@ export class BridgeSessionCore {
|
|
|
5308
5500
|
return candidate ? { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) } : null;
|
|
5309
5501
|
}
|
|
5310
5502
|
async listCodexAuthState() {
|
|
5311
|
-
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.resolveAuthDir());
|
|
5503
|
+
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.store.listCodexAuthCandidateStates(this.authRuntimeId()), this.resolveAuthDir());
|
|
5312
5504
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
5313
5505
|
const candidateQuotaIdentities = await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
|
|
5314
5506
|
state.candidates.forEach((candidate) => {
|
|
@@ -5374,6 +5566,12 @@ export class BridgeSessionCore {
|
|
|
5374
5566
|
return false;
|
|
5375
5567
|
}
|
|
5376
5568
|
}
|
|
5569
|
+
markCodexAuthCandidateNeedsRepair(candidateName) {
|
|
5570
|
+
this.store.setCodexAuthCandidateState(candidateName, 'needs_repair');
|
|
5571
|
+
}
|
|
5572
|
+
markCodexAuthCandidateActive(candidateName) {
|
|
5573
|
+
this.store.setCodexAuthCandidateState(candidateName, 'active');
|
|
5574
|
+
}
|
|
5377
5575
|
async syncCodexAuthCandidate(candidateName) {
|
|
5378
5576
|
try {
|
|
5379
5577
|
await this.coordinator?.authCandidateUpdated?.(this.authRuntimeId(), candidateName);
|
|
@@ -5410,7 +5608,7 @@ export class BridgeSessionCore {
|
|
|
5410
5608
|
try {
|
|
5411
5609
|
for (const candidate of candidates) {
|
|
5412
5610
|
const before = await readChatGptAuthMetadata(candidate.path);
|
|
5413
|
-
if (!before || !chatGptAuthMetadataMatchesCandidateName(candidate.name, before)) {
|
|
5611
|
+
if (candidate.disabled || candidate.state === 'needs_repair' || !before || !chatGptAuthMetadataMatchesCandidateName(candidate.name, before)) {
|
|
5414
5612
|
result.skipped.push(candidate.name);
|
|
5415
5613
|
continue;
|
|
5416
5614
|
}
|
|
@@ -5655,6 +5853,9 @@ export class BridgeSessionCore {
|
|
|
5655
5853
|
if (!candidate) {
|
|
5656
5854
|
return;
|
|
5657
5855
|
}
|
|
5856
|
+
if (candidate.state === 'needs_repair') {
|
|
5857
|
+
return;
|
|
5858
|
+
}
|
|
5658
5859
|
try {
|
|
5659
5860
|
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
5660
5861
|
if (!metadata || !chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)) {
|
|
@@ -6746,7 +6947,7 @@ export class BridgeSessionCore {
|
|
|
6746
6947
|
return this.interruptOrphanWaitingUserInput(preview);
|
|
6747
6948
|
}
|
|
6748
6949
|
await this.stopWatchingScopeThread(preview.scopeId, preview.threadId);
|
|
6749
|
-
const active = this.createActiveTurnState(preview.scopeId, target.chatId, target.chatType, target.topicId, preview.threadId, preview.turnId, preview.messageId,
|
|
6950
|
+
const active = this.createActiveTurnState(preview.scopeId, target.chatId, target.chatType, target.topicId, preview.threadId, preview.turnId, preview.messageId, preview.isObserved);
|
|
6750
6951
|
this.setActiveTurn(preview.scopeId, preview.turnId, active);
|
|
6751
6952
|
const watcher = {
|
|
6752
6953
|
scopeId: preview.scopeId,
|
|
@@ -6773,6 +6974,7 @@ export class BridgeSessionCore {
|
|
|
6773
6974
|
scopeId: preview.scopeId,
|
|
6774
6975
|
threadId: preview.threadId,
|
|
6775
6976
|
turnId: preview.turnId,
|
|
6977
|
+
isObserved: preview.isObserved,
|
|
6776
6978
|
});
|
|
6777
6979
|
return true;
|
|
6778
6980
|
}
|
|
@@ -6953,6 +7155,7 @@ export class BridgeSessionCore {
|
|
|
6953
7155
|
scopeId: active.scopeId,
|
|
6954
7156
|
threadId: active.threadId,
|
|
6955
7157
|
messageId,
|
|
7158
|
+
isObserved: active.isObserved,
|
|
6956
7159
|
});
|
|
6957
7160
|
}
|
|
6958
7161
|
catch (error) {
|
|
@@ -8234,7 +8437,7 @@ function parseActiveTurnKey(key) {
|
|
|
8234
8437
|
function codexAuthDir(explicitAuthDir = null) {
|
|
8235
8438
|
return explicitAuthDir || process.env.CODEX_AUTH_DIR || path.join(os.homedir(), '.codex');
|
|
8236
8439
|
}
|
|
8237
|
-
async function listCodexAuthState(disabledNames = new Set(), explicitAuthDir = null) {
|
|
8440
|
+
async function listCodexAuthState(disabledNames = new Set(), candidateStates = new Map(), explicitAuthDir = null) {
|
|
8238
8441
|
const authDir = codexAuthDir(explicitAuthDir);
|
|
8239
8442
|
const authPath = path.join(authDir, 'auth.json');
|
|
8240
8443
|
const currentTargetPath = await resolveCurrentAuthTarget(authDir, authPath);
|
|
@@ -8259,6 +8462,7 @@ async function listCodexAuthState(disabledNames = new Set(), explicitAuthDir = n
|
|
|
8259
8462
|
path: candidatePath,
|
|
8260
8463
|
isCurrent: currentTargetPath === candidatePath,
|
|
8261
8464
|
disabled: disabledNames.has(entry.name),
|
|
8465
|
+
state: candidateStates.get(entry.name) ?? 'active',
|
|
8262
8466
|
mtimeMs: stat.mtimeMs,
|
|
8263
8467
|
credentialKind: 'invalid',
|
|
8264
8468
|
credentialLastRefreshMs: null,
|
|
@@ -8370,7 +8574,7 @@ async function restoreCodexAuthTarget(authDir, authPath, targetPath, regularAuth
|
|
|
8370
8574
|
});
|
|
8371
8575
|
}
|
|
8372
8576
|
async function switchCodexAuth(targetPath, explicitAuthDir = null) {
|
|
8373
|
-
const state = await listCodexAuthState(new Set(), explicitAuthDir);
|
|
8577
|
+
const state = await listCodexAuthState(new Set(), new Map(), explicitAuthDir);
|
|
8374
8578
|
const candidate = state.candidates.find(entry => entry.path === targetPath);
|
|
8375
8579
|
if (!candidate) {
|
|
8376
8580
|
throw new Error(`Auth candidate is no longer available: ${path.basename(targetPath)}`);
|
|
@@ -8446,7 +8650,7 @@ function filterCodexAuthCandidates(candidates, view) {
|
|
|
8446
8650
|
.map((candidate, index) => ({ candidate, index }))
|
|
8447
8651
|
.filter(({ candidate }) => !searchTerm || candidate.name.toLowerCase().includes(searchTerm))
|
|
8448
8652
|
.filter(({ candidate }) => view.filter === 'all'
|
|
8449
|
-
|| (view.filter === 'enabled' && !candidate.disabled)
|
|
8653
|
+
|| (view.filter === 'enabled' && !candidate.disabled && candidate.state !== 'needs_repair')
|
|
8450
8654
|
|| (view.filter === 'attention' && codexAuthCandidateNeedsAttention(candidate)));
|
|
8451
8655
|
}
|
|
8452
8656
|
function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopyPaste = false, view = null) {
|
|
@@ -8505,12 +8709,12 @@ function authChoiceKeyboard(locale, record) {
|
|
|
8505
8709
|
const page = codexAuthListPage(record.candidates, record);
|
|
8506
8710
|
const rows = page.visible.map(({ candidate, index }) => [
|
|
8507
8711
|
{
|
|
8508
|
-
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaButtonPrefix(candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${candidate.disabled ? ' · off' : ''}`),
|
|
8509
|
-
callback_data: `auth:${record.localId}:${index}`,
|
|
8712
|
+
text: clipButtonText(`${candidate.state === 'needs_repair' ? '? ' : candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaButtonPrefix(candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${candidate.disabled ? ' · off' : ''}`),
|
|
8713
|
+
callback_data: candidate.state === 'needs_repair' ? `auth:${record.localId}:repair:${index}` : `auth:${record.localId}:${index}`,
|
|
8510
8714
|
},
|
|
8511
8715
|
{
|
|
8512
|
-
text: t(locale, candidate.disabled ? 'button_auth_disable' : 'button_auth_enable'),
|
|
8513
|
-
callback_data: `auth:${record.localId}:toggle:${index}`,
|
|
8716
|
+
text: candidate.state === 'needs_repair' ? '?' : t(locale, candidate.disabled ? 'button_auth_disable' : 'button_auth_enable'),
|
|
8717
|
+
callback_data: candidate.state === 'needs_repair' ? `auth:${record.localId}:repair:${index}` : `auth:${record.localId}:toggle:${index}`,
|
|
8514
8718
|
},
|
|
8515
8719
|
]);
|
|
8516
8720
|
const navigationRow = [];
|
|
@@ -8555,6 +8759,9 @@ function authFilterButton(locale, record, filter) {
|
|
|
8555
8759
|
};
|
|
8556
8760
|
}
|
|
8557
8761
|
function codexAuthCandidateHealth(candidate) {
|
|
8762
|
+
if (candidate.state === 'needs_repair') {
|
|
8763
|
+
return 'needs_repair';
|
|
8764
|
+
}
|
|
8558
8765
|
if (candidate.disabled) {
|
|
8559
8766
|
return 'disabled';
|
|
8560
8767
|
}
|
|
@@ -8589,6 +8796,7 @@ function codexAuthCandidateNeedsAttention(candidate) {
|
|
|
8589
8796
|
|| health === 'unknown'
|
|
8590
8797
|
|| health === 'low'
|
|
8591
8798
|
|| health === 'exhausted'
|
|
8799
|
+
|| health === 'needs_repair'
|
|
8592
8800
|
|| health === 'invalid';
|
|
8593
8801
|
}
|
|
8594
8802
|
function formatCodexAuthCandidateStatus(locale, candidate) {
|
|
@@ -8613,6 +8821,13 @@ function authRefreshAllConfirmKeyboard(locale, record) {
|
|
|
8613
8821
|
[{ text: t(locale, 'button_cancel'), callback_data: `auth:${record.localId}:refresh_all_cancel` }],
|
|
8614
8822
|
];
|
|
8615
8823
|
}
|
|
8824
|
+
function authRepairKeyboard(locale, record, index) {
|
|
8825
|
+
return [
|
|
8826
|
+
[{ text: t(locale, 'button_auth_repair_login'), callback_data: `auth:${record.localId}:repair_login:${index}` }],
|
|
8827
|
+
[{ text: t(locale, 'button_auth_delete'), callback_data: `auth:${record.localId}:repair_delete:${index}` }],
|
|
8828
|
+
[{ text: t(locale, 'button_cancel'), callback_data: `auth:${record.localId}:repair_cancel:${index}` }],
|
|
8829
|
+
];
|
|
8830
|
+
}
|
|
8616
8831
|
function formatAuthRefreshAllResult(locale, result, mode = 'manual') {
|
|
8617
8832
|
const lines = [t(locale, mode === 'proactive' ? 'auth_proactive_refresh_done' : 'auth_refresh_all_done', {
|
|
8618
8833
|
refreshed: String(result.refreshed.length),
|
package/dist/i18n.d.ts
CHANGED
|
@@ -162,6 +162,7 @@ declare const MESSAGES: {
|
|
|
162
162
|
readonly auth_candidate_status_enabled: "enabled";
|
|
163
163
|
readonly auth_candidate_status_disabled: "disabled";
|
|
164
164
|
readonly auth_candidate_health_disabled: "disabled";
|
|
165
|
+
readonly auth_candidate_health_needs_repair: "needs login repair";
|
|
165
166
|
readonly auth_candidate_health_ready: "ready";
|
|
166
167
|
readonly auth_candidate_health_low: "low quota";
|
|
167
168
|
readonly auth_candidate_health_exhausted: "quota exhausted";
|
|
@@ -174,6 +175,19 @@ declare const MESSAGES: {
|
|
|
174
175
|
readonly auth_candidate_disabled: "Disabled auth candidate for auto-rotation: {value}";
|
|
175
176
|
readonly auth_candidate_enabled_short: "Auth enabled";
|
|
176
177
|
readonly auth_candidate_disabled_short: "Auth disabled";
|
|
178
|
+
readonly auth_candidate_needs_repair: "{value} needs login repair. Open /auth and use the ? action to repair or delete it.";
|
|
179
|
+
readonly auth_repair_actions_short: "Repair actions";
|
|
180
|
+
readonly auth_repair_actions_message: "{value} has been verified unusable and could not be recovered from sync. Choose an action.";
|
|
181
|
+
readonly auth_repair_login_preparing: "Preparing device login repair for {value}...";
|
|
182
|
+
readonly auth_repair_started: "Device login repair started for {value}.";
|
|
183
|
+
readonly auth_repair_done: "Auth candidate repaired: {value}";
|
|
184
|
+
readonly auth_repair_failed: "Auth repair failed for {value}: {error}";
|
|
185
|
+
readonly auth_repair_missing_file: "Auth repair finished but {value} was not written.";
|
|
186
|
+
readonly auth_repair_identity_mismatch: "Auth repair finished, but the logged-in account does not match {value}.";
|
|
187
|
+
readonly auth_repair_reverted: "Restored previous auth. The candidate still needs repair.";
|
|
188
|
+
readonly auth_candidate_deleted: "Deleted auth candidate: {value}";
|
|
189
|
+
readonly auth_candidate_deleted_short: "Auth deleted";
|
|
190
|
+
readonly auth_delete_current_restarted: "The deleted candidate was current, so Codex app-server was restarted without it.";
|
|
177
191
|
readonly auth_no_candidates: "No auth candidates found. Expected files like auth.json_personal in the auth dir.";
|
|
178
192
|
readonly auth_choice_expired: "This auth list is no longer active";
|
|
179
193
|
readonly auth_choice_mismatch: "Auth choice does not match this message";
|
|
@@ -239,6 +253,8 @@ declare const MESSAGES: {
|
|
|
239
253
|
readonly button_auth_reload: "🔄 Reload auth";
|
|
240
254
|
readonly button_auth_safe_sync: "🧷 Safe sync";
|
|
241
255
|
readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
|
|
256
|
+
readonly button_auth_repair_login: "🔑 Login repair";
|
|
257
|
+
readonly button_auth_delete: "🗑️ Delete";
|
|
242
258
|
readonly button_auth_enable: "✅";
|
|
243
259
|
readonly button_auth_disable: "⏸️";
|
|
244
260
|
readonly button_auth_filter_all: "All";
|
|
@@ -818,6 +834,7 @@ declare const MESSAGES: {
|
|
|
818
834
|
readonly auth_candidate_status_enabled: "启用";
|
|
819
835
|
readonly auth_candidate_status_disabled: "禁用";
|
|
820
836
|
readonly auth_candidate_health_disabled: "已禁用";
|
|
837
|
+
readonly auth_candidate_health_needs_repair: "需要登录修复";
|
|
821
838
|
readonly auth_candidate_health_ready: "正常";
|
|
822
839
|
readonly auth_candidate_health_low: "额度偏低";
|
|
823
840
|
readonly auth_candidate_health_exhausted: "额度耗尽";
|
|
@@ -830,6 +847,19 @@ declare const MESSAGES: {
|
|
|
830
847
|
readonly auth_candidate_disabled: "已禁用 auth 候选自动轮换:{value}";
|
|
831
848
|
readonly auth_candidate_enabled_short: "auth 已启用";
|
|
832
849
|
readonly auth_candidate_disabled_short: "auth 已禁用";
|
|
850
|
+
readonly auth_candidate_needs_repair: "{value} 需要登录修复。打开 /auth 后使用 ? 操作修复或删除。";
|
|
851
|
+
readonly auth_repair_actions_short: "修复操作";
|
|
852
|
+
readonly auth_repair_actions_message: "{value} 已确认不可用,并且同步恢复失败。请选择处理方式。";
|
|
853
|
+
readonly auth_repair_login_preparing: "正在准备为 {value} 进行设备登录修复...";
|
|
854
|
+
readonly auth_repair_started: "已开始为 {value} 进行设备登录修复。";
|
|
855
|
+
readonly auth_repair_done: "auth 候选已修复:{value}";
|
|
856
|
+
readonly auth_repair_failed: "{value} 修复失败:{error}";
|
|
857
|
+
readonly auth_repair_missing_file: "修复流程已结束,但没有写入 {value}。";
|
|
858
|
+
readonly auth_repair_identity_mismatch: "修复流程已结束,但登录账号与 {value} 不匹配。";
|
|
859
|
+
readonly auth_repair_reverted: "已恢复之前的 auth。该候选仍需要修复。";
|
|
860
|
+
readonly auth_candidate_deleted: "已删除 auth 候选:{value}";
|
|
861
|
+
readonly auth_candidate_deleted_short: "auth 已删除";
|
|
862
|
+
readonly auth_delete_current_restarted: "被删除的候选是当前 auth,已在删除后重启 Codex app-server。";
|
|
833
863
|
readonly auth_no_candidates: "没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。";
|
|
834
864
|
readonly auth_choice_expired: "这个 auth 列表已经不再有效";
|
|
835
865
|
readonly auth_choice_mismatch: "这个 auth 选择按钮不属于当前消息";
|
|
@@ -895,6 +925,8 @@ declare const MESSAGES: {
|
|
|
895
925
|
readonly button_auth_reload: "🔄 重载 auth";
|
|
896
926
|
readonly button_auth_safe_sync: "🧷 安全同步";
|
|
897
927
|
readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
|
|
928
|
+
readonly button_auth_repair_login: "🔑 登录修复";
|
|
929
|
+
readonly button_auth_delete: "🗑️ 删除";
|
|
898
930
|
readonly button_auth_enable: "✅";
|
|
899
931
|
readonly button_auth_disable: "⏸️";
|
|
900
932
|
readonly button_auth_filter_all: "全部";
|
package/dist/i18n.js
CHANGED
|
@@ -160,6 +160,7 @@ const MESSAGES = {
|
|
|
160
160
|
auth_candidate_status_enabled: 'enabled',
|
|
161
161
|
auth_candidate_status_disabled: 'disabled',
|
|
162
162
|
auth_candidate_health_disabled: 'disabled',
|
|
163
|
+
auth_candidate_health_needs_repair: 'needs login repair',
|
|
163
164
|
auth_candidate_health_ready: 'ready',
|
|
164
165
|
auth_candidate_health_low: 'low quota',
|
|
165
166
|
auth_candidate_health_exhausted: 'quota exhausted',
|
|
@@ -172,6 +173,19 @@ const MESSAGES = {
|
|
|
172
173
|
auth_candidate_disabled: 'Disabled auth candidate for auto-rotation: {value}',
|
|
173
174
|
auth_candidate_enabled_short: 'Auth enabled',
|
|
174
175
|
auth_candidate_disabled_short: 'Auth disabled',
|
|
176
|
+
auth_candidate_needs_repair: '{value} needs login repair. Open /auth and use the ? action to repair or delete it.',
|
|
177
|
+
auth_repair_actions_short: 'Repair actions',
|
|
178
|
+
auth_repair_actions_message: '{value} has been verified unusable and could not be recovered from sync. Choose an action.',
|
|
179
|
+
auth_repair_login_preparing: 'Preparing device login repair for {value}...',
|
|
180
|
+
auth_repair_started: 'Device login repair started for {value}.',
|
|
181
|
+
auth_repair_done: 'Auth candidate repaired: {value}',
|
|
182
|
+
auth_repair_failed: 'Auth repair failed for {value}: {error}',
|
|
183
|
+
auth_repair_missing_file: 'Auth repair finished but {value} was not written.',
|
|
184
|
+
auth_repair_identity_mismatch: 'Auth repair finished, but the logged-in account does not match {value}.',
|
|
185
|
+
auth_repair_reverted: 'Restored previous auth. The candidate still needs repair.',
|
|
186
|
+
auth_candidate_deleted: 'Deleted auth candidate: {value}',
|
|
187
|
+
auth_candidate_deleted_short: 'Auth deleted',
|
|
188
|
+
auth_delete_current_restarted: 'The deleted candidate was current, so Codex app-server was restarted without it.',
|
|
175
189
|
auth_no_candidates: 'No auth candidates found. Expected files like auth.json_personal in the auth dir.',
|
|
176
190
|
auth_choice_expired: 'This auth list is no longer active',
|
|
177
191
|
auth_choice_mismatch: 'Auth choice does not match this message',
|
|
@@ -237,6 +251,8 @@ const MESSAGES = {
|
|
|
237
251
|
button_auth_reload: '🔄 Reload auth',
|
|
238
252
|
button_auth_safe_sync: '🧷 Safe sync',
|
|
239
253
|
button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
|
|
254
|
+
button_auth_repair_login: '🔑 Login repair',
|
|
255
|
+
button_auth_delete: '🗑️ Delete',
|
|
240
256
|
button_auth_enable: '✅',
|
|
241
257
|
button_auth_disable: '⏸️',
|
|
242
258
|
button_auth_filter_all: 'All',
|
|
@@ -816,6 +832,7 @@ const MESSAGES = {
|
|
|
816
832
|
auth_candidate_status_enabled: '启用',
|
|
817
833
|
auth_candidate_status_disabled: '禁用',
|
|
818
834
|
auth_candidate_health_disabled: '已禁用',
|
|
835
|
+
auth_candidate_health_needs_repair: '需要登录修复',
|
|
819
836
|
auth_candidate_health_ready: '正常',
|
|
820
837
|
auth_candidate_health_low: '额度偏低',
|
|
821
838
|
auth_candidate_health_exhausted: '额度耗尽',
|
|
@@ -828,6 +845,19 @@ const MESSAGES = {
|
|
|
828
845
|
auth_candidate_disabled: '已禁用 auth 候选自动轮换:{value}',
|
|
829
846
|
auth_candidate_enabled_short: 'auth 已启用',
|
|
830
847
|
auth_candidate_disabled_short: 'auth 已禁用',
|
|
848
|
+
auth_candidate_needs_repair: '{value} 需要登录修复。打开 /auth 后使用 ? 操作修复或删除。',
|
|
849
|
+
auth_repair_actions_short: '修复操作',
|
|
850
|
+
auth_repair_actions_message: '{value} 已确认不可用,并且同步恢复失败。请选择处理方式。',
|
|
851
|
+
auth_repair_login_preparing: '正在准备为 {value} 进行设备登录修复...',
|
|
852
|
+
auth_repair_started: '已开始为 {value} 进行设备登录修复。',
|
|
853
|
+
auth_repair_done: 'auth 候选已修复:{value}',
|
|
854
|
+
auth_repair_failed: '{value} 修复失败:{error}',
|
|
855
|
+
auth_repair_missing_file: '修复流程已结束,但没有写入 {value}。',
|
|
856
|
+
auth_repair_identity_mismatch: '修复流程已结束,但登录账号与 {value} 不匹配。',
|
|
857
|
+
auth_repair_reverted: '已恢复之前的 auth。该候选仍需要修复。',
|
|
858
|
+
auth_candidate_deleted: '已删除 auth 候选:{value}',
|
|
859
|
+
auth_candidate_deleted_short: 'auth 已删除',
|
|
860
|
+
auth_delete_current_restarted: '被删除的候选是当前 auth,已在删除后重启 Codex app-server。',
|
|
831
861
|
auth_no_candidates: '没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。',
|
|
832
862
|
auth_choice_expired: '这个 auth 列表已经不再有效',
|
|
833
863
|
auth_choice_mismatch: '这个 auth 选择按钮不属于当前消息',
|
|
@@ -893,6 +923,8 @@ const MESSAGES = {
|
|
|
893
923
|
button_auth_reload: '🔄 重载 auth',
|
|
894
924
|
button_auth_safe_sync: '🧷 安全同步',
|
|
895
925
|
button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
|
|
926
|
+
button_auth_repair_login: '🔑 登录修复',
|
|
927
|
+
button_auth_delete: '🗑️ 删除',
|
|
896
928
|
button_auth_enable: '✅',
|
|
897
929
|
button_auth_disable: '⏸️',
|
|
898
930
|
button_auth_filter_all: '全部',
|
package/dist/main.js
CHANGED
|
@@ -324,6 +324,7 @@ async function runServeCli() {
|
|
|
324
324
|
canSelfUpdate: () => authSyncLocalIdle()
|
|
325
325
|
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
326
326
|
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
327
|
+
authCandidateDeleted: (_runtimeId, candidateName) => mirror.deleteCandidate(candidateName).then(() => undefined),
|
|
327
328
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
328
329
|
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
329
330
|
if (local)
|
|
@@ -492,6 +493,7 @@ async function runServeCli() {
|
|
|
492
493
|
canSelfUpdate: () => singleAuthSyncLocalIdle()
|
|
493
494
|
&& (singleAuthSync ? singleAuthSync.isIdle() : singleLocalAuthRefreshLease.isIdle()),
|
|
494
495
|
authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
|
|
496
|
+
authCandidateDeleted: (_runtimeId, candidateName) => singleMirror?.deleteCandidate(candidateName).then(() => undefined) ?? Promise.resolve(),
|
|
495
497
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
496
498
|
const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
|
|
497
499
|
if (local)
|
package/dist/store/database.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface ActiveTurnPreviewRecord {
|
|
|
4
4
|
scopeId: string;
|
|
5
5
|
threadId: string;
|
|
6
6
|
messageId: number;
|
|
7
|
+
isObserved: boolean;
|
|
7
8
|
createdAt: number;
|
|
8
9
|
updatedAt: number;
|
|
9
10
|
}
|
|
@@ -37,6 +38,7 @@ export interface CodexAuthQuotaSnapshotRecord {
|
|
|
37
38
|
secondaryRemainingPercent: number | null;
|
|
38
39
|
updatedAt: number;
|
|
39
40
|
}
|
|
41
|
+
export type CodexAuthCandidateState = 'active' | 'needs_repair';
|
|
40
42
|
export declare class BridgeStore {
|
|
41
43
|
private db;
|
|
42
44
|
constructor(dbPath: string);
|
|
@@ -73,7 +75,9 @@ export declare class BridgeStore {
|
|
|
73
75
|
getPendingApprovalByServerRequestId(serverRequestId: string): PendingApprovalRecord | null;
|
|
74
76
|
markApprovalResolved(localId: string): void;
|
|
75
77
|
countPendingApprovals(): number;
|
|
76
|
-
saveActiveTurnPreview(record: Pick<ActiveTurnPreviewRecord, 'turnId' | 'scopeId' | 'threadId' | 'messageId'>
|
|
78
|
+
saveActiveTurnPreview(record: Pick<ActiveTurnPreviewRecord, 'turnId' | 'scopeId' | 'threadId' | 'messageId'> & {
|
|
79
|
+
isObserved?: boolean;
|
|
80
|
+
}): void;
|
|
77
81
|
listActiveTurnPreviews(): ActiveTurnPreviewRecord[];
|
|
78
82
|
removeActiveTurnPreview(turnId: string): void;
|
|
79
83
|
removeActiveTurnPreviewByMessage(scopeId: string, messageId: number): void;
|
|
@@ -117,7 +121,10 @@ export declare class BridgeStore {
|
|
|
117
121
|
getWeixinContextToken(scopeId: string): string | null;
|
|
118
122
|
setWeixinContextToken(scopeId: string, contextToken: string): void;
|
|
119
123
|
listDisabledCodexAuthCandidateNames(runtimeId?: string): Set<string>;
|
|
124
|
+
listCodexAuthCandidateStates(runtimeId?: string): Map<string, CodexAuthCandidateState>;
|
|
120
125
|
setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
|
|
126
|
+
setCodexAuthCandidateState(name: string, state: CodexAuthCandidateState, runtimeId?: string): void;
|
|
127
|
+
deleteCodexAuthCandidate(name: string): void;
|
|
121
128
|
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
|
|
122
129
|
listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
|
|
123
130
|
private ensureColumn;
|
package/dist/store/database.js
CHANGED
|
@@ -70,6 +70,7 @@ export class BridgeStore {
|
|
|
70
70
|
scope_id TEXT NOT NULL,
|
|
71
71
|
thread_id TEXT NOT NULL,
|
|
72
72
|
message_id INTEGER NOT NULL,
|
|
73
|
+
is_observed INTEGER NOT NULL DEFAULT 0,
|
|
73
74
|
created_at INTEGER NOT NULL,
|
|
74
75
|
updated_at INTEGER NOT NULL
|
|
75
76
|
);
|
|
@@ -169,12 +170,14 @@ export class BridgeStore {
|
|
|
169
170
|
CREATE TABLE IF NOT EXISTS codex_auth_candidates (
|
|
170
171
|
name TEXT PRIMARY KEY,
|
|
171
172
|
disabled INTEGER NOT NULL DEFAULT 0,
|
|
173
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
172
174
|
updated_at INTEGER NOT NULL
|
|
173
175
|
);
|
|
174
176
|
CREATE TABLE IF NOT EXISTS codex_auth_candidate_runtime (
|
|
175
177
|
runtime_id TEXT NOT NULL,
|
|
176
178
|
name TEXT NOT NULL,
|
|
177
179
|
disabled INTEGER NOT NULL DEFAULT 0,
|
|
180
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
178
181
|
updated_at INTEGER NOT NULL,
|
|
179
182
|
PRIMARY KEY (runtime_id, name)
|
|
180
183
|
);
|
|
@@ -207,6 +210,9 @@ export class BridgeStore {
|
|
|
207
210
|
this.ensureColumn('pending_approvals', 'payload_json', 'TEXT');
|
|
208
211
|
this.ensureColumn('pending_user_inputs', 'status', "TEXT NOT NULL DEFAULT 'pending'");
|
|
209
212
|
this.ensureColumn('pending_user_inputs', 'submitted_at', 'INTEGER');
|
|
213
|
+
this.ensureColumn('active_turn_previews', 'is_observed', 'INTEGER NOT NULL DEFAULT 0');
|
|
214
|
+
this.ensureColumn('codex_auth_candidates', 'state', "TEXT NOT NULL DEFAULT 'active'");
|
|
215
|
+
this.ensureColumn('codex_auth_candidate_runtime', 'state', "TEXT NOT NULL DEFAULT 'active'");
|
|
210
216
|
this.ensureColumn('codex_auth_quota_snapshots', 'plan_type', 'TEXT');
|
|
211
217
|
this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
|
|
212
218
|
this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
|
|
@@ -410,22 +416,23 @@ export class BridgeStore {
|
|
|
410
416
|
saveActiveTurnPreview(record) {
|
|
411
417
|
const now = Date.now();
|
|
412
418
|
this.db.prepare('DELETE FROM active_turn_previews WHERE turn_id = ? OR scope_id = ?').run(record.turnId, record.scopeId);
|
|
413
|
-
this.db.prepare(`
|
|
414
|
-
INSERT INTO active_turn_previews (turn_id, scope_id, thread_id, message_id, created_at, updated_at)
|
|
415
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
416
|
-
`).run(record.turnId, record.scopeId, record.threadId, record.messageId, now, now);
|
|
419
|
+
this.db.prepare(`
|
|
420
|
+
INSERT INTO active_turn_previews (turn_id, scope_id, thread_id, message_id, is_observed, created_at, updated_at)
|
|
421
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
422
|
+
`).run(record.turnId, record.scopeId, record.threadId, record.messageId, record.isObserved ? 1 : 0, now, now);
|
|
417
423
|
}
|
|
418
424
|
listActiveTurnPreviews() {
|
|
419
|
-
const rows = this.db.prepare(`
|
|
420
|
-
SELECT turn_id, scope_id, thread_id, message_id, created_at, updated_at
|
|
421
|
-
FROM active_turn_previews
|
|
422
|
-
ORDER BY created_at ASC
|
|
425
|
+
const rows = this.db.prepare(`
|
|
426
|
+
SELECT turn_id, scope_id, thread_id, message_id, is_observed, created_at, updated_at
|
|
427
|
+
FROM active_turn_previews
|
|
428
|
+
ORDER BY created_at ASC
|
|
423
429
|
`).all();
|
|
424
430
|
return rows.map((row) => ({
|
|
425
431
|
turnId: String(row.turn_id),
|
|
426
432
|
scopeId: String(row.scope_id),
|
|
427
433
|
threadId: String(row.thread_id),
|
|
428
434
|
messageId: Number(row.message_id),
|
|
435
|
+
isObserved: Boolean(row.is_observed),
|
|
429
436
|
createdAt: Number(row.created_at),
|
|
430
437
|
updatedAt: Number(row.updated_at),
|
|
431
438
|
}));
|
|
@@ -835,21 +842,60 @@ export class BridgeStore {
|
|
|
835
842
|
const rows = this.db.prepare('SELECT name FROM codex_auth_candidates WHERE disabled = 1').all();
|
|
836
843
|
return new Set(rows.map(row => String(row.name)));
|
|
837
844
|
}
|
|
845
|
+
listCodexAuthCandidateStates(runtimeId = 'default') {
|
|
846
|
+
const states = new Map();
|
|
847
|
+
const globalRows = this.db.prepare('SELECT name, state FROM codex_auth_candidates').all();
|
|
848
|
+
for (const row of globalRows) {
|
|
849
|
+
states.set(String(row.name), normalizeCodexAuthCandidateState(row.state));
|
|
850
|
+
}
|
|
851
|
+
if (runtimeId !== 'default') {
|
|
852
|
+
const runtimeRows = this.db.prepare('SELECT name, state FROM codex_auth_candidate_runtime WHERE runtime_id = ?').all(runtimeId);
|
|
853
|
+
for (const row of runtimeRows) {
|
|
854
|
+
states.set(String(row.name), normalizeCodexAuthCandidateState(row.state));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return states;
|
|
858
|
+
}
|
|
838
859
|
setCodexAuthCandidateDisabled(name, disabled, runtimeId = 'default') {
|
|
839
860
|
if (runtimeId !== 'default') {
|
|
840
861
|
this.db.prepare(`
|
|
841
|
-
INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, updated_at)
|
|
842
|
-
VALUES (?, ?, ?, ?)
|
|
862
|
+
INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, state, updated_at)
|
|
863
|
+
VALUES (?, ?, ?, 'active', ?)
|
|
843
864
|
ON CONFLICT(runtime_id, name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
844
865
|
`).run(runtimeId, name, disabled ? 1 : 0, Date.now());
|
|
845
866
|
return;
|
|
846
867
|
}
|
|
847
868
|
this.db.prepare(`
|
|
848
|
-
INSERT INTO codex_auth_candidates (name, disabled, updated_at)
|
|
849
|
-
VALUES (?, ?, ?)
|
|
869
|
+
INSERT INTO codex_auth_candidates (name, disabled, state, updated_at)
|
|
870
|
+
VALUES (?, ?, 'active', ?)
|
|
850
871
|
ON CONFLICT(name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
851
872
|
`).run(name, disabled ? 1 : 0, Date.now());
|
|
852
873
|
}
|
|
874
|
+
setCodexAuthCandidateState(name, state, runtimeId = 'default') {
|
|
875
|
+
if (runtimeId !== 'default') {
|
|
876
|
+
this.db.prepare(`
|
|
877
|
+
INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, state, updated_at)
|
|
878
|
+
VALUES (?, ?, 0, ?, ?)
|
|
879
|
+
ON CONFLICT(runtime_id, name) DO UPDATE SET state = excluded.state, updated_at = excluded.updated_at
|
|
880
|
+
`).run(runtimeId, name, state, Date.now());
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.db.prepare(`
|
|
884
|
+
INSERT INTO codex_auth_candidates (name, disabled, state, updated_at)
|
|
885
|
+
VALUES (?, 0, ?, ?)
|
|
886
|
+
ON CONFLICT(name) DO UPDATE SET state = excluded.state, updated_at = excluded.updated_at
|
|
887
|
+
`).run(name, state, Date.now());
|
|
888
|
+
this.db.prepare(`
|
|
889
|
+
UPDATE codex_auth_candidate_runtime
|
|
890
|
+
SET state = ?, updated_at = ?
|
|
891
|
+
WHERE name = ?
|
|
892
|
+
`).run(state, Date.now(), name);
|
|
893
|
+
}
|
|
894
|
+
deleteCodexAuthCandidate(name) {
|
|
895
|
+
this.db.prepare('DELETE FROM codex_auth_candidates WHERE name = ?').run(name);
|
|
896
|
+
this.db.prepare('DELETE FROM codex_auth_candidate_runtime WHERE name = ?').run(name);
|
|
897
|
+
this.db.prepare('DELETE FROM codex_auth_quota_snapshots WHERE candidate_name = ?').run(name);
|
|
898
|
+
}
|
|
853
899
|
setCodexAuthQuotaSnapshot(runtimeId, candidateName, accountId, quotaIdentityId, snapshot) {
|
|
854
900
|
this.db.prepare(`
|
|
855
901
|
INSERT INTO codex_auth_quota_snapshots (
|
|
@@ -932,6 +978,9 @@ function nullableNumber(value) {
|
|
|
932
978
|
function nullableString(value) {
|
|
933
979
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
934
980
|
}
|
|
981
|
+
function normalizeCodexAuthCandidateState(value) {
|
|
982
|
+
return value === 'needs_repair' ? 'needs_repair' : 'active';
|
|
983
|
+
}
|
|
935
984
|
function normalizeCollaborationMode(value) {
|
|
936
985
|
return value === 'default' || value === 'plan' ? value : null;
|
|
937
986
|
}
|
package/docs/user-manual.md
CHANGED
|
@@ -415,7 +415,9 @@ Quota remaining: window:percent|auth
|
|
|
415
415
|
[🔄 Reload auth]
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
-
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,
|
|
418
|
+
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, invalid auth file, and needs login repair states.
|
|
419
|
+
|
|
420
|
+
When an auth candidate has already failed while in use and FoxClaw cannot recover a newer same-account credential from local mirror or cross-node sync, it is marked as `needs login repair`. These candidates are skipped by auto-rotation and proactive refresh, and are hidden from the `Enabled` filter. Their row shows a `?` action. Tapping it opens two choices: Login repair starts device-code login with that candidate selected; Delete removes the candidate from canonical storage and all local bot runtimes, and clears cached quota for it.
|
|
419
421
|
|
|
420
422
|
`/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.
|
|
421
423
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -415,7 +415,9 @@ Candidates: 2
|
|
|
415
415
|
[🔄 Reload auth]
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
-
右侧 `✅` / `⏸️` 表示当前是否参与自动轮转。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--` 表示该候选还没有额度历史快照。健康摘要会区分正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key
|
|
418
|
+
右侧 `✅` / `⏸️` 表示当前是否参与自动轮转。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--` 表示该候选还没有额度历史快照。健康摘要会区分正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key、无效 auth 文件和需要登录修复。
|
|
419
|
+
|
|
420
|
+
当某个候选在实际使用中已经失败,并且 FoxClaw 无法从本机 mirror 或跨节点同步恢复同账号较新凭据时,会标为“需要登录修复”。这类候选不会参与自动轮转和后台主动刷新,也不会出现在“已启用”筛选里。它的按钮会显示 `?`。点击后有两个选择:`登录修复` 会在选中该候选的状态下启动设备码登录;`删除` 会从 canonical 和所有本机 bot runtime 中删除这个候选,并清理它的额度缓存。
|
|
419
421
|
|
|
420
422
|
`/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 并显示摘要。
|
|
421
423
|
|