@foxden-app/foxclaw 0.5.16 → 0.5.18
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 +20 -0
- package/dist/controller/controller.d.ts +2 -0
- package/dist/controller/controller.js +107 -6
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +4 -0
- package/dist/main.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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.18 - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- `/auth` 面板按钮和 `/auth use <n>` 手动切换后会立即通过 Codex app-server 验证新 auth:ChatGPT 候选必须能返回 account 和 rate-limit usage,成功后马上写入该候选的额度快照,所以列表不再继续显示旧额度。
|
|
9
|
+
- 如果切换后的 auth 无法读取、身份不匹配,或 Codex 没有返回有效 account/usage,FoxClaw 会把该候选标记为“需要登录修复”(`?`),恢复到切换前的 auth,并再次重启 Codex app-server,让当前 runtime 不停留在坏 auth 上。
|
|
10
|
+
- 自动 auth 轮换也复用同一套切换后验证;验证失败的候选不会继续重试请求或参与后续轮询。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- Manual auth switches from the `/auth` panel buttons and `/auth use <n>` now validate the newly selected auth through Codex app-server immediately. ChatGPT candidates must return account and rate-limit usage, and successful switches record a fresh quota snapshot so the list no longer keeps showing stale usage.
|
|
14
|
+
- If the selected auth cannot be parsed, has an identity mismatch, or Codex does not return valid account/usage data, FoxClaw marks that candidate as “needs login repair” (`?`), restores the previous auth, and restarts Codex app-server again so the runtime does not remain on a bad auth.
|
|
15
|
+
- Automatic auth rotation now uses the same post-switch validation; failed candidates are not used for retrying the request or for later polling.
|
|
16
|
+
|
|
17
|
+
## 0.5.17 - 2026-06-08
|
|
18
|
+
|
|
19
|
+
### 中文
|
|
20
|
+
- `foxclaw <subcommand> --help` / `-h` 现在只打印帮助,不再继续执行 `install-systemd`、`start`、`restart` 等带副作用的子命令,避免一次查帮助意外触发服务重启。
|
|
21
|
+
|
|
22
|
+
### English
|
|
23
|
+
- `foxclaw <subcommand> --help` / `-h` now prints usage and stops before running side-effecting subcommands such as `install-systemd`, `start`, or `restart`, preventing an accidental service restart while checking help.
|
|
24
|
+
|
|
5
25
|
## 0.5.16 - 2026-06-08
|
|
6
26
|
|
|
7
27
|
### 中文
|
|
@@ -346,6 +346,8 @@ export declare class BridgeSessionCore {
|
|
|
346
346
|
private readCodexAuthSwitchLabels;
|
|
347
347
|
private codexAuthSwitchParams;
|
|
348
348
|
private switchCodexAuthAndRestart;
|
|
349
|
+
private validateCurrentCodexAuthCandidate;
|
|
350
|
+
private formatAuthSwitchValidationLines;
|
|
349
351
|
private recoverCodexAuthCandidate;
|
|
350
352
|
private markCodexAuthCandidateNeedsRepair;
|
|
351
353
|
private markCodexAuthCandidateActive;
|
|
@@ -5362,14 +5362,16 @@ export class BridgeSessionCore {
|
|
|
5362
5362
|
if (record.messageId !== null) {
|
|
5363
5363
|
await this.editMessage(event.scopeId, record.messageId, switchingMessage, []);
|
|
5364
5364
|
}
|
|
5365
|
-
await this.switchCodexAuthAndRestart(event.scopeId, locale, candidate, false, false);
|
|
5365
|
+
const outcome = await this.switchCodexAuthAndRestart(event.scopeId, locale, candidate, false, false);
|
|
5366
5366
|
const state = await this.listCodexAuthState();
|
|
5367
|
-
await this.refreshCurrentCodexAuthQuota(state);
|
|
5368
5367
|
record.candidates = state.candidates;
|
|
5369
5368
|
record.createdAt = Date.now();
|
|
5370
5369
|
clampCodexAuthListOffset(record);
|
|
5371
5370
|
if (record.messageId !== null) {
|
|
5372
|
-
await this.editMessage(event.scopeId, record.messageId,
|
|
5371
|
+
await this.editMessage(event.scopeId, record.messageId, [
|
|
5372
|
+
...this.formatAuthSwitchValidationLines(locale, outcome),
|
|
5373
|
+
renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record),
|
|
5374
|
+
].filter(Boolean).join('\n\n'), authChoiceKeyboard(locale, record));
|
|
5373
5375
|
}
|
|
5374
5376
|
}
|
|
5375
5377
|
async maybeRunPendingAuthRotation() {
|
|
@@ -5417,7 +5419,10 @@ export class BridgeSessionCore {
|
|
|
5417
5419
|
...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
|
|
5418
5420
|
error: formatShortStatusError(rotation.reason),
|
|
5419
5421
|
}));
|
|
5420
|
-
await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true);
|
|
5422
|
+
const outcome = await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true);
|
|
5423
|
+
if (!outcome.ok) {
|
|
5424
|
+
return false;
|
|
5425
|
+
}
|
|
5421
5426
|
if (rotation.retry) {
|
|
5422
5427
|
await this.retryTurnAfterAuthRotation(rotation.scopeId, locale, rotation.retry);
|
|
5423
5428
|
return true;
|
|
@@ -5533,15 +5538,61 @@ export class BridgeSessionCore {
|
|
|
5533
5538
|
};
|
|
5534
5539
|
}
|
|
5535
5540
|
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic, sendResult = true) {
|
|
5541
|
+
const authDir = this.resolveAuthDir();
|
|
5542
|
+
const initialState = await listCodexAuthState(new Set(), new Map(), authDir);
|
|
5543
|
+
const authStat = await fs.lstat(initialState.authPath).catch(() => null);
|
|
5544
|
+
const originalRegularAuth = authStat?.isFile()
|
|
5545
|
+
? await fs.readFile(initialState.authPath, 'utf8').catch(() => null)
|
|
5546
|
+
: null;
|
|
5536
5547
|
const recovered = await this.recoverCodexAuthCandidate(candidate.name, { crossNode: automatic });
|
|
5537
|
-
const result = await switchCodexAuth(candidate.path,
|
|
5548
|
+
const result = await switchCodexAuth(candidate.path, authDir);
|
|
5538
5549
|
this.authRotationFailedTargets.delete(candidate.path);
|
|
5539
5550
|
this.pendingTurnErrors.clear();
|
|
5540
5551
|
this.attachedThreads.clear();
|
|
5541
5552
|
await this.app.restart();
|
|
5553
|
+
const validation = await this.validateCurrentCodexAuthCandidate(candidate);
|
|
5554
|
+
if (!validation.ok) {
|
|
5555
|
+
this.markCodexAuthCandidateNeedsRepair(candidate.name);
|
|
5556
|
+
let restoredPrevious = false;
|
|
5557
|
+
try {
|
|
5558
|
+
await restoreCodexAuthTarget(initialState.authDir, initialState.authPath, initialState.currentTargetPath, originalRegularAuth);
|
|
5559
|
+
this.pendingTurnErrors.clear();
|
|
5560
|
+
this.attachedThreads.clear();
|
|
5561
|
+
await this.app.restart();
|
|
5562
|
+
restoredPrevious = true;
|
|
5563
|
+
}
|
|
5564
|
+
catch (error) {
|
|
5565
|
+
this.logger.warn('codex.auth_switch_restore_failed', {
|
|
5566
|
+
candidate: candidate.name,
|
|
5567
|
+
error: toErrorMeta(error),
|
|
5568
|
+
});
|
|
5569
|
+
}
|
|
5570
|
+
const outcome = {
|
|
5571
|
+
...result,
|
|
5572
|
+
ok: false,
|
|
5573
|
+
candidateName: candidate.name,
|
|
5574
|
+
recovered,
|
|
5575
|
+
error: validation.error,
|
|
5576
|
+
restoredPrevious,
|
|
5577
|
+
};
|
|
5578
|
+
if (sendResult) {
|
|
5579
|
+
const lines = this.formatAuthSwitchValidationLines(locale, outcome);
|
|
5580
|
+
await this.sendMessage(scopeId, lines.join('\n'));
|
|
5581
|
+
}
|
|
5582
|
+
return outcome;
|
|
5583
|
+
}
|
|
5584
|
+
this.markCodexAuthCandidateActive(candidate.name);
|
|
5542
5585
|
await this.syncCodexAuthCandidate(candidate.name);
|
|
5586
|
+
const outcome = {
|
|
5587
|
+
...result,
|
|
5588
|
+
ok: true,
|
|
5589
|
+
candidateName: candidate.name,
|
|
5590
|
+
recovered,
|
|
5591
|
+
error: null,
|
|
5592
|
+
restoredPrevious: false,
|
|
5593
|
+
};
|
|
5543
5594
|
if (!sendResult) {
|
|
5544
|
-
return;
|
|
5595
|
+
return outcome;
|
|
5545
5596
|
}
|
|
5546
5597
|
const lines = [t(locale, automatic ? 'auth_auto_done' : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
|
|
5547
5598
|
if (recovered) {
|
|
@@ -5549,6 +5600,56 @@ export class BridgeSessionCore {
|
|
|
5549
5600
|
}
|
|
5550
5601
|
lines.push(...await this.buildCodexUsageStatusLines(locale));
|
|
5551
5602
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
5603
|
+
return outcome;
|
|
5604
|
+
}
|
|
5605
|
+
async validateCurrentCodexAuthCandidate(candidate) {
|
|
5606
|
+
try {
|
|
5607
|
+
if (await isCodexApiKeyAuthCandidate(candidate.path)) {
|
|
5608
|
+
const account = await this.app.readAccount(false);
|
|
5609
|
+
if (!account) {
|
|
5610
|
+
return { ok: false, error: 'Codex did not return account info after switch' };
|
|
5611
|
+
}
|
|
5612
|
+
return { ok: true };
|
|
5613
|
+
}
|
|
5614
|
+
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
5615
|
+
if (!metadata) {
|
|
5616
|
+
return { ok: false, error: 'candidate is not a readable ChatGPT auth file' };
|
|
5617
|
+
}
|
|
5618
|
+
if (!chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)) {
|
|
5619
|
+
return { ok: false, error: 'candidate auth identity does not match candidate name' };
|
|
5620
|
+
}
|
|
5621
|
+
const [account, rateLimits] = await Promise.all([
|
|
5622
|
+
this.app.readAccount(false),
|
|
5623
|
+
this.app.readAccountRateLimits(),
|
|
5624
|
+
]);
|
|
5625
|
+
if (!account) {
|
|
5626
|
+
return { ok: false, error: 'Codex did not return account info after switch' };
|
|
5627
|
+
}
|
|
5628
|
+
if (account.type !== 'chatgpt') {
|
|
5629
|
+
return { ok: false, error: `Codex account type is ${account.type || 'unknown'}, expected ChatGPT` };
|
|
5630
|
+
}
|
|
5631
|
+
const snapshot = selectCodexRateLimitSnapshot(rateLimits);
|
|
5632
|
+
if (!snapshot) {
|
|
5633
|
+
return { ok: false, error: 'Codex did not return ChatGPT rate limits after switch' };
|
|
5634
|
+
}
|
|
5635
|
+
await this.recordCodexAuthQuotaSnapshot(candidate.name, metadata, snapshot);
|
|
5636
|
+
return { ok: true };
|
|
5637
|
+
}
|
|
5638
|
+
catch (error) {
|
|
5639
|
+
return { ok: false, error: formatUserError(error) };
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
formatAuthSwitchValidationLines(locale, outcome) {
|
|
5643
|
+
if (outcome.ok) {
|
|
5644
|
+
return [];
|
|
5645
|
+
}
|
|
5646
|
+
return [
|
|
5647
|
+
t(locale, 'auth_switch_validation_failed', {
|
|
5648
|
+
value: outcome.candidateName,
|
|
5649
|
+
error: outcome.error ?? t(locale, 'unknown'),
|
|
5650
|
+
}),
|
|
5651
|
+
outcome.restoredPrevious ? t(locale, 'auth_switch_validation_reverted') : '',
|
|
5652
|
+
].filter(Boolean);
|
|
5552
5653
|
}
|
|
5553
5654
|
async recoverCodexAuthCandidate(candidateName, options = { crossNode: true }) {
|
|
5554
5655
|
if (!this.coordinator?.recoverAuthCandidate) {
|
package/dist/i18n.d.ts
CHANGED
|
@@ -194,6 +194,8 @@ declare const MESSAGES: {
|
|
|
194
194
|
readonly auth_choice_recorded: "Auth selected";
|
|
195
195
|
readonly auth_switching: "Switching Codex auth: {from} -> {to}...";
|
|
196
196
|
readonly auth_switch_done: "Codex auth switched: {from} -> {to}.";
|
|
197
|
+
readonly auth_switch_validation_failed: "Selected auth failed validation: {error}. Marked {value} for login repair.";
|
|
198
|
+
readonly auth_switch_validation_reverted: "Restored the previous auth after the failed switch.";
|
|
197
199
|
readonly auth_recovered_newer_candidate: "Recovered a newer same-account credential for {value} from another Codex home before reload.";
|
|
198
200
|
readonly auth_refresh_all_confirm_short: "Review refresh all risk first.";
|
|
199
201
|
readonly auth_refresh_all_confirm_message: "Refresh all will rotate ChatGPT refresh tokens for every candidate. If OpenAI/Codex consumes a 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.\n\nOnly continue when every bot is idle and you accept this risk.";
|
|
@@ -866,6 +868,8 @@ declare const MESSAGES: {
|
|
|
866
868
|
readonly auth_choice_recorded: "已选择 auth";
|
|
867
869
|
readonly auth_switching: "正在切换 Codex auth:{from} -> {to}...";
|
|
868
870
|
readonly auth_switch_done: "Codex auth 已切换:{from} -> {to}。";
|
|
871
|
+
readonly auth_switch_validation_failed: "选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。";
|
|
872
|
+
readonly auth_switch_validation_reverted: "已在切换失败后恢复到之前的 auth。";
|
|
869
873
|
readonly auth_recovered_newer_candidate: "重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。";
|
|
870
874
|
readonly auth_refresh_all_confirm_short: "请先确认刷新全部风险。";
|
|
871
875
|
readonly auth_refresh_all_confirm_message: "刷新全部会轮换每个 ChatGPT 候选的 refresh token。如果 OpenAI/Codex 已经消费旧 refresh token,但因为网络、进程或磁盘故障导致新 token 没能成功保存,该候选可能需要重新设备登录,甚至重新手机号验证。\n\n只有在所有 bot 都空闲,并且你能接受这个风险时,才继续。";
|
package/dist/i18n.js
CHANGED
|
@@ -192,6 +192,8 @@ const MESSAGES = {
|
|
|
192
192
|
auth_choice_recorded: 'Auth selected',
|
|
193
193
|
auth_switching: 'Switching Codex auth: {from} -> {to}...',
|
|
194
194
|
auth_switch_done: 'Codex auth switched: {from} -> {to}.',
|
|
195
|
+
auth_switch_validation_failed: 'Selected auth failed validation: {error}. Marked {value} for login repair.',
|
|
196
|
+
auth_switch_validation_reverted: 'Restored the previous auth after the failed switch.',
|
|
195
197
|
auth_recovered_newer_candidate: 'Recovered a newer same-account credential for {value} from another Codex home before reload.',
|
|
196
198
|
auth_refresh_all_confirm_short: 'Review refresh all risk first.',
|
|
197
199
|
auth_refresh_all_confirm_message: 'Refresh all will rotate ChatGPT refresh tokens for every candidate. If OpenAI/Codex consumes a 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.\n\nOnly continue when every bot is idle and you accept this risk.',
|
|
@@ -864,6 +866,8 @@ const MESSAGES = {
|
|
|
864
866
|
auth_choice_recorded: '已选择 auth',
|
|
865
867
|
auth_switching: '正在切换 Codex auth:{from} -> {to}...',
|
|
866
868
|
auth_switch_done: 'Codex auth 已切换:{from} -> {to}。',
|
|
869
|
+
auth_switch_validation_failed: '选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。',
|
|
870
|
+
auth_switch_validation_reverted: '已在切换失败后恢复到之前的 auth。',
|
|
867
871
|
auth_recovered_newer_candidate: '重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。',
|
|
868
872
|
auth_refresh_all_confirm_short: '请先确认刷新全部风险。',
|
|
869
873
|
auth_refresh_all_confirm_message: '刷新全部会轮换每个 ChatGPT 候选的 refresh token。如果 OpenAI/Codex 已经消费旧 refresh token,但因为网络、进程或磁盘故障导致新 token 没能成功保存,该候选可能需要重新设备登录,甚至重新手机号验证。\n\n只有在所有 bot 都空闲,并且你能接受这个风险时,才继续。',
|
package/dist/main.js
CHANGED
|
@@ -66,7 +66,7 @@ async function main() {
|
|
|
66
66
|
console.log(readPackageVersion());
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
if (isHelpCommand(command)) {
|
|
69
|
+
if (isHelpCommand(command) || hasHelpFlag()) {
|
|
70
70
|
printUsage();
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
@@ -149,6 +149,9 @@ function isVersionCommand(value) {
|
|
|
149
149
|
function isHelpCommand(value) {
|
|
150
150
|
return value === 'help' || value === '--help' || value === '-h';
|
|
151
151
|
}
|
|
152
|
+
function hasHelpFlag() {
|
|
153
|
+
return process.argv.slice(3).some(isHelpCommand);
|
|
154
|
+
}
|
|
152
155
|
function readPackageVersion() {
|
|
153
156
|
try {
|
|
154
157
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|