@foxden-app/foxclaw 0.5.20 → 0.5.22
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/.env.example +4 -0
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/dist/auth/cross_node_sync.d.ts +55 -0
- package/dist/auth/cross_node_sync.js +199 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/controller/controller.d.ts +5 -1
- package/dist/controller/controller.js +217 -28
- package/dist/i18n.d.ts +36 -0
- package/dist/i18n.js +36 -0
- package/dist/main.js +141 -7
- package/dist/store/database.d.ts +9 -0
- package/dist/store/database.js +87 -0
- package/docs/cross-node-auth-sync.md +4 -0
- package/docs/user-manual.md +3 -0
- package/docs/zh/cross-node-auth-sync.md +4 -0
- package/docs/zh/user-manual.md +3 -0
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -48,6 +48,10 @@ CODEX_CLI_BIN=/absolute/path/to/codex
|
|
|
48
48
|
# AUTH_SYNC_PEERS=@other_node_bot,@third_node_bot
|
|
49
49
|
# AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
|
|
50
50
|
# AUTH_SYNC_NODE_ID=
|
|
51
|
+
# Resource-rich auth pool mode: unrecoverable candidates that would need repair
|
|
52
|
+
# are deleted automatically and propagated across auth-sync peers. /config can
|
|
53
|
+
# toggle this at runtime and writes AUTH_AUTO_DELETE_NEEDS_REPAIR back here.
|
|
54
|
+
# AUTH_AUTO_DELETE_NEEDS_REPAIR=false
|
|
51
55
|
|
|
52
56
|
# Optional: standard HTTP(S) proxy for Telegram and ChatGPT/Codex backend requests.
|
|
53
57
|
# Put these in the same env file that `foxclaw start` installs into systemd/launchd.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
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.22 - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- Codex 报 `usageLimitExceeded`、`You've hit your usage limit`、usage/rate/quota/billing/credits limit 这类额度耗尽时,不再把当前 auth 候选标记为需要修复,也不会触发 `AUTH_AUTO_DELETE_NEEDS_REPAIR` 的自动剔除和跨节点删除。
|
|
9
|
+
- 额度耗尽仍会临时避开当前候选,切到另一个维护中的候选重试;提示文案改为“Codex 额度限制”,不再误写成 “Codex auth 问题”。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Codex quota exhaustion such as `usageLimitExceeded`, `You've hit your usage limit`, usage/rate/quota/billing/credits limit is no longer treated as an invalid auth candidate, so it does not mark `needs_repair` or trigger `AUTH_AUTO_DELETE_NEEDS_REPAIR` auto-delete / cross-node delete.
|
|
13
|
+
- Quota exhaustion still temporarily skips the current candidate and retries with another maintained candidate; notifications now call this a Codex usage limit instead of an auth problem.
|
|
14
|
+
|
|
15
|
+
## 0.5.21 - 2026-06-09
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- 新增资源富裕 auth 池模式:`AUTH_AUTO_DELETE_NEEDS_REPAIR=true` 或 `/config auth_auto_delete on` 会把原本要标记为 `?`/需要登录修复、且无法恢复的候选自动剔除。
|
|
19
|
+
- 自动剔除会通过跨节点 auth sync 发送删除 tombstone,peer 收到后删除同名候选,并优先处理删除,避免待导入队列把坏候选复活。
|
|
20
|
+
- `/status` 和 `/config` 现在显示 auth 池摘要:历史见过的候选数、当前存活数、因失效自动剔除数;`/config` 的开关会写回当前 FoxClaw `.env`。
|
|
21
|
+
- 开启自动剔除后,auth mirror / auth sync 的候选级同步、导入、删除和恢复通知会静默或汇总为池子摘要;同步发送/导入失败、删除失败和系统级 `sync_error` 仍会明确提示。
|
|
22
|
+
|
|
23
|
+
### English
|
|
24
|
+
- Added a resource-rich auth pool mode: `AUTH_AUTO_DELETE_NEEDS_REPAIR=true` or `/config auth_auto_delete on` automatically deletes unrecoverable candidates that would otherwise be marked `?` / needs login repair.
|
|
25
|
+
- Auto-delete now publishes a cross-node auth-sync delete tombstone, so peers delete the same candidate and process deletes before pending imports to avoid resurrecting bad candidates.
|
|
26
|
+
- `/status` and `/config` now show an auth-pool summary: total candidates seen, currently alive, and invalid-deleted count. The `/config` toggle writes back to the active FoxClaw `.env`.
|
|
27
|
+
- When auto-delete is enabled, candidate-level auth mirror / auth sync publish, import, delete, and recovery chatter is silenced or collapsed into pool summaries; sync send/import failures, delete failures, and system-level `sync_error` still notify explicitly.
|
|
28
|
+
|
|
5
29
|
## 0.5.20 - 2026-06-08
|
|
6
30
|
|
|
7
31
|
### 中文
|
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ TG_BOT_TOKEN=123456:token_a
|
|
|
216
216
|
|
|
217
217
|
FoxClaw 仍然只运行一个系统服务。默认情况下,它会为每个 bot 启动独立 `codex app-server` 和独立 `CODEX_HOME`。因此 A 私聊运行 turn 时,B 私聊仍可独立切换自己的 `/auth`。候选凭据由 FoxClaw 在登录或刷新在线验证后镜像同步;切换或重载前还会从其他 Codex home 恢复同账号较新凭据。各 bot 的当前选择互不影响。每个 bot 首次私聊发送 `/help` 和 `/status`;`/auth` 会标明正在操作的 bot runtime,`/status` 会列出全部 bot 的连接、runtime 类型、当前 auth 和活动 turn 摘要。
|
|
218
218
|
|
|
219
|
-
多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_contact_bot`。推荐每台机器只选一个联系人 bot;同一节点内的其他 bot 继续走本机 auth 镜像。多 bot 模式下,默认用 `TG_BOT_TOKENS` 的第一个 token 作为联系人 bot。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本,并在联系人 bot
|
|
219
|
+
多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_contact_bot`。推荐每台机器只选一个联系人 bot;同一节点内的其他 bot 继续走本机 auth 镜像。多 bot 模式下,默认用 `TG_BOT_TOKENS` 的第一个 token 作为联系人 bot。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本,并在联系人 bot 私聊里汇总报告发送、接收、排队、导入、失败和人工介入提示。资源富裕、只关心池子数量时,可用 `/config auth_auto_delete on` 或 `AUTH_AUTO_DELETE_NEEDS_REPAIR=true` 让无法恢复的候选自动跨节点剔除,并把通知压缩为池子摘要。`/auth sync events [过滤]` 和 `/auth sync trace <requestId>` 可查看最近通讯流水。跨节点恢复不会自动刷新 token,`/auth refresh all confirm` 会先申请跨节点刷新锁。完整配置、`@BotFather` 操作和验证步骤见 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
|
|
220
220
|
|
|
221
221
|
如果你需要一路 Telegram 与终端互通 session,把同一个 token 同时填入 `TG_BOT_TOKENS` 和 `TG_BOT_TOKEN`。这个 bot 使用默认 `CODEX_HOME`(未设置时通常是 `~/.codex`)和默认 auth,因此能看到终端 Codex 的本地线程;它不再享有隔离 runtime 的“互不影响”保证,切换 auth 会影响终端和其他默认 runtime。
|
|
222
222
|
|
|
@@ -123,6 +123,46 @@ export type AuthSyncNotification = {
|
|
|
123
123
|
peer: string;
|
|
124
124
|
mode: AuthSyncRemoteImportMode;
|
|
125
125
|
reason: string;
|
|
126
|
+
} | {
|
|
127
|
+
kind: 'candidate_delete_sent';
|
|
128
|
+
candidateName: string;
|
|
129
|
+
peers: string[];
|
|
130
|
+
reason: string | null;
|
|
131
|
+
} | {
|
|
132
|
+
kind: 'candidate_delete_failed';
|
|
133
|
+
candidateName: string;
|
|
134
|
+
peers: string[];
|
|
135
|
+
reason: string;
|
|
136
|
+
} | {
|
|
137
|
+
kind: 'remote_delete_received';
|
|
138
|
+
candidateName: string;
|
|
139
|
+
sourceNodeId: string;
|
|
140
|
+
sourceLabel: string;
|
|
141
|
+
peer: string;
|
|
142
|
+
queued: boolean;
|
|
143
|
+
queueLength: number;
|
|
144
|
+
reason: string | null;
|
|
145
|
+
} | {
|
|
146
|
+
kind: 'remote_delete_deleted';
|
|
147
|
+
candidateName: string;
|
|
148
|
+
sourceNodeId: string;
|
|
149
|
+
sourceLabel: string;
|
|
150
|
+
peer: string;
|
|
151
|
+
reason: string | null;
|
|
152
|
+
} | {
|
|
153
|
+
kind: 'remote_delete_skipped';
|
|
154
|
+
candidateName: string;
|
|
155
|
+
sourceNodeId: string;
|
|
156
|
+
sourceLabel: string;
|
|
157
|
+
peer: string;
|
|
158
|
+
reason: string;
|
|
159
|
+
} | {
|
|
160
|
+
kind: 'remote_delete_failed';
|
|
161
|
+
candidateName: string;
|
|
162
|
+
sourceNodeId: string;
|
|
163
|
+
sourceLabel: string;
|
|
164
|
+
peer: string;
|
|
165
|
+
reason: string;
|
|
126
166
|
} | {
|
|
127
167
|
kind: 'recovery_started';
|
|
128
168
|
candidateName: string;
|
|
@@ -170,6 +210,15 @@ export interface AuthSyncImportCallbacks {
|
|
|
170
210
|
nodeId: string;
|
|
171
211
|
label?: string | null;
|
|
172
212
|
}) => Promise<AuthMirrorImportResult>;
|
|
213
|
+
deleteLocalCandidate?: (candidateName: string, source: {
|
|
214
|
+
nodeId: string;
|
|
215
|
+
label?: string | null;
|
|
216
|
+
reason?: string | null;
|
|
217
|
+
}) => Promise<{
|
|
218
|
+
ok: boolean;
|
|
219
|
+
deleted: boolean;
|
|
220
|
+
reason?: string | null;
|
|
221
|
+
}>;
|
|
173
222
|
isIdle: () => boolean;
|
|
174
223
|
notify?: (event: AuthSyncNotification) => Promise<void>;
|
|
175
224
|
}
|
|
@@ -201,6 +250,7 @@ export declare class CrossNodeAuthSync {
|
|
|
201
250
|
private readonly peers;
|
|
202
251
|
private readonly peerKeys;
|
|
203
252
|
private readonly pendingImports;
|
|
253
|
+
private readonly pendingDeletes;
|
|
204
254
|
private readonly pendingPulls;
|
|
205
255
|
private readonly pendingLeases;
|
|
206
256
|
private readonly pendingTests;
|
|
@@ -208,6 +258,7 @@ export declare class CrossNodeAuthSync {
|
|
|
208
258
|
private lastPeerActivityAt;
|
|
209
259
|
private timer;
|
|
210
260
|
private importProcessorActive;
|
|
261
|
+
private deleteProcessorActive;
|
|
211
262
|
private activeRemoteLease;
|
|
212
263
|
private activeLocalLease;
|
|
213
264
|
private lastNotifiedError;
|
|
@@ -223,6 +274,7 @@ export declare class CrossNodeAuthSync {
|
|
|
223
274
|
sent: number;
|
|
224
275
|
skipped: number;
|
|
225
276
|
}>;
|
|
277
|
+
publishCandidateDeletion(candidateName: string, reason?: string | null): Promise<boolean>;
|
|
226
278
|
publishDigest(): Promise<void>;
|
|
227
279
|
requestRecovery(candidateName: string, current?: {
|
|
228
280
|
accountId: string | null;
|
|
@@ -242,6 +294,9 @@ export declare class CrossNodeAuthSync {
|
|
|
242
294
|
private handleLeaseReply;
|
|
243
295
|
private enqueueImport;
|
|
244
296
|
private processPendingImports;
|
|
297
|
+
private enqueueDelete;
|
|
298
|
+
private processPendingDeletes;
|
|
299
|
+
private deleteRemoteCandidate;
|
|
245
300
|
private validateAndImport;
|
|
246
301
|
private sendToAll;
|
|
247
302
|
private sendToPeer;
|
|
@@ -21,6 +21,7 @@ export class CrossNodeAuthSync {
|
|
|
21
21
|
peers;
|
|
22
22
|
peerKeys;
|
|
23
23
|
pendingImports = [];
|
|
24
|
+
pendingDeletes = [];
|
|
24
25
|
pendingPulls = new Map();
|
|
25
26
|
pendingLeases = new Map();
|
|
26
27
|
pendingTests = new Map();
|
|
@@ -28,6 +29,7 @@ export class CrossNodeAuthSync {
|
|
|
28
29
|
lastPeerActivityAt = new Map();
|
|
29
30
|
timer = null;
|
|
30
31
|
importProcessorActive = false;
|
|
32
|
+
deleteProcessorActive = false;
|
|
31
33
|
activeRemoteLease = null;
|
|
32
34
|
activeLocalLease = null;
|
|
33
35
|
lastNotifiedError = null;
|
|
@@ -79,8 +81,8 @@ export class CrossNodeAuthSync {
|
|
|
79
81
|
return;
|
|
80
82
|
this.timer = setInterval(() => {
|
|
81
83
|
this.expireLeases();
|
|
82
|
-
void this.processPendingImports().catch((error) => {
|
|
83
|
-
this.recordError(`pending
|
|
84
|
+
void this.processPendingDeletes().then(() => this.processPendingImports()).catch((error) => {
|
|
85
|
+
this.recordError(`pending auth sync work failed: ${formatError(error)}`);
|
|
84
86
|
});
|
|
85
87
|
}, 5_000);
|
|
86
88
|
this.timer.unref();
|
|
@@ -124,7 +126,9 @@ export class CrossNodeAuthSync {
|
|
|
124
126
|
}
|
|
125
127
|
isIdle() {
|
|
126
128
|
return this.pendingImports.length === 0
|
|
129
|
+
&& this.pendingDeletes.length === 0
|
|
127
130
|
&& !this.importProcessorActive
|
|
131
|
+
&& !this.deleteProcessorActive
|
|
128
132
|
&& this.pendingPulls.size === 0
|
|
129
133
|
&& this.pendingLeases.size === 0
|
|
130
134
|
&& this.pendingTests.size === 0
|
|
@@ -205,6 +209,48 @@ export class CrossNodeAuthSync {
|
|
|
205
209
|
throw error;
|
|
206
210
|
}
|
|
207
211
|
}
|
|
212
|
+
async publishCandidateDeletion(candidateName, reason = null) {
|
|
213
|
+
if (!this.isReady() || !isAuthCandidateName(candidateName))
|
|
214
|
+
return false;
|
|
215
|
+
const requestId = crypto.randomUUID();
|
|
216
|
+
try {
|
|
217
|
+
await this.sendToAll({
|
|
218
|
+
kind: 'delete.candidate',
|
|
219
|
+
requestId,
|
|
220
|
+
candidateName,
|
|
221
|
+
reason,
|
|
222
|
+
deletedAt: new Date().toISOString(),
|
|
223
|
+
});
|
|
224
|
+
this.recordEvent({
|
|
225
|
+
direction: 'local',
|
|
226
|
+
kind: 'delete.candidate',
|
|
227
|
+
stage: 'sent',
|
|
228
|
+
peer: null,
|
|
229
|
+
requestId,
|
|
230
|
+
candidateName,
|
|
231
|
+
detail: reason,
|
|
232
|
+
});
|
|
233
|
+
this.clearCandidateFailure(candidateName);
|
|
234
|
+
await this.writeState();
|
|
235
|
+
this.notify({ kind: 'candidate_delete_sent', candidateName, peers: [...this.peers], reason });
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const formatted = formatError(error);
|
|
240
|
+
this.recordError(`candidate delete publish failed for ${candidateName}: ${formatted}`, false);
|
|
241
|
+
this.recordEvent({
|
|
242
|
+
direction: 'local',
|
|
243
|
+
kind: 'delete.candidate',
|
|
244
|
+
stage: 'send_failed',
|
|
245
|
+
peer: null,
|
|
246
|
+
requestId,
|
|
247
|
+
candidateName,
|
|
248
|
+
detail: formatted,
|
|
249
|
+
});
|
|
250
|
+
this.notify({ kind: 'candidate_delete_failed', candidateName, peers: [...this.peers], reason: formatted });
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
208
254
|
async publishDigest() {
|
|
209
255
|
if (!this.isReady())
|
|
210
256
|
return;
|
|
@@ -455,6 +501,9 @@ export class CrossNodeAuthSync {
|
|
|
455
501
|
case 'pull.response':
|
|
456
502
|
await this.handlePullResponse(message, senderNodeId, sourceLabel, normalizePeerIdentity(peer));
|
|
457
503
|
return;
|
|
504
|
+
case 'delete.candidate':
|
|
505
|
+
this.enqueueDelete(message, senderNodeId, sourceLabel, normalizePeerIdentity(peer));
|
|
506
|
+
return;
|
|
458
507
|
case 'digest':
|
|
459
508
|
await this.handleDigest(message, normalizePeerIdentity(peer));
|
|
460
509
|
return;
|
|
@@ -717,6 +766,8 @@ export class CrossNodeAuthSync {
|
|
|
717
766
|
async processPendingImports() {
|
|
718
767
|
if (this.importProcessorActive)
|
|
719
768
|
return;
|
|
769
|
+
if (this.pendingDeletes.length > 0 || this.deleteProcessorActive)
|
|
770
|
+
return;
|
|
720
771
|
if (!this.callbacks.isIdle())
|
|
721
772
|
return;
|
|
722
773
|
this.importProcessorActive = true;
|
|
@@ -732,6 +783,150 @@ export class CrossNodeAuthSync {
|
|
|
732
783
|
this.importProcessorActive = false;
|
|
733
784
|
}
|
|
734
785
|
}
|
|
786
|
+
enqueueDelete(message, sourceNodeId, sourceLabel, fromPeer) {
|
|
787
|
+
if (!isAuthCandidateName(message.candidateName))
|
|
788
|
+
return;
|
|
789
|
+
const queued = this.deleteProcessorActive
|
|
790
|
+
|| this.importProcessorActive
|
|
791
|
+
|| !this.callbacks.isIdle()
|
|
792
|
+
|| this.pendingImports.length > 0
|
|
793
|
+
|| this.pendingDeletes.length > 0;
|
|
794
|
+
for (let index = this.pendingImports.length - 1; index >= 0; index -= 1) {
|
|
795
|
+
if (this.pendingImports[index]?.bundle.candidateName === message.candidateName) {
|
|
796
|
+
this.pendingImports.splice(index, 1);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
this.pendingDeletes.push({
|
|
800
|
+
candidateName: message.candidateName,
|
|
801
|
+
requestId: message.requestId,
|
|
802
|
+
sourceNodeId,
|
|
803
|
+
sourceLabel,
|
|
804
|
+
receivedAt: Date.now(),
|
|
805
|
+
fromPeer,
|
|
806
|
+
reason: message.reason ?? null,
|
|
807
|
+
});
|
|
808
|
+
this.recordEvent({
|
|
809
|
+
direction: 'local',
|
|
810
|
+
kind: 'delete.candidate',
|
|
811
|
+
stage: queued ? 'queued' : 'processing',
|
|
812
|
+
peer: fromPeer,
|
|
813
|
+
requestId: message.requestId,
|
|
814
|
+
candidateName: message.candidateName,
|
|
815
|
+
detail: message.reason ?? null,
|
|
816
|
+
});
|
|
817
|
+
this.notify({
|
|
818
|
+
kind: 'remote_delete_received',
|
|
819
|
+
candidateName: message.candidateName,
|
|
820
|
+
sourceNodeId,
|
|
821
|
+
sourceLabel: sourceLabel ?? fromPeer,
|
|
822
|
+
peer: fromPeer,
|
|
823
|
+
queued,
|
|
824
|
+
queueLength: this.pendingDeletes.length,
|
|
825
|
+
reason: message.reason ?? null,
|
|
826
|
+
});
|
|
827
|
+
void this.processPendingDeletes().then(() => this.processPendingImports()).catch((error) => {
|
|
828
|
+
this.recordError(`remote delete failed: ${formatError(error)}`);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
async processPendingDeletes() {
|
|
832
|
+
if (this.deleteProcessorActive)
|
|
833
|
+
return;
|
|
834
|
+
if (!this.callbacks.isIdle())
|
|
835
|
+
return;
|
|
836
|
+
this.deleteProcessorActive = true;
|
|
837
|
+
try {
|
|
838
|
+
while (this.pendingDeletes.length > 0) {
|
|
839
|
+
if (!this.callbacks.isIdle())
|
|
840
|
+
return;
|
|
841
|
+
const pending = this.pendingDeletes.shift();
|
|
842
|
+
await this.deleteRemoteCandidate(pending);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
this.deleteProcessorActive = false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
async deleteRemoteCandidate(pending) {
|
|
850
|
+
const source = pending.sourceLabel ?? pending.fromPeer;
|
|
851
|
+
if (!this.callbacks.deleteLocalCandidate) {
|
|
852
|
+
this.recordEvent({
|
|
853
|
+
direction: 'local',
|
|
854
|
+
kind: 'delete.candidate',
|
|
855
|
+
stage: 'skipped',
|
|
856
|
+
peer: pending.fromPeer,
|
|
857
|
+
requestId: pending.requestId,
|
|
858
|
+
candidateName: pending.candidateName,
|
|
859
|
+
detail: 'delete callback is not configured',
|
|
860
|
+
});
|
|
861
|
+
this.notify({
|
|
862
|
+
kind: 'remote_delete_skipped',
|
|
863
|
+
candidateName: pending.candidateName,
|
|
864
|
+
sourceNodeId: pending.sourceNodeId,
|
|
865
|
+
sourceLabel: source,
|
|
866
|
+
peer: pending.fromPeer,
|
|
867
|
+
reason: 'delete callback is not configured',
|
|
868
|
+
});
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const result = await this.callbacks.deleteLocalCandidate(pending.candidateName, {
|
|
872
|
+
nodeId: pending.sourceNodeId,
|
|
873
|
+
label: source,
|
|
874
|
+
reason: pending.reason,
|
|
875
|
+
});
|
|
876
|
+
if (!result.ok) {
|
|
877
|
+
const reason = result.reason ?? 'remote delete failed';
|
|
878
|
+
this.recordEvent({
|
|
879
|
+
direction: 'local',
|
|
880
|
+
kind: 'delete.candidate',
|
|
881
|
+
stage: 'failed',
|
|
882
|
+
peer: pending.fromPeer,
|
|
883
|
+
requestId: pending.requestId,
|
|
884
|
+
candidateName: pending.candidateName,
|
|
885
|
+
detail: reason,
|
|
886
|
+
});
|
|
887
|
+
this.notify({
|
|
888
|
+
kind: 'remote_delete_failed',
|
|
889
|
+
candidateName: pending.candidateName,
|
|
890
|
+
sourceNodeId: pending.sourceNodeId,
|
|
891
|
+
sourceLabel: source,
|
|
892
|
+
peer: pending.fromPeer,
|
|
893
|
+
reason,
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const clearedFailure = this.clearCandidateFailure(pending.candidateName);
|
|
898
|
+
this.recordEvent({
|
|
899
|
+
direction: 'local',
|
|
900
|
+
kind: 'delete.candidate',
|
|
901
|
+
stage: result.deleted ? 'deleted' : 'skipped',
|
|
902
|
+
peer: pending.fromPeer,
|
|
903
|
+
requestId: pending.requestId,
|
|
904
|
+
candidateName: pending.candidateName,
|
|
905
|
+
detail: result.reason ?? pending.reason,
|
|
906
|
+
});
|
|
907
|
+
if (clearedFailure) {
|
|
908
|
+
await this.writeState();
|
|
909
|
+
}
|
|
910
|
+
if (result.deleted) {
|
|
911
|
+
this.notify({
|
|
912
|
+
kind: 'remote_delete_deleted',
|
|
913
|
+
candidateName: pending.candidateName,
|
|
914
|
+
sourceNodeId: pending.sourceNodeId,
|
|
915
|
+
sourceLabel: source,
|
|
916
|
+
peer: pending.fromPeer,
|
|
917
|
+
reason: pending.reason,
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
this.notify({
|
|
922
|
+
kind: 'remote_delete_skipped',
|
|
923
|
+
candidateName: pending.candidateName,
|
|
924
|
+
sourceNodeId: pending.sourceNodeId,
|
|
925
|
+
sourceLabel: source,
|
|
926
|
+
peer: pending.fromPeer,
|
|
927
|
+
reason: result.reason ?? 'candidate was already absent',
|
|
928
|
+
});
|
|
929
|
+
}
|
|
735
930
|
async validateAndImport(bundle, sourceNodeId, sourceLabel, fromPeer, mode) {
|
|
736
931
|
const source = sourceLabel ?? fromPeer;
|
|
737
932
|
if (!isValidBundle(bundle)) {
|
|
@@ -1215,6 +1410,7 @@ function requestIdFromMessage(message) {
|
|
|
1215
1410
|
return message.requestId ?? null;
|
|
1216
1411
|
case 'pull.request':
|
|
1217
1412
|
case 'pull.response':
|
|
1413
|
+
case 'delete.candidate':
|
|
1218
1414
|
case 'test.ping':
|
|
1219
1415
|
case 'test.pong':
|
|
1220
1416
|
return message.requestId;
|
|
@@ -1232,6 +1428,7 @@ function candidateNameFromMessage(message) {
|
|
|
1232
1428
|
switch (message.kind) {
|
|
1233
1429
|
case 'push.bundle':
|
|
1234
1430
|
case 'pull.request':
|
|
1431
|
+
case 'delete.candidate':
|
|
1235
1432
|
return message.candidateName;
|
|
1236
1433
|
case 'pull.response':
|
|
1237
1434
|
return message.bundle?.candidateName ?? null;
|
package/dist/config.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface AppConfig {
|
|
|
44
44
|
statusPath: string;
|
|
45
45
|
logPath: string;
|
|
46
46
|
lockPath: string;
|
|
47
|
+
envPath: string | null;
|
|
47
48
|
/** When true, start Weixin (iLink) long-poll alongside Telegram. */
|
|
48
49
|
wxEnabled: boolean;
|
|
49
50
|
/** Allowed `from_user_id` values for inbound Weixin messages (empty = allow any). */
|
|
@@ -61,6 +62,7 @@ export interface AppConfig {
|
|
|
61
62
|
authSyncClusterId: string;
|
|
62
63
|
authSyncStatePath: string;
|
|
63
64
|
authSyncTempDir: string;
|
|
65
|
+
authAutoDeleteNeedsRepair: boolean;
|
|
64
66
|
}
|
|
65
67
|
export declare function loadConfig(): AppConfig;
|
|
66
68
|
export declare function selectDefaultRuntimeBotToken(configuredTokens: string[], legacyToken: string | null): string | null;
|
package/dist/config.js
CHANGED
|
@@ -81,6 +81,7 @@ export function loadConfig() {
|
|
|
81
81
|
statusPath: DEFAULT_STATUS_PATH,
|
|
82
82
|
logPath: DEFAULT_LOG_PATH,
|
|
83
83
|
lockPath: process.env.LOCK_PATH || DEFAULT_LOCK_PATH,
|
|
84
|
+
envPath: getLoadedEnvPath(),
|
|
84
85
|
wxEnabled: boolEnv('WX_ENABLED', false),
|
|
85
86
|
wxAllowedIlinkUserIds: parseCommaSeparatedIds(process.env.WX_ALLOWED_ILINK_USER_IDS),
|
|
86
87
|
weixinAccountsDir: process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts'),
|
|
@@ -95,6 +96,7 @@ export function loadConfig() {
|
|
|
95
96
|
authSyncClusterId: process.env.AUTH_SYNC_CLUSTER_ID?.trim() || 'default',
|
|
96
97
|
authSyncStatePath: process.env.AUTH_SYNC_STATE_PATH || DEFAULT_AUTH_SYNC_STATE_PATH,
|
|
97
98
|
authSyncTempDir: process.env.AUTH_SYNC_TEMP_DIR || DEFAULT_AUTH_SYNC_TEMP_DIR,
|
|
99
|
+
authAutoDeleteNeedsRepair: boolEnv('AUTH_AUTO_DELETE_NEEDS_REPAIR', false),
|
|
98
100
|
};
|
|
99
101
|
ensureAppDirs(config);
|
|
100
102
|
return config;
|
|
@@ -9,7 +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
|
+
authCandidateDeleted?: (runtimeId: string, candidateName: string, reason?: string | null) => Promise<void>;
|
|
13
13
|
recoverAuthCandidate?: (runtimeId: string, candidateName: string, options?: {
|
|
14
14
|
crossNode?: boolean;
|
|
15
15
|
}) => Promise<boolean>;
|
|
@@ -195,6 +195,7 @@ export declare class BridgeSessionCore {
|
|
|
195
195
|
private registerActiveTurn;
|
|
196
196
|
private createActiveTurnState;
|
|
197
197
|
getCurrentAuthLabel(): Promise<string | null>;
|
|
198
|
+
handleExternalCodexAuthCandidateDeleted(candidateName: string, reason?: string | null): Promise<void>;
|
|
198
199
|
validateExternalCodexAuthCandidate(candidateName: string, rawAuth: string, expectedAccountId: string): Promise<{
|
|
199
200
|
ok: boolean;
|
|
200
201
|
reason?: string | null;
|
|
@@ -327,6 +328,9 @@ export declare class BridgeSessionCore {
|
|
|
327
328
|
private handleAppsCommand;
|
|
328
329
|
private handleFeaturesCommand;
|
|
329
330
|
private handleConfigCommand;
|
|
331
|
+
private handleConfigToggleCallback;
|
|
332
|
+
private setAuthAutoDeleteNeedsRepair;
|
|
333
|
+
private formatConfigToggleUpdate;
|
|
330
334
|
private handleRequirementsCommand;
|
|
331
335
|
private handleProviderCommand;
|
|
332
336
|
private handleMcpCommand;
|