@foxden-app/foxclaw 0.5.76 → 0.5.78
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/cross_node_sync.d.ts +7 -0
- package/dist/auth/cross_node_sync.js +191 -1
- package/dist/codex_app/client.js +1 -1
- package/dist/controller/controller.js +30 -23
- package/dist/controller/presentation.js +3 -1
- package/dist/i18n.d.ts +8 -6
- package/dist/i18n.js +8 -6
- package/dist/types.d.ts +1 -1
- package/docs/cross-node-auth-sync.md +2 -2
- package/docs/user-manual.md +5 -5
- package/docs/zh/cross-node-auth-sync.md +2 -2
- package/docs/zh/user-manual.md +5 -5
- 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.78 - 2026-07-12
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- `/auth` 面板收敛为“设备登录”和“安全同步”两个维护入口,移除权限、重载 auth 和单独的全节点审计按钮。“安全同步”现在直接执行完整的全节点验证、最新有效副本协商、8 天临期刷新和最终分发;`/auth sync audit` 继续作为兼容别名。
|
|
9
|
+
- 全节点审计现在也覆盖只存在于单个节点的旧候选:通过现有 AES-256-GCM 通道发送给其他节点做只验证、不导入的独立复核。任一节点验证有效就恢复并分发;没有有效结果且至少两个节点确认无效才标记 `?`。
|
|
10
|
+
- Telegram `/status` 新增当前 bot 实际使用的 Codex Home,并与 `/auth`、`/threads` 等时效面板一样按 `TELEGRAM_PANEL_TTL_MS` 自动撤回,默认 5 分钟。
|
|
11
|
+
- 完整支持 Codex 新增的 `max`、`ultra` 推理强度,包括模型能力读取、命令解析、设置持久化和下一轮请求透传,不再提示“未知推理强度”。
|
|
12
|
+
|
|
13
|
+
### English
|
|
14
|
+
- Simplified the `/auth` panel to Device login and Safe sync, removing the Permissions, Reload auth, and separate cluster-audit buttons. Safe sync now runs the complete all-node validation, newest-valid-copy reconciliation, 8-day stale refresh, and final distribution flow; `/auth sync audit` remains a compatibility alias.
|
|
15
|
+
- All-node audits now cover legacy candidates found on only one node. The candidate is sent through the existing AES-256-GCM channel for independent validation without import. Any valid result restores and distributes it; it is marked `?` only when no valid result exists and at least two nodes reject it.
|
|
16
|
+
- Telegram `/status` now shows the effective Codex Home for the current bot and expires through `TELEGRAM_PANEL_TTL_MS`, like `/auth` and `/threads`, with a five-minute default.
|
|
17
|
+
- Added end-to-end support for the new Codex `max` and `ultra` reasoning efforts across model capability parsing, commands, persisted settings, and turn requests.
|
|
18
|
+
|
|
19
|
+
## 0.5.77 - 2026-07-11
|
|
20
|
+
|
|
21
|
+
### 中文
|
|
22
|
+
- 修复跨节点升级广播触发的后台升级完成后,旧服务把内部 `cluster:*` scope 当成 Telegram scope 解析并反复记录 `Expected telegram: scope` 的问题。集群升级现在只记录完成状态并清理轮询文件,不再尝试向不存在的聊天发送回报。
|
|
23
|
+
|
|
24
|
+
### English
|
|
25
|
+
- Fixed cluster-broadcast updates repeatedly logging `Expected telegram: scope` after completion because the old service tried to parse the internal `cluster:*` scope as a Telegram destination. Cluster updates now record completion and clear the poll state without trying to send a chat reply to a non-chat scope.
|
|
26
|
+
|
|
5
27
|
## 0.5.76 - 2026-07-11
|
|
6
28
|
|
|
7
29
|
### 中文
|
|
@@ -299,6 +299,7 @@ export declare class CrossNodeAuthSync {
|
|
|
299
299
|
private readonly pendingLeases;
|
|
300
300
|
private readonly pendingTests;
|
|
301
301
|
private readonly pendingAudits;
|
|
302
|
+
private readonly pendingAuditVerifications;
|
|
302
303
|
private seenNonces;
|
|
303
304
|
private lastPeerActivityAt;
|
|
304
305
|
private timer;
|
|
@@ -338,11 +339,17 @@ export declare class CrossNodeAuthSync {
|
|
|
338
339
|
private handleMessage;
|
|
339
340
|
private handleAuditRequest;
|
|
340
341
|
private handleAuditResponse;
|
|
342
|
+
private handleAuditVerificationRequest;
|
|
343
|
+
private handleAuditVerificationResponse;
|
|
341
344
|
private handleAuditApply;
|
|
342
345
|
private applyAuditState;
|
|
343
346
|
private buildAuditReport;
|
|
347
|
+
private verifySingleNodeInvalidCandidates;
|
|
348
|
+
private verifyBundlesIntoAuditReport;
|
|
349
|
+
private validateAuditBundle;
|
|
344
350
|
private reconcileAuditReports;
|
|
345
351
|
private finishPendingAudit;
|
|
352
|
+
private finishPendingAuditVerification;
|
|
346
353
|
private handleServiceUpdateRequest;
|
|
347
354
|
private handlePullRequest;
|
|
348
355
|
private handlePullResponse;
|
|
@@ -27,6 +27,7 @@ export class CrossNodeAuthSync {
|
|
|
27
27
|
pendingLeases = new Map();
|
|
28
28
|
pendingTests = new Map();
|
|
29
29
|
pendingAudits = new Map();
|
|
30
|
+
pendingAuditVerifications = new Map();
|
|
30
31
|
seenNonces = new Map();
|
|
31
32
|
lastPeerActivityAt = new Map();
|
|
32
33
|
timer = null;
|
|
@@ -523,6 +524,7 @@ export class CrossNodeAuthSync {
|
|
|
523
524
|
}
|
|
524
525
|
const [localReport, remote] = await Promise.all([localReportPromise, remoteReportsPromise]);
|
|
525
526
|
const reports = [localReport, ...remote.reports.values()];
|
|
527
|
+
await this.verifySingleNodeInvalidCandidates(requestId, reports);
|
|
526
528
|
const result = await this.reconcileAuditReports(requestId, reports, remote.missing);
|
|
527
529
|
this.recordEvent({
|
|
528
530
|
direction: 'local',
|
|
@@ -641,6 +643,12 @@ export class CrossNodeAuthSync {
|
|
|
641
643
|
case 'audit.response':
|
|
642
644
|
this.handleAuditResponse(message, normalizePeerIdentity(peer));
|
|
643
645
|
return;
|
|
646
|
+
case 'audit.verify.request':
|
|
647
|
+
await this.handleAuditVerificationRequest(message, normalizePeerIdentity(peer));
|
|
648
|
+
return;
|
|
649
|
+
case 'audit.verify.response':
|
|
650
|
+
this.handleAuditVerificationResponse(message, normalizePeerIdentity(peer));
|
|
651
|
+
return;
|
|
644
652
|
case 'audit.apply':
|
|
645
653
|
await this.handleAuditApply(message, senderNodeId, sourceLabel, normalizePeerIdentity(peer));
|
|
646
654
|
return;
|
|
@@ -686,6 +694,62 @@ export class CrossNodeAuthSync {
|
|
|
686
694
|
this.finishPendingAudit(message.requestId);
|
|
687
695
|
}
|
|
688
696
|
}
|
|
697
|
+
async handleAuditVerificationRequest(message, peer) {
|
|
698
|
+
let report;
|
|
699
|
+
if (!this.callbacks.isIdle()) {
|
|
700
|
+
report = { nodeId: this.nodeId ?? 'local', status: 'busy', records: [], reason: 'runtime is not idle' };
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
const records = [];
|
|
704
|
+
const bundles = Array.isArray(message.bundles)
|
|
705
|
+
? message.bundles.filter(bundle => isValidBundle(bundle) && sha256(bundle.rawAuth) === bundle.authSha256)
|
|
706
|
+
: [];
|
|
707
|
+
for (const bundle of bundles) {
|
|
708
|
+
const validation = await this.validateAuditBundle(bundle);
|
|
709
|
+
records.push({
|
|
710
|
+
candidateName: bundle.candidateName,
|
|
711
|
+
authSha256: bundle.authSha256,
|
|
712
|
+
status: validation.ok ? 'valid' : 'invalid',
|
|
713
|
+
reason: validation.ok ? null : validation.reason ?? 'usage validation failed',
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
report = { nodeId: this.nodeId ?? 'local', status: 'completed', records, reason: null };
|
|
717
|
+
}
|
|
718
|
+
await this.sendToPeer(peer, {
|
|
719
|
+
kind: 'audit.verify.response',
|
|
720
|
+
requestId: message.requestId,
|
|
721
|
+
verificationId: message.verificationId,
|
|
722
|
+
report,
|
|
723
|
+
});
|
|
724
|
+
this.recordEvent({
|
|
725
|
+
direction: 'local',
|
|
726
|
+
kind: 'audit.verify.response',
|
|
727
|
+
stage: 'sent',
|
|
728
|
+
peer,
|
|
729
|
+
requestId: message.requestId,
|
|
730
|
+
candidateName: null,
|
|
731
|
+
detail: `${report.status}; records=${report.records.length}`,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
handleAuditVerificationResponse(message, peer) {
|
|
735
|
+
const pending = this.pendingAuditVerifications.get(message.verificationId);
|
|
736
|
+
if (!pending || pending.finished || !isValidAuditVerificationReport(message.report))
|
|
737
|
+
return;
|
|
738
|
+
const matchedPeer = this.matchConfiguredPeer(peer) ?? peer;
|
|
739
|
+
pending.reports.set(matchedPeer, message.report);
|
|
740
|
+
this.recordEvent({
|
|
741
|
+
direction: 'local',
|
|
742
|
+
kind: 'audit.verify.response',
|
|
743
|
+
stage: 'received',
|
|
744
|
+
peer: matchedPeer,
|
|
745
|
+
requestId: message.requestId,
|
|
746
|
+
candidateName: null,
|
|
747
|
+
detail: `${message.report.status}; records=${message.report.records.length}; node=${message.report.nodeId}`,
|
|
748
|
+
});
|
|
749
|
+
if (pending.peers.every(peerName => pending.reports.has(peerName))) {
|
|
750
|
+
this.finishPendingAuditVerification(message.verificationId);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
689
753
|
async handleAuditApply(message, senderNodeId, sourceLabel, peer) {
|
|
690
754
|
const outcome = await this.validateAndImport({ ...message.bundle, requestId: message.requestId }, senderNodeId, sourceLabel, peer, 'push', message.replaceExisting);
|
|
691
755
|
if (!outcome.ok)
|
|
@@ -747,7 +811,7 @@ export class CrossNodeAuthSync {
|
|
|
747
811
|
lastRefreshMs: candidate.lastRefreshMs,
|
|
748
812
|
status: validation.ok ? 'valid' : 'invalid',
|
|
749
813
|
reason: validation.ok ? null : validation.reason ?? 'usage validation failed',
|
|
750
|
-
bundle
|
|
814
|
+
bundle,
|
|
751
815
|
});
|
|
752
816
|
}
|
|
753
817
|
return {
|
|
@@ -757,6 +821,107 @@ export class CrossNodeAuthSync {
|
|
|
757
821
|
reason: null,
|
|
758
822
|
};
|
|
759
823
|
}
|
|
824
|
+
async verifySingleNodeInvalidCandidates(requestId, reports) {
|
|
825
|
+
const completedReports = reports.filter(report => report.status === 'completed');
|
|
826
|
+
const entries = new Map();
|
|
827
|
+
for (const report of completedReports) {
|
|
828
|
+
for (const record of report.records) {
|
|
829
|
+
const candidateEntries = entries.get(record.candidateName) ?? [];
|
|
830
|
+
candidateEntries.push({ report, record });
|
|
831
|
+
entries.set(record.candidateName, candidateEntries);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const bundles = [];
|
|
835
|
+
for (const candidateEntries of entries.values()) {
|
|
836
|
+
if (candidateEntries.some(entry => entry.record.status === 'valid'))
|
|
837
|
+
continue;
|
|
838
|
+
const invalidNodes = new Set(candidateEntries.map(entry => entry.report.nodeId));
|
|
839
|
+
if (invalidNodes.size >= 2)
|
|
840
|
+
continue;
|
|
841
|
+
const latest = candidateEntries
|
|
842
|
+
.filter(entry => entry.record.bundle !== null)
|
|
843
|
+
.sort((left, right) => right.record.lastRefreshMs - left.record.lastRefreshMs)[0];
|
|
844
|
+
if (latest?.record.bundle)
|
|
845
|
+
bundles.push(latest.record.bundle);
|
|
846
|
+
}
|
|
847
|
+
if (bundles.length === 0)
|
|
848
|
+
return;
|
|
849
|
+
const localReport = completedReports.find(report => report.nodeId === this.nodeId);
|
|
850
|
+
if (localReport) {
|
|
851
|
+
await this.verifyBundlesIntoAuditReport(localReport, bundles);
|
|
852
|
+
}
|
|
853
|
+
if (!this.isReady())
|
|
854
|
+
return;
|
|
855
|
+
const verificationId = crypto.randomUUID();
|
|
856
|
+
const remoteReports = await new Promise((resolve) => {
|
|
857
|
+
const timer = setTimeout(() => this.finishPendingAuditVerification(verificationId), AUDIT_TIMEOUT_MS);
|
|
858
|
+
timer.unref();
|
|
859
|
+
this.pendingAuditVerifications.set(verificationId, {
|
|
860
|
+
peers: [...this.peers],
|
|
861
|
+
reports: new Map(),
|
|
862
|
+
resolve,
|
|
863
|
+
timer,
|
|
864
|
+
finished: false,
|
|
865
|
+
});
|
|
866
|
+
void this.sendToAll({ kind: 'audit.verify.request', requestId, verificationId, bundles }).catch(() => {
|
|
867
|
+
this.finishPendingAuditVerification(verificationId);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
const bundlesByHash = new Map(bundles.map(bundle => [bundle.authSha256, bundle]));
|
|
871
|
+
for (const verification of remoteReports.values()) {
|
|
872
|
+
if (verification.status !== 'completed')
|
|
873
|
+
continue;
|
|
874
|
+
let target = completedReports.find(report => report.nodeId === verification.nodeId);
|
|
875
|
+
if (!target) {
|
|
876
|
+
target = { nodeId: verification.nodeId, status: 'completed', records: [], reason: null };
|
|
877
|
+
reports.push(target);
|
|
878
|
+
completedReports.push(target);
|
|
879
|
+
}
|
|
880
|
+
for (const record of verification.records) {
|
|
881
|
+
const bundle = bundlesByHash.get(record.authSha256);
|
|
882
|
+
if (!bundle || bundle.candidateName !== record.candidateName)
|
|
883
|
+
continue;
|
|
884
|
+
if (record.status === 'valid') {
|
|
885
|
+
target.records = target.records.filter(existing => existing.candidateName !== record.candidateName);
|
|
886
|
+
}
|
|
887
|
+
else if (target.records.some(existing => existing.candidateName === record.candidateName)) {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
target.records.push({
|
|
891
|
+
candidateName: bundle.candidateName,
|
|
892
|
+
accountId: bundle.accountId,
|
|
893
|
+
quotaIdentityId: bundle.quotaIdentityId ?? null,
|
|
894
|
+
lastRefreshMs: bundle.lastRefreshMs,
|
|
895
|
+
status: record.status,
|
|
896
|
+
reason: record.reason,
|
|
897
|
+
bundle,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
async verifyBundlesIntoAuditReport(report, bundles) {
|
|
903
|
+
for (const bundle of bundles) {
|
|
904
|
+
if (report.records.some(record => record.candidateName === bundle.candidateName))
|
|
905
|
+
continue;
|
|
906
|
+
const validation = await this.validateAuditBundle(bundle);
|
|
907
|
+
report.records.push({
|
|
908
|
+
candidateName: bundle.candidateName,
|
|
909
|
+
accountId: bundle.accountId,
|
|
910
|
+
quotaIdentityId: bundle.quotaIdentityId ?? null,
|
|
911
|
+
lastRefreshMs: bundle.lastRefreshMs,
|
|
912
|
+
status: validation.ok ? 'valid' : 'invalid',
|
|
913
|
+
reason: validation.ok ? null : validation.reason ?? 'usage validation failed',
|
|
914
|
+
bundle,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async validateAuditBundle(bundle) {
|
|
919
|
+
const expiresAt = readAccessTokenExpiresAtMs(bundle.rawAuth);
|
|
920
|
+
if (expiresAt === null || expiresAt <= Date.now() + REMOTE_ACCESS_TOKEN_MIN_TTL_MS) {
|
|
921
|
+
return { ok: false, reason: 'access token is expired or missing exp' };
|
|
922
|
+
}
|
|
923
|
+
return this.callbacks.validateCandidate(bundle.candidateName, bundle.rawAuth, bundle.accountId);
|
|
924
|
+
}
|
|
760
925
|
async reconcileAuditReports(requestId, reports, missingPeers) {
|
|
761
926
|
const completedReports = reports.filter(report => report.status === 'completed');
|
|
762
927
|
const busyNodes = reports.filter(report => report.status === 'busy').map(report => report.nodeId);
|
|
@@ -872,6 +1037,15 @@ export class CrossNodeAuthSync {
|
|
|
872
1037
|
const missing = pending.peers.filter(peer => !pending.reports.has(peer));
|
|
873
1038
|
pending.resolve({ reports: pending.reports, missing });
|
|
874
1039
|
}
|
|
1040
|
+
finishPendingAuditVerification(verificationId) {
|
|
1041
|
+
const pending = this.pendingAuditVerifications.get(verificationId);
|
|
1042
|
+
if (!pending || pending.finished)
|
|
1043
|
+
return;
|
|
1044
|
+
pending.finished = true;
|
|
1045
|
+
clearTimeout(pending.timer);
|
|
1046
|
+
this.pendingAuditVerifications.delete(verificationId);
|
|
1047
|
+
pending.resolve(pending.reports);
|
|
1048
|
+
}
|
|
875
1049
|
async handleServiceUpdateRequest(message, senderNodeId, sourceLabel, peer) {
|
|
876
1050
|
let result = {
|
|
877
1051
|
accepted: false,
|
|
@@ -1722,6 +1896,18 @@ function isValidAuditReport(value) {
|
|
|
1722
1896
|
&& (record.bundle === null || isValidBundle(record.bundle))
|
|
1723
1897
|
&& (record.status !== 'valid' || record.bundle !== null)));
|
|
1724
1898
|
}
|
|
1899
|
+
function isValidAuditVerificationReport(value) {
|
|
1900
|
+
return Boolean(value)
|
|
1901
|
+
&& typeof value.nodeId === 'string'
|
|
1902
|
+
&& (value.status === 'completed' || value.status === 'busy')
|
|
1903
|
+
&& Array.isArray(value.records)
|
|
1904
|
+
&& (typeof value.reason === 'string' || value.reason === null)
|
|
1905
|
+
&& value.records.every(record => (typeof record.candidateName === 'string'
|
|
1906
|
+
&& isAuthCandidateName(record.candidateName)
|
|
1907
|
+
&& typeof record.authSha256 === 'string'
|
|
1908
|
+
&& (record.status === 'valid' || record.status === 'invalid')
|
|
1909
|
+
&& (typeof record.reason === 'string' || record.reason === null)));
|
|
1910
|
+
}
|
|
1725
1911
|
function auditIdentityKey(accountId, quotaIdentityId) {
|
|
1726
1912
|
return `${accountId}\u0000${quotaIdentityId || accountId}`;
|
|
1727
1913
|
}
|
|
@@ -1835,6 +2021,8 @@ function requestIdFromMessage(message) {
|
|
|
1835
2021
|
case 'test.pong':
|
|
1836
2022
|
case 'audit.request':
|
|
1837
2023
|
case 'audit.response':
|
|
2024
|
+
case 'audit.verify.request':
|
|
2025
|
+
case 'audit.verify.response':
|
|
1838
2026
|
case 'audit.apply':
|
|
1839
2027
|
case 'audit.state':
|
|
1840
2028
|
case 'service.update.request':
|
|
@@ -1859,6 +2047,8 @@ function candidateNameFromMessage(message) {
|
|
|
1859
2047
|
return message.bundle?.candidateName ?? null;
|
|
1860
2048
|
case 'audit.apply':
|
|
1861
2049
|
return message.bundle.candidateName;
|
|
2050
|
+
case 'audit.verify.request':
|
|
2051
|
+
return message.bundles.length === 1 ? message.bundles[0]?.candidateName ?? null : null;
|
|
1862
2052
|
case 'audit.state':
|
|
1863
2053
|
return message.candidateName;
|
|
1864
2054
|
default:
|
package/dist/codex_app/client.js
CHANGED
|
@@ -1199,7 +1199,7 @@ function normalizeCollaborationMode(value) {
|
|
|
1199
1199
|
return value === 'default' || value === 'plan' ? value : null;
|
|
1200
1200
|
}
|
|
1201
1201
|
function normalizeReasoningEffort(value) {
|
|
1202
|
-
return typeof value === 'string' && ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(value)
|
|
1202
|
+
return typeof value === 'string' && ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'].includes(value)
|
|
1203
1203
|
? value
|
|
1204
1204
|
: null;
|
|
1205
1205
|
}
|
|
@@ -523,7 +523,11 @@ export class BridgeSessionCore {
|
|
|
523
523
|
}
|
|
524
524
|
lines.push(...codexUsageLines);
|
|
525
525
|
lines.push(...codexLocalUsageLines);
|
|
526
|
-
|
|
526
|
+
lines.splice(5, 0, t(locale, 'status_codex_home', {
|
|
527
|
+
value: this.config.codexHome ?? path.join(os.homedir(), '.codex'),
|
|
528
|
+
}));
|
|
529
|
+
const messageId = await this.sendRichInternalMessage(scopeId, '/status', lines.join('\n'));
|
|
530
|
+
this.scheduleStalePanelDeletion(scopeId, messageId);
|
|
527
531
|
return;
|
|
528
532
|
}
|
|
529
533
|
case 'account': {
|
|
@@ -4655,6 +4659,11 @@ export class BridgeSessionCore {
|
|
|
4655
4659
|
this.scheduleSelfUpdateStatusPoll();
|
|
4656
4660
|
return;
|
|
4657
4661
|
}
|
|
4662
|
+
if (status.scopeId.startsWith('cluster:')) {
|
|
4663
|
+
this.coordinator?.selfUpdateCompleted?.(status);
|
|
4664
|
+
await this.selfUpdater?.clearStatus();
|
|
4665
|
+
return;
|
|
4666
|
+
}
|
|
4658
4667
|
if (!this.ownsScope(status.scopeId)) {
|
|
4659
4668
|
this.scheduleSelfUpdateStatusPoll();
|
|
4660
4669
|
return;
|
|
@@ -4943,7 +4952,7 @@ export class BridgeSessionCore {
|
|
|
4943
4952
|
await this.sendMessage(scopeId, message);
|
|
4944
4953
|
return;
|
|
4945
4954
|
}
|
|
4946
|
-
if (action === 'audit' || action === 'check') {
|
|
4955
|
+
if (action === 'audit' || action === 'check' || action === 'safe') {
|
|
4947
4956
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4948
4957
|
await this.sendMessage(scopeId, t(locale, 'auth_cluster_audit_blocked_active'));
|
|
4949
4958
|
return;
|
|
@@ -4954,10 +4963,10 @@ export class BridgeSessionCore {
|
|
|
4954
4963
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
|
|
4955
4964
|
return;
|
|
4956
4965
|
}
|
|
4957
|
-
await this.sendRichInternalMessage(scopeId, '/auth sync
|
|
4966
|
+
await this.sendRichInternalMessage(scopeId, '/auth sync safe', formatAuthClusterAuditResult(locale, outcome));
|
|
4958
4967
|
return;
|
|
4959
4968
|
}
|
|
4960
|
-
if (action === '
|
|
4969
|
+
if (action === 'push' && args[1]?.toLowerCase() === 'all') {
|
|
4961
4970
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4962
4971
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_push_blocked_active'));
|
|
4963
4972
|
return;
|
|
@@ -5798,8 +5807,20 @@ export class BridgeSessionCore {
|
|
|
5798
5807
|
if (record.messageId !== null) {
|
|
5799
5808
|
await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_safe_starting'), []);
|
|
5800
5809
|
}
|
|
5801
|
-
|
|
5802
|
-
|
|
5810
|
+
if (record.messageId !== null) {
|
|
5811
|
+
this.pauseStalePanelDeletion(event.scopeId, record.messageId);
|
|
5812
|
+
}
|
|
5813
|
+
let outcome;
|
|
5814
|
+
try {
|
|
5815
|
+
outcome = await this.runAuthClusterAudit();
|
|
5816
|
+
}
|
|
5817
|
+
catch (error) {
|
|
5818
|
+
if (record.messageId !== null) {
|
|
5819
|
+
await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_cluster_audit_failed', { error: formatUserError(error) }), authChoiceKeyboard(locale, record));
|
|
5820
|
+
}
|
|
5821
|
+
return;
|
|
5822
|
+
}
|
|
5823
|
+
if (!outcome) {
|
|
5803
5824
|
if (record.messageId !== null) {
|
|
5804
5825
|
await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_disabled'), authChoiceKeyboard(locale, record));
|
|
5805
5826
|
}
|
|
@@ -5811,12 +5832,7 @@ export class BridgeSessionCore {
|
|
|
5811
5832
|
record.createdAt = Date.now();
|
|
5812
5833
|
clampCodexAuthListOffset(record);
|
|
5813
5834
|
if (record.messageId !== null) {
|
|
5814
|
-
await this.editAuthPanelMessage(event.scopeId, record.messageId, `${
|
|
5815
|
-
localSynced: result.localSynced,
|
|
5816
|
-
localSkipped: result.localSkipped,
|
|
5817
|
-
sent: result.sent,
|
|
5818
|
-
skipped: result.skipped,
|
|
5819
|
-
})}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
5835
|
+
await this.editAuthPanelMessage(event.scopeId, record.messageId, `${formatAuthClusterAuditResult(locale, outcome)}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
5820
5836
|
}
|
|
5821
5837
|
return;
|
|
5822
5838
|
}
|
|
@@ -10488,17 +10504,8 @@ function authChoiceKeyboard(locale, record) {
|
|
|
10488
10504
|
if (record.searchTerm) {
|
|
10489
10505
|
rows.push([{ text: t(locale, 'button_clear_filter'), callback_data: `auth:${record.localId}:clear_search` }]);
|
|
10490
10506
|
}
|
|
10491
|
-
rows.push([
|
|
10492
|
-
|
|
10493
|
-
{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
|
|
10494
|
-
]);
|
|
10495
|
-
rows.push([
|
|
10496
|
-
{ text: t(locale, 'button_auth_cluster_audit'), callback_data: `auth:${record.localId}:cluster_audit` },
|
|
10497
|
-
]);
|
|
10498
|
-
rows.push([
|
|
10499
|
-
{ text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` },
|
|
10500
|
-
{ text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` },
|
|
10501
|
-
]);
|
|
10507
|
+
rows.push([{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` }]);
|
|
10508
|
+
rows.push([{ text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` }]);
|
|
10502
10509
|
return rows;
|
|
10503
10510
|
}
|
|
10504
10511
|
function formatCodexAuthCandidateDisplayName(name) {
|
|
@@ -542,7 +542,9 @@ export function normalizeRequestedEffort(value) {
|
|
|
542
542
|
|| normalized === 'low'
|
|
543
543
|
|| normalized === 'medium'
|
|
544
544
|
|| normalized === 'high'
|
|
545
|
-
|| normalized === 'xhigh'
|
|
545
|
+
|| normalized === 'xhigh'
|
|
546
|
+
|| normalized === 'max'
|
|
547
|
+
|| normalized === 'ultra') {
|
|
546
548
|
return normalized;
|
|
547
549
|
}
|
|
548
550
|
return null;
|
package/dist/i18n.d.ts
CHANGED
|
@@ -95,6 +95,7 @@ declare const MESSAGES: {
|
|
|
95
95
|
readonly status_user_agent: "Codex/FoxClaw agent: {value}";
|
|
96
96
|
readonly status_current_thread: "Current thread: {value}";
|
|
97
97
|
readonly status_configured_model: "Configured model: {value}";
|
|
98
|
+
readonly status_codex_home: "Codex home: {value}";
|
|
98
99
|
readonly status_configured_effort: "Configured effort: {value}";
|
|
99
100
|
readonly status_fast: "Fast: {value}";
|
|
100
101
|
readonly status_collaboration_mode: "Mode: {value}";
|
|
@@ -245,7 +246,7 @@ declare const MESSAGES: {
|
|
|
245
246
|
readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
|
|
246
247
|
readonly auth_sync_test_missing: "Missing replies: {value}";
|
|
247
248
|
readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
|
|
248
|
-
readonly auth_sync_safe_starting: "
|
|
249
|
+
readonly auth_sync_safe_starting: "Auditing every node, selecting the newest valid auth, and safely synchronizing the cluster...";
|
|
249
250
|
readonly auth_sync_safe_done: "Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.";
|
|
250
251
|
readonly auth_cluster_audit_starting: "Asking all auth-sync peers to validate accounts, reconcile the newest usable credentials, and refresh credentials at least 8 days old...";
|
|
251
252
|
readonly auth_cluster_audit_blocked_active: "Cluster auth check requires every local runtime, approval, input, login, and auth mirror write to be idle.";
|
|
@@ -282,7 +283,7 @@ declare const MESSAGES: {
|
|
|
282
283
|
readonly auth_sync_trace_missing: "Usage: /auth sync trace <requestId>";
|
|
283
284
|
readonly button_login_device: "🔑 Login";
|
|
284
285
|
readonly button_auth_reload: "🔄 Reload auth";
|
|
285
|
-
readonly button_auth_safe_sync: "
|
|
286
|
+
readonly button_auth_safe_sync: "🩺 Safe sync";
|
|
286
287
|
readonly button_auth_cluster_audit: "🩺 Check all nodes and reconcile auth";
|
|
287
288
|
readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
|
|
288
289
|
readonly button_auth_repair_login: "🔑 Login repair";
|
|
@@ -394,7 +395,7 @@ declare const MESSAGES: {
|
|
|
394
395
|
readonly effort_adjusted_model: "Effort {effort} is not supported by {model}, so it was adjusted.";
|
|
395
396
|
readonly effort_change_blocked: "Cannot change reasoning effort while a turn is active. Use /interrupt or wait.";
|
|
396
397
|
readonly effort_reset: "Configured effort reset to server default.";
|
|
397
|
-
readonly usage_effort: "Usage: /effort <none|minimal|low|medium|high|xhigh|default>\nOr just use /models and tap buttons.";
|
|
398
|
+
readonly usage_effort: "Usage: /effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\nOr just use /models and tap buttons.";
|
|
398
399
|
readonly model_does_not_support_effort: "{model} does not support {effort}.\nSupported: {supported}";
|
|
399
400
|
readonly effort_configured: "Configured effort: {effort}";
|
|
400
401
|
readonly usage_fast: "Usage: /fast <on|off|toggle>";
|
|
@@ -818,6 +819,7 @@ declare const MESSAGES: {
|
|
|
818
819
|
readonly status_user_agent: "Codex/FoxClaw 标识:{value}";
|
|
819
820
|
readonly status_current_thread: "当前线程:{value}";
|
|
820
821
|
readonly status_configured_model: "已配置模型:{value}";
|
|
822
|
+
readonly status_codex_home: "Codex Home:{value}";
|
|
821
823
|
readonly status_configured_effort: "已配置推理强度:{value}";
|
|
822
824
|
readonly status_fast: "Fast:{value}";
|
|
823
825
|
readonly status_collaboration_mode: "模式:{value}";
|
|
@@ -968,7 +970,7 @@ declare const MESSAGES: {
|
|
|
968
970
|
readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
|
|
969
971
|
readonly auth_sync_test_missing: "未回应:{value}";
|
|
970
972
|
readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
|
|
971
|
-
readonly auth_sync_safe_starting: "
|
|
973
|
+
readonly auth_sync_safe_starting: "正在自检全部节点、选择最新有效 auth,并安全同步整个集群...";
|
|
972
974
|
readonly auth_sync_safe_done: "安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。";
|
|
973
975
|
readonly auth_cluster_audit_starting: "正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...";
|
|
974
976
|
readonly auth_cluster_audit_blocked_active: "集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。";
|
|
@@ -1005,7 +1007,7 @@ declare const MESSAGES: {
|
|
|
1005
1007
|
readonly auth_sync_trace_missing: "用法:/auth sync trace <requestId>";
|
|
1006
1008
|
readonly button_login_device: "🔑 设备登录";
|
|
1007
1009
|
readonly button_auth_reload: "🔄 重载 auth";
|
|
1008
|
-
readonly button_auth_safe_sync: "
|
|
1010
|
+
readonly button_auth_safe_sync: "🩺 安全同步";
|
|
1009
1011
|
readonly button_auth_cluster_audit: "🩺 全节点自检并同步";
|
|
1010
1012
|
readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
|
|
1011
1013
|
readonly button_auth_repair_login: "🔑 登录修复";
|
|
@@ -1117,7 +1119,7 @@ declare const MESSAGES: {
|
|
|
1117
1119
|
readonly effort_adjusted_model: "{model} 不支持 {effort},所以已自动调整推理强度。";
|
|
1118
1120
|
readonly effort_change_blocked: "当前有回复在进行中,暂时不能切换推理强度。请先等待,或使用 /interrupt。";
|
|
1119
1121
|
readonly effort_reset: "推理强度已重置为服务端默认值。";
|
|
1120
|
-
readonly usage_effort: "用法:/effort <none|minimal|low|medium|high|xhigh|default>\n或者直接使用 /models 点按钮。";
|
|
1122
|
+
readonly usage_effort: "用法:/effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\n或者直接使用 /models 点按钮。";
|
|
1121
1123
|
readonly model_does_not_support_effort: "{model} 不支持 {effort}。\n支持的强度:{supported}";
|
|
1122
1124
|
readonly effort_configured: "已配置推理强度:{effort}";
|
|
1123
1125
|
readonly usage_fast: "用法:/fast <on|off|toggle>";
|
package/dist/i18n.js
CHANGED
|
@@ -93,6 +93,7 @@ const MESSAGES = {
|
|
|
93
93
|
status_user_agent: 'Codex/FoxClaw agent: {value}',
|
|
94
94
|
status_current_thread: 'Current thread: {value}',
|
|
95
95
|
status_configured_model: 'Configured model: {value}',
|
|
96
|
+
status_codex_home: 'Codex home: {value}',
|
|
96
97
|
status_configured_effort: 'Configured effort: {value}',
|
|
97
98
|
status_fast: 'Fast: {value}',
|
|
98
99
|
status_collaboration_mode: 'Mode: {value}',
|
|
@@ -243,7 +244,7 @@ const MESSAGES = {
|
|
|
243
244
|
auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
|
|
244
245
|
auth_sync_test_missing: 'Missing replies: {value}',
|
|
245
246
|
auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
|
|
246
|
-
auth_sync_safe_starting: '
|
|
247
|
+
auth_sync_safe_starting: 'Auditing every node, selecting the newest valid auth, and safely synchronizing the cluster...',
|
|
247
248
|
auth_sync_safe_done: 'Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.',
|
|
248
249
|
auth_cluster_audit_starting: 'Asking all auth-sync peers to validate accounts, reconcile the newest usable credentials, and refresh credentials at least 8 days old...',
|
|
249
250
|
auth_cluster_audit_blocked_active: 'Cluster auth check requires every local runtime, approval, input, login, and auth mirror write to be idle.',
|
|
@@ -280,7 +281,7 @@ const MESSAGES = {
|
|
|
280
281
|
auth_sync_trace_missing: 'Usage: /auth sync trace <requestId>',
|
|
281
282
|
button_login_device: '🔑 Login',
|
|
282
283
|
button_auth_reload: '🔄 Reload auth',
|
|
283
|
-
button_auth_safe_sync: '
|
|
284
|
+
button_auth_safe_sync: '🩺 Safe sync',
|
|
284
285
|
button_auth_cluster_audit: '🩺 Check all nodes and reconcile auth',
|
|
285
286
|
button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
|
|
286
287
|
button_auth_repair_login: '🔑 Login repair',
|
|
@@ -392,7 +393,7 @@ const MESSAGES = {
|
|
|
392
393
|
effort_adjusted_model: 'Effort {effort} is not supported by {model}, so it was adjusted.',
|
|
393
394
|
effort_change_blocked: 'Cannot change reasoning effort while a turn is active. Use /interrupt or wait.',
|
|
394
395
|
effort_reset: 'Configured effort reset to server default.',
|
|
395
|
-
usage_effort: 'Usage: /effort <none|minimal|low|medium|high|xhigh|default>\nOr just use /models and tap buttons.',
|
|
396
|
+
usage_effort: 'Usage: /effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\nOr just use /models and tap buttons.',
|
|
396
397
|
model_does_not_support_effort: '{model} does not support {effort}.\nSupported: {supported}',
|
|
397
398
|
effort_configured: 'Configured effort: {effort}',
|
|
398
399
|
usage_fast: 'Usage: /fast <on|off|toggle>',
|
|
@@ -816,6 +817,7 @@ const MESSAGES = {
|
|
|
816
817
|
status_user_agent: 'Codex/FoxClaw 标识:{value}',
|
|
817
818
|
status_current_thread: '当前线程:{value}',
|
|
818
819
|
status_configured_model: '已配置模型:{value}',
|
|
820
|
+
status_codex_home: 'Codex Home:{value}',
|
|
819
821
|
status_configured_effort: '已配置推理强度:{value}',
|
|
820
822
|
status_fast: 'Fast:{value}',
|
|
821
823
|
status_collaboration_mode: '模式:{value}',
|
|
@@ -966,7 +968,7 @@ const MESSAGES = {
|
|
|
966
968
|
auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
|
|
967
969
|
auth_sync_test_missing: '未回应:{value}',
|
|
968
970
|
auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
|
|
969
|
-
auth_sync_safe_starting: '
|
|
971
|
+
auth_sync_safe_starting: '正在自检全部节点、选择最新有效 auth,并安全同步整个集群...',
|
|
970
972
|
auth_sync_safe_done: '安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。',
|
|
971
973
|
auth_cluster_audit_starting: '正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...',
|
|
972
974
|
auth_cluster_audit_blocked_active: '集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。',
|
|
@@ -1003,7 +1005,7 @@ const MESSAGES = {
|
|
|
1003
1005
|
auth_sync_trace_missing: '用法:/auth sync trace <requestId>',
|
|
1004
1006
|
button_login_device: '🔑 设备登录',
|
|
1005
1007
|
button_auth_reload: '🔄 重载 auth',
|
|
1006
|
-
button_auth_safe_sync: '
|
|
1008
|
+
button_auth_safe_sync: '🩺 安全同步',
|
|
1007
1009
|
button_auth_cluster_audit: '🩺 全节点自检并同步',
|
|
1008
1010
|
button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
|
|
1009
1011
|
button_auth_repair_login: '🔑 登录修复',
|
|
@@ -1115,7 +1117,7 @@ const MESSAGES = {
|
|
|
1115
1117
|
effort_adjusted_model: '{model} 不支持 {effort},所以已自动调整推理强度。',
|
|
1116
1118
|
effort_change_blocked: '当前有回复在进行中,暂时不能切换推理强度。请先等待,或使用 /interrupt。',
|
|
1117
1119
|
effort_reset: '推理强度已重置为服务端默认值。',
|
|
1118
|
-
usage_effort: '用法:/effort <none|minimal|low|medium|high|xhigh|default>\n或者直接使用 /models 点按钮。',
|
|
1120
|
+
usage_effort: '用法:/effort <none|minimal|low|medium|high|xhigh|max|ultra|default>\n或者直接使用 /models 点按钮。',
|
|
1119
1121
|
model_does_not_support_effort: '{model} 不支持 {effort}。\n支持的强度:{supported}',
|
|
1120
1122
|
effort_configured: '已配置推理强度:{effort}',
|
|
1121
1123
|
usage_fast: '用法:/fast <on|off|toggle>',
|
package/dist/types.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type SandboxModeValue = 'read-only' | 'workspace-write' | 'danger-full-ac
|
|
|
11
11
|
export type AccessPresetValue = 'read-only' | 'default' | 'full-access';
|
|
12
12
|
export type CollaborationModeValue = 'default' | 'plan';
|
|
13
13
|
export type ActiveTurnMessageMode = 'steer' | 'queue';
|
|
14
|
-
export type ReasoningEffortValue = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
|
14
|
+
export type ReasoningEffortValue = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra';
|
|
15
15
|
export type ThreadStatusKind = 'active' | 'idle' | 'notLoaded' | 'systemError';
|
|
16
16
|
export interface ChatSessionSettings {
|
|
17
17
|
/** Bridge scope id (e.g. `telegram:…`). */
|
|
@@ -153,11 +153,11 @@ Auth sync test complete: sent 1, replies 1.
|
|
|
153
153
|
|
|
154
154
|
If it shows `Missing replies: @peer_bot`, Telegram delivery may have succeeded, but the peer did not receive, decrypt, pass allowlist validation, or run the same auth sync configuration.
|
|
155
155
|
|
|
156
|
-
The `/auth` panel
|
|
156
|
+
The `/auth` panel's **Safe sync** action (`/auth sync safe`; `/auth sync audit` remains a compatibility alias) performs the complete all-node audit and reconciliation flow. It asks every configured peer to validate every local candidate against the usage endpoint, then:
|
|
157
157
|
|
|
158
158
|
- selects the newest usage-validated copy for each same-name, same-account and same-user identity and distributes it to every peer;
|
|
159
159
|
- allows a validated older copy to replace a newer timestamped copy only when the newer copy failed this explicit audit;
|
|
160
|
-
-
|
|
160
|
+
- sends a single-node invalid candidate through the encrypted channel for validation-only checks on other nodes, without importing it, and marks it `?` only when no valid result exists and at least two nodes independently report it invalid;
|
|
161
161
|
- leaves candidates unchanged when any peer is missing or busy, or when account/user identities conflict;
|
|
162
162
|
- refreshes enabled ChatGPT candidates whose `last_refresh` is at least 8 days old on the initiating node, then distributes the refreshed copies.
|
|
163
163
|
|
package/docs/user-manual.md
CHANGED
|
@@ -268,7 +268,7 @@ foxclaw send-media /absolute/path/output.mp4 "caption"
|
|
|
268
268
|
It controls:
|
|
269
269
|
|
|
270
270
|
- Model: server default, or a model returned by app-server.
|
|
271
|
-
- Reasoning effort: for example `low`, `medium`, `high`, or `
|
|
271
|
+
- Reasoning effort: for example `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`, depending on model support.
|
|
272
272
|
- Fast tier: available when supported by the selected model.
|
|
273
273
|
- Access: `read-only`, `default`, or `full-access`.
|
|
274
274
|
- Mode: `Agent` or `Plan`.
|
|
@@ -433,13 +433,13 @@ Quota remaining: window:percent|auth
|
|
|
433
433
|
[✅ 20|25|personal] [✅]
|
|
434
434
|
[🔐 —|—|team] [✅]
|
|
435
435
|
[☑️ All] [Enabled] [Attention]
|
|
436
|
-
[
|
|
437
|
-
[
|
|
436
|
+
[🔑 Login]
|
|
437
|
+
[🩺 Safe sync]
|
|
438
438
|
```
|
|
439
439
|
|
|
440
440
|
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.
|
|
441
441
|
|
|
442
|
-
The **
|
|
442
|
+
The **Safe sync** button asks every configured auth-sync peer to validate candidates, adopts the newest valid same-account copy, and distributes it. A candidate that exists on only one node is sent to other nodes for validation only, without being imported; it is marked `?` only when no node validates it and at least two nodes independently reject it. The initiating node also refreshes enabled ChatGPT credentials last refreshed at least 8 days ago. Missing, busy, or identity-conflicting peers prevent stale refresh for that run.
|
|
443
443
|
|
|
444
444
|
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.
|
|
445
445
|
|
|
@@ -497,7 +497,7 @@ Commands:
|
|
|
497
497
|
- `/auth sync events [filter]`: show recent sync event records, optionally filtered by candidate, peer, request id, kind, stage, or detail.
|
|
498
498
|
- `/auth sync trace <requestId>`: show recent records for one request id or event id.
|
|
499
499
|
- `/auth sync test`: send an encrypted ping and wait for peer pong replies to verify peer config, shared key, and Bot-to-Bot private messages.
|
|
500
|
-
- `/auth sync
|
|
500
|
+
- `/auth sync safe`: run the same all-node validation, reconciliation, invalid verification, stale-refresh, and distribution flow as the `/auth` Safe sync button. `/auth sync audit` remains a compatibility alias.
|
|
501
501
|
- `/auth sync push all`: manually broadcast all locally verified candidates without refreshing tokens. “Sent” does not mean the peer imported files; check `/auth sync status` and `/auth` on the peer.
|
|
502
502
|
|
|
503
503
|
Equivalent commands:
|
|
@@ -153,11 +153,11 @@ auth sync 测试完成:已发送 1,收到回应 1。
|
|
|
153
153
|
|
|
154
154
|
如果显示 `未回应:@peer_bot`,说明 Telegram 发送可能成功,但对方没有成功接收、解密、通过 allowlist,或没有运行同一组 auth sync 配置。
|
|
155
155
|
|
|
156
|
-
`/auth`
|
|
156
|
+
`/auth` 面板的 **安全同步**(命令等价入口是 `/auth sync safe`,旧的 `/auth sync audit` 仍兼容)会执行完整的全节点自检与同步。它会让所有已配置 peer 用 usage 接口逐个验证本机候选,然后:
|
|
157
157
|
|
|
158
158
|
- 对同名、同 account、同 ChatGPT 用户身份的候选,选择最新且验证有效的副本并分发给所有 peer;
|
|
159
159
|
- 只有显式审计已经证明时间戳更晚的副本无效时,才允许经过验证的较旧有效副本替换它;
|
|
160
|
-
-
|
|
160
|
+
- 对只存在于单个节点的无效候选,会通过加密通道交给其他节点做只验证、不导入的复核;没有任何有效结果且至少两个节点独立判定无效时,才把候选标为 `?`;
|
|
161
161
|
- 任一 peer 未回应、忙碌或账号/用户身份冲突时,不做无效裁决;
|
|
162
162
|
- 由发起节点刷新 `last_refresh` 已满 8 天的已启用 ChatGPT 候选,再把刷新结果分发给所有 peer。
|
|
163
163
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -268,7 +268,7 @@ foxclaw send-media /absolute/path/output.mp4 "说明"
|
|
|
268
268
|
它能配置:
|
|
269
269
|
|
|
270
270
|
- 模型:使用服务端默认模型,或选择 app-server 返回的模型。
|
|
271
|
-
- reasoning effort:例如 `low`、`medium`、`high`、`xhigh`,取决于模型支持情况。
|
|
271
|
+
- reasoning effort:例如 `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`,取决于模型支持情况。
|
|
272
272
|
- Fast tier:模型支持时可开关 fast 服务档。
|
|
273
273
|
- Access:`read-only`、`default`、`full-access`。
|
|
274
274
|
- Mode:`Agent` 或 `Plan`。
|
|
@@ -433,13 +433,13 @@ Candidates: 2
|
|
|
433
433
|
[✅ 20|25|personal] [✅]
|
|
434
434
|
[🔐 —|—|team] [✅]
|
|
435
435
|
[☑️ 全部] [已启用] [需关注]
|
|
436
|
-
[
|
|
437
|
-
[
|
|
436
|
+
[🔑 设备登录]
|
|
437
|
+
[🩺 安全同步]
|
|
438
438
|
```
|
|
439
439
|
|
|
440
440
|
右侧 `✅` / `⏸️` 表示当前是否参与自动轮转。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--` 表示该候选还没有额度历史快照。健康摘要会区分正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key、无效 auth 文件和需要登录修复。
|
|
441
441
|
|
|
442
|
-
|
|
442
|
+
**安全同步** 按钮会通知所有已配置 auth-sync peer 验证候选,采用并分发同账号最新有效副本。只存在于一台机器的无效候选也会交给其他节点做只验证、不导入的复核;没有任何节点验证有效且至少两个节点独立确认无效后才标记 `?`。`last_refresh` 已满 8 天的已启用 ChatGPT auth 由发起节点刷新后再分发。存在未回应、忙碌或身份冲突节点时,本轮不会做临期刷新。
|
|
443
443
|
|
|
444
444
|
当某个候选在实际使用中已经失败,并且 FoxClaw 无法从本机 mirror 或跨节点同步恢复同账号较新凭据时,会标为“需要登录修复”。这类候选不会参与自动轮转和后台主动刷新,也不会出现在“已启用”筛选里。它的按钮会显示 `?`。点击后有两个选择:`登录修复` 会在选中该候选的状态下启动设备码登录;`删除` 会从 canonical 和所有本机 bot runtime 中删除这个候选,并清理它的额度缓存。
|
|
445
445
|
|
|
@@ -497,7 +497,7 @@ AUTH_AUTO_DELETE_NEEDS_REPAIR=false
|
|
|
497
497
|
- `/auth sync events [过滤]`:查看最近同步事件,可按候选名、peer、request id、事件类型、阶段或详情过滤。
|
|
498
498
|
- `/auth sync trace <requestId>`:查看某个 request id 或事件 id 的最近流水。
|
|
499
499
|
- `/auth sync test`:发送加密 ping 并等待 peer 返回 pong,确认 peer、共享密钥和 Bot-to-Bot 私聊可用。
|
|
500
|
-
- `/auth sync
|
|
500
|
+
- `/auth sync safe`:执行与 `/auth` 面板“安全同步”按钮相同的全节点验证、协商、无效复核、临期刷新和分发流程。`/auth sync audit` 保留为兼容别名。
|
|
501
501
|
- `/auth sync push all`:手动广播当前节点已验证的全部候选,不刷新 token;“已发送”不等于对端已经导入,需要在 peer 上看 `/auth sync status` 和 `/auth`。
|
|
502
502
|
|
|
503
503
|
命令等价用法:
|