@foxden-app/foxclaw 0.5.19 → 0.5.21
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 +26 -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 +7 -1
- package/dist/controller/controller.js +277 -82
- package/dist/i18n.d.ts +28 -0
- package/dist/i18n.js +28 -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 +5 -1
- package/docs/user-manual.md +4 -1
- package/docs/zh/cross-node-auth-sync.md +5 -1
- package/docs/zh/user-manual.md +4 -1
- package/package.json +1 -1
package/dist/i18n.d.ts
CHANGED
|
@@ -187,6 +187,8 @@ declare const MESSAGES: {
|
|
|
187
187
|
readonly auth_repair_reverted: "Restored previous auth. The candidate still needs repair.";
|
|
188
188
|
readonly auth_candidate_deleted: "Deleted auth candidate: {value}";
|
|
189
189
|
readonly auth_candidate_deleted_short: "Auth deleted";
|
|
190
|
+
readonly auth_candidate_auto_deleted: "Auto-deleted unrecoverable auth candidate: {value}";
|
|
191
|
+
readonly auth_pool_summary: "Auth pool: total seen {total}, alive {alive}, invalid-deleted {deleted}.";
|
|
190
192
|
readonly auth_delete_current_restarted: "The deleted candidate was current, so Codex app-server was restarted without it.";
|
|
191
193
|
readonly auth_no_candidates: "No auth candidates found. Expected files like auth.json_personal in the auth dir.";
|
|
192
194
|
readonly auth_choice_expired: "This auth list is no longer active";
|
|
@@ -195,6 +197,8 @@ declare const MESSAGES: {
|
|
|
195
197
|
readonly auth_switching: "Switching Codex auth: {from} -> {to}...";
|
|
196
198
|
readonly auth_switch_done: "Codex auth switched: {from} -> {to}.";
|
|
197
199
|
readonly auth_switch_validation_failed: "Selected auth failed validation: {error}. Marked {value} for login repair.";
|
|
200
|
+
readonly auth_switch_validation_auto_deleted: "Selected auth failed validation: {error}. Auto-deleted {value}.";
|
|
201
|
+
readonly auth_switch_validation_auto_deleted_quiet: "Selected auth failed validation and was auto-deleted.";
|
|
198
202
|
readonly auth_switch_validation_reverted: "Restored the previous auth after the failed switch.";
|
|
199
203
|
readonly auth_recovered_newer_candidate: "Recovered a newer same-account credential for {value} from another Codex home before reload.";
|
|
200
204
|
readonly auth_refresh_all_confirm_short: "Review refresh all risk first.";
|
|
@@ -211,8 +215,10 @@ declare const MESSAGES: {
|
|
|
211
215
|
readonly auth_refresh_all_skipped: "Skipped non-ChatGPT/invalid candidates: {value}";
|
|
212
216
|
readonly auth_refresh_all_failed: "Failed: {value}";
|
|
213
217
|
readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching: {from} -> {to}...";
|
|
218
|
+
readonly auth_auto_switching_quiet: "Codex auth problem detected ({error}). Selecting another maintained candidate...";
|
|
214
219
|
readonly auth_auto_recovered_current: "Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.";
|
|
215
220
|
readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
|
|
221
|
+
readonly auth_auto_done_quiet: "Codex auth problem handled with another maintained candidate.";
|
|
216
222
|
readonly auth_auto_retrying: "Retrying the same request with the new auth...";
|
|
217
223
|
readonly auth_auto_retry_thread_missing: "Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.";
|
|
218
224
|
readonly auth_auto_no_candidate: "Codex auth problem detected ({error}), but no unused auth candidate is available.";
|
|
@@ -225,6 +231,7 @@ declare const MESSAGES: {
|
|
|
225
231
|
readonly auth_add_reverted: "Restored previous auth.";
|
|
226
232
|
readonly auth_add_missing_file: "Login completed, but the new auth file was not created: {value}";
|
|
227
233
|
readonly auth_sync_disabled: "Cross-node auth sync is disabled.";
|
|
234
|
+
readonly auth_sync_validation_busy: "Auth sync is validating refreshed credentials and temporarily restarting Codex app-server. Wait a moment, then send this message again.";
|
|
228
235
|
readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
|
|
229
236
|
readonly auth_sync_test_missing: "Missing replies: {value}";
|
|
230
237
|
readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
|
|
@@ -262,6 +269,8 @@ declare const MESSAGES: {
|
|
|
262
269
|
readonly button_auth_filter_all: "All";
|
|
263
270
|
readonly button_auth_filter_enabled: "Enabled";
|
|
264
271
|
readonly button_auth_filter_attention: "Attention";
|
|
272
|
+
readonly button_config_auth_auto_delete_on: "Auto-delete on";
|
|
273
|
+
readonly button_config_auth_auto_delete_off: "Auto-delete off";
|
|
265
274
|
readonly another_turn_running: "Another turn is already running. Use /interrupt, /takeover, /queue, or wait.";
|
|
266
275
|
readonly working: "Working...";
|
|
267
276
|
readonly usage_open: "Usage: /open <n>";
|
|
@@ -592,6 +601,11 @@ declare const MESSAGES: {
|
|
|
592
601
|
readonly features_empty: "No features found.";
|
|
593
602
|
readonly config_title: "Config summary";
|
|
594
603
|
readonly config_layers: "Config layers: {count}";
|
|
604
|
+
readonly config_foxclaw_title: "FoxClaw runtime config";
|
|
605
|
+
readonly config_auth_auto_delete_needs_repair: "Auto-delete unrecoverable auth candidates: {value}";
|
|
606
|
+
readonly config_auth_auto_delete_updated: "Auto-delete unrecoverable auth candidates set to: {value}";
|
|
607
|
+
readonly config_auth_auto_delete_usage: "Usage: /config auth_auto_delete <on|off>";
|
|
608
|
+
readonly config_env_update_failed: "Runtime setting changed, but updating {value} failed: {error}";
|
|
595
609
|
readonly requirements_title: "Config requirements";
|
|
596
610
|
readonly requirements_empty: "No config requirements are configured.";
|
|
597
611
|
readonly provider_title: "Model provider capabilities";
|
|
@@ -861,6 +875,8 @@ declare const MESSAGES: {
|
|
|
861
875
|
readonly auth_repair_reverted: "已恢复之前的 auth。该候选仍需要修复。";
|
|
862
876
|
readonly auth_candidate_deleted: "已删除 auth 候选:{value}";
|
|
863
877
|
readonly auth_candidate_deleted_short: "auth 已删除";
|
|
878
|
+
readonly auth_candidate_auto_deleted: "已自动剔除无法恢复的 auth 候选:{value}";
|
|
879
|
+
readonly auth_pool_summary: "auth 池:历史 {total},存活 {alive},因失效剔除 {deleted}。";
|
|
864
880
|
readonly auth_delete_current_restarted: "被删除的候选是当前 auth,已在删除后重启 Codex app-server。";
|
|
865
881
|
readonly auth_no_candidates: "没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。";
|
|
866
882
|
readonly auth_choice_expired: "这个 auth 列表已经不再有效";
|
|
@@ -869,6 +885,8 @@ declare const MESSAGES: {
|
|
|
869
885
|
readonly auth_switching: "正在切换 Codex auth:{from} -> {to}...";
|
|
870
886
|
readonly auth_switch_done: "Codex auth 已切换:{from} -> {to}。";
|
|
871
887
|
readonly auth_switch_validation_failed: "选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。";
|
|
888
|
+
readonly auth_switch_validation_auto_deleted: "选中的 auth 验证失败:{error}。已自动剔除 {value}。";
|
|
889
|
+
readonly auth_switch_validation_auto_deleted_quiet: "选中的 auth 验证失败,已自动剔除。";
|
|
872
890
|
readonly auth_switch_validation_reverted: "已在切换失败后恢复到之前的 auth。";
|
|
873
891
|
readonly auth_recovered_newer_candidate: "重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。";
|
|
874
892
|
readonly auth_refresh_all_confirm_short: "请先确认刷新全部风险。";
|
|
@@ -885,8 +903,10 @@ declare const MESSAGES: {
|
|
|
885
903
|
readonly auth_refresh_all_skipped: "已跳过非 ChatGPT/无效候选:{value}";
|
|
886
904
|
readonly auth_refresh_all_failed: "失败:{value}";
|
|
887
905
|
readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
|
|
906
|
+
readonly auth_auto_switching_quiet: "检测到 Codex auth 问题({error}),正在选择另一个维护中的候选...";
|
|
888
907
|
readonly auth_auto_recovered_current: "检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。";
|
|
889
908
|
readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
|
|
909
|
+
readonly auth_auto_done_quiet: "Codex auth 问题已用另一个维护中的候选处理。";
|
|
890
910
|
readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
|
|
891
911
|
readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
|
|
892
912
|
readonly auth_auto_no_candidate: "检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。";
|
|
@@ -899,6 +919,7 @@ declare const MESSAGES: {
|
|
|
899
919
|
readonly auth_add_reverted: "已恢复之前的 auth。";
|
|
900
920
|
readonly auth_add_missing_file: "登录已完成,但没有创建新的 auth 文件:{value}";
|
|
901
921
|
readonly auth_sync_disabled: "跨节点 auth 同步未启用。";
|
|
922
|
+
readonly auth_sync_validation_busy: "正在验证跨节点 auth,并临时重启 Codex app-server。请稍等片刻后再发送这条消息。";
|
|
902
923
|
readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
|
|
903
924
|
readonly auth_sync_test_missing: "未回应:{value}";
|
|
904
925
|
readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
|
|
@@ -936,6 +957,8 @@ declare const MESSAGES: {
|
|
|
936
957
|
readonly button_auth_filter_all: "全部";
|
|
937
958
|
readonly button_auth_filter_enabled: "已启用";
|
|
938
959
|
readonly button_auth_filter_attention: "需关注";
|
|
960
|
+
readonly button_config_auth_auto_delete_on: "开启自动剔除";
|
|
961
|
+
readonly button_config_auth_auto_delete_off: "关闭自动剔除";
|
|
939
962
|
readonly another_turn_running: "已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。";
|
|
940
963
|
readonly working: "处理中...";
|
|
941
964
|
readonly usage_open: "用法:/open <编号>";
|
|
@@ -1266,6 +1289,11 @@ declare const MESSAGES: {
|
|
|
1266
1289
|
readonly features_empty: "没有找到功能开关。";
|
|
1267
1290
|
readonly config_title: "配置摘要";
|
|
1268
1291
|
readonly config_layers: "配置层数:{count}";
|
|
1292
|
+
readonly config_foxclaw_title: "FoxClaw 运行时配置";
|
|
1293
|
+
readonly config_auth_auto_delete_needs_repair: "自动剔除无法恢复的 auth 候选:{value}";
|
|
1294
|
+
readonly config_auth_auto_delete_updated: "自动剔除无法恢复的 auth 候选已设置为:{value}";
|
|
1295
|
+
readonly config_auth_auto_delete_usage: "用法:/config auth_auto_delete <on|off>";
|
|
1296
|
+
readonly config_env_update_failed: "运行时设置已改变,但更新 {value} 失败:{error}";
|
|
1269
1297
|
readonly requirements_title: "配置要求";
|
|
1270
1298
|
readonly requirements_empty: "当前没有配置要求。";
|
|
1271
1299
|
readonly provider_title: "模型供应商能力";
|
package/dist/i18n.js
CHANGED
|
@@ -185,6 +185,8 @@ const MESSAGES = {
|
|
|
185
185
|
auth_repair_reverted: 'Restored previous auth. The candidate still needs repair.',
|
|
186
186
|
auth_candidate_deleted: 'Deleted auth candidate: {value}',
|
|
187
187
|
auth_candidate_deleted_short: 'Auth deleted',
|
|
188
|
+
auth_candidate_auto_deleted: 'Auto-deleted unrecoverable auth candidate: {value}',
|
|
189
|
+
auth_pool_summary: 'Auth pool: total seen {total}, alive {alive}, invalid-deleted {deleted}.',
|
|
188
190
|
auth_delete_current_restarted: 'The deleted candidate was current, so Codex app-server was restarted without it.',
|
|
189
191
|
auth_no_candidates: 'No auth candidates found. Expected files like auth.json_personal in the auth dir.',
|
|
190
192
|
auth_choice_expired: 'This auth list is no longer active',
|
|
@@ -193,6 +195,8 @@ const MESSAGES = {
|
|
|
193
195
|
auth_switching: 'Switching Codex auth: {from} -> {to}...',
|
|
194
196
|
auth_switch_done: 'Codex auth switched: {from} -> {to}.',
|
|
195
197
|
auth_switch_validation_failed: 'Selected auth failed validation: {error}. Marked {value} for login repair.',
|
|
198
|
+
auth_switch_validation_auto_deleted: 'Selected auth failed validation: {error}. Auto-deleted {value}.',
|
|
199
|
+
auth_switch_validation_auto_deleted_quiet: 'Selected auth failed validation and was auto-deleted.',
|
|
196
200
|
auth_switch_validation_reverted: 'Restored the previous auth after the failed switch.',
|
|
197
201
|
auth_recovered_newer_candidate: 'Recovered a newer same-account credential for {value} from another Codex home before reload.',
|
|
198
202
|
auth_refresh_all_confirm_short: 'Review refresh all risk first.',
|
|
@@ -209,8 +213,10 @@ const MESSAGES = {
|
|
|
209
213
|
auth_refresh_all_skipped: 'Skipped non-ChatGPT/invalid candidates: {value}',
|
|
210
214
|
auth_refresh_all_failed: 'Failed: {value}',
|
|
211
215
|
auth_auto_switching: 'Codex auth problem detected ({error}). Switching: {from} -> {to}...',
|
|
216
|
+
auth_auto_switching_quiet: 'Codex auth problem detected ({error}). Selecting another maintained candidate...',
|
|
212
217
|
auth_auto_recovered_current: 'Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.',
|
|
213
218
|
auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
|
|
219
|
+
auth_auto_done_quiet: 'Codex auth problem handled with another maintained candidate.',
|
|
214
220
|
auth_auto_retrying: 'Retrying the same request with the new auth...',
|
|
215
221
|
auth_auto_retry_thread_missing: 'Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.',
|
|
216
222
|
auth_auto_no_candidate: 'Codex auth problem detected ({error}), but no unused auth candidate is available.',
|
|
@@ -223,6 +229,7 @@ const MESSAGES = {
|
|
|
223
229
|
auth_add_reverted: 'Restored previous auth.',
|
|
224
230
|
auth_add_missing_file: 'Login completed, but the new auth file was not created: {value}',
|
|
225
231
|
auth_sync_disabled: 'Cross-node auth sync is disabled.',
|
|
232
|
+
auth_sync_validation_busy: 'Auth sync is validating refreshed credentials and temporarily restarting Codex app-server. Wait a moment, then send this message again.',
|
|
226
233
|
auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
|
|
227
234
|
auth_sync_test_missing: 'Missing replies: {value}',
|
|
228
235
|
auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
|
|
@@ -260,6 +267,8 @@ const MESSAGES = {
|
|
|
260
267
|
button_auth_filter_all: 'All',
|
|
261
268
|
button_auth_filter_enabled: 'Enabled',
|
|
262
269
|
button_auth_filter_attention: 'Attention',
|
|
270
|
+
button_config_auth_auto_delete_on: 'Auto-delete on',
|
|
271
|
+
button_config_auth_auto_delete_off: 'Auto-delete off',
|
|
263
272
|
another_turn_running: 'Another turn is already running. Use /interrupt, /takeover, /queue, or wait.',
|
|
264
273
|
working: 'Working...',
|
|
265
274
|
usage_open: 'Usage: /open <n>',
|
|
@@ -590,6 +599,11 @@ const MESSAGES = {
|
|
|
590
599
|
features_empty: 'No features found.',
|
|
591
600
|
config_title: 'Config summary',
|
|
592
601
|
config_layers: 'Config layers: {count}',
|
|
602
|
+
config_foxclaw_title: 'FoxClaw runtime config',
|
|
603
|
+
config_auth_auto_delete_needs_repair: 'Auto-delete unrecoverable auth candidates: {value}',
|
|
604
|
+
config_auth_auto_delete_updated: 'Auto-delete unrecoverable auth candidates set to: {value}',
|
|
605
|
+
config_auth_auto_delete_usage: 'Usage: /config auth_auto_delete <on|off>',
|
|
606
|
+
config_env_update_failed: 'Runtime setting changed, but updating {value} failed: {error}',
|
|
593
607
|
requirements_title: 'Config requirements',
|
|
594
608
|
requirements_empty: 'No config requirements are configured.',
|
|
595
609
|
provider_title: 'Model provider capabilities',
|
|
@@ -859,6 +873,8 @@ const MESSAGES = {
|
|
|
859
873
|
auth_repair_reverted: '已恢复之前的 auth。该候选仍需要修复。',
|
|
860
874
|
auth_candidate_deleted: '已删除 auth 候选:{value}',
|
|
861
875
|
auth_candidate_deleted_short: 'auth 已删除',
|
|
876
|
+
auth_candidate_auto_deleted: '已自动剔除无法恢复的 auth 候选:{value}',
|
|
877
|
+
auth_pool_summary: 'auth 池:历史 {total},存活 {alive},因失效剔除 {deleted}。',
|
|
862
878
|
auth_delete_current_restarted: '被删除的候选是当前 auth,已在删除后重启 Codex app-server。',
|
|
863
879
|
auth_no_candidates: '没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。',
|
|
864
880
|
auth_choice_expired: '这个 auth 列表已经不再有效',
|
|
@@ -867,6 +883,8 @@ const MESSAGES = {
|
|
|
867
883
|
auth_switching: '正在切换 Codex auth:{from} -> {to}...',
|
|
868
884
|
auth_switch_done: 'Codex auth 已切换:{from} -> {to}。',
|
|
869
885
|
auth_switch_validation_failed: '选中的 auth 验证失败:{error}。已将 {value} 标记为需要登录修复。',
|
|
886
|
+
auth_switch_validation_auto_deleted: '选中的 auth 验证失败:{error}。已自动剔除 {value}。',
|
|
887
|
+
auth_switch_validation_auto_deleted_quiet: '选中的 auth 验证失败,已自动剔除。',
|
|
870
888
|
auth_switch_validation_reverted: '已在切换失败后恢复到之前的 auth。',
|
|
871
889
|
auth_recovered_newer_candidate: '重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。',
|
|
872
890
|
auth_refresh_all_confirm_short: '请先确认刷新全部风险。',
|
|
@@ -883,8 +901,10 @@ const MESSAGES = {
|
|
|
883
901
|
auth_refresh_all_skipped: '已跳过非 ChatGPT/无效候选:{value}',
|
|
884
902
|
auth_refresh_all_failed: '失败:{value}',
|
|
885
903
|
auth_auto_switching: '检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...',
|
|
904
|
+
auth_auto_switching_quiet: '检测到 Codex auth 问题({error}),正在选择另一个维护中的候选...',
|
|
886
905
|
auth_auto_recovered_current: '检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。',
|
|
887
906
|
auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
|
|
907
|
+
auth_auto_done_quiet: 'Codex auth 问题已用另一个维护中的候选处理。',
|
|
888
908
|
auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
|
|
889
909
|
auth_auto_retry_thread_missing: 'Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。',
|
|
890
910
|
auth_auto_no_candidate: '检测到 Codex auth 问题({error}),但没有可用的未失败候选 auth。',
|
|
@@ -897,6 +917,7 @@ const MESSAGES = {
|
|
|
897
917
|
auth_add_reverted: '已恢复之前的 auth。',
|
|
898
918
|
auth_add_missing_file: '登录已完成,但没有创建新的 auth 文件:{value}',
|
|
899
919
|
auth_sync_disabled: '跨节点 auth 同步未启用。',
|
|
920
|
+
auth_sync_validation_busy: '正在验证跨节点 auth,并临时重启 Codex app-server。请稍等片刻后再发送这条消息。',
|
|
900
921
|
auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
|
|
901
922
|
auth_sync_test_missing: '未回应:{value}',
|
|
902
923
|
auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
|
|
@@ -934,6 +955,8 @@ const MESSAGES = {
|
|
|
934
955
|
button_auth_filter_all: '全部',
|
|
935
956
|
button_auth_filter_enabled: '已启用',
|
|
936
957
|
button_auth_filter_attention: '需关注',
|
|
958
|
+
button_config_auth_auto_delete_on: '开启自动剔除',
|
|
959
|
+
button_config_auth_auto_delete_off: '关闭自动剔除',
|
|
937
960
|
another_turn_running: '已经有一个回复在进行中。请先等待,或使用 /interrupt、/takeover、/queue。',
|
|
938
961
|
working: '处理中...',
|
|
939
962
|
usage_open: '用法:/open <编号>',
|
|
@@ -1264,6 +1287,11 @@ const MESSAGES = {
|
|
|
1264
1287
|
features_empty: '没有找到功能开关。',
|
|
1265
1288
|
config_title: '配置摘要',
|
|
1266
1289
|
config_layers: '配置层数:{count}',
|
|
1290
|
+
config_foxclaw_title: 'FoxClaw 运行时配置',
|
|
1291
|
+
config_auth_auto_delete_needs_repair: '自动剔除无法恢复的 auth 候选:{value}',
|
|
1292
|
+
config_auth_auto_delete_updated: '自动剔除无法恢复的 auth 候选已设置为:{value}',
|
|
1293
|
+
config_auth_auto_delete_usage: '用法:/config auth_auto_delete <on|off>',
|
|
1294
|
+
config_env_update_failed: '运行时设置已改变,但更新 {value} 失败:{error}',
|
|
1267
1295
|
requirements_title: '配置要求',
|
|
1268
1296
|
requirements_empty: '当前没有配置要求。',
|
|
1269
1297
|
provider_title: '模型供应商能力',
|
package/dist/main.js
CHANGED
|
@@ -253,7 +253,9 @@ async function runServeCli() {
|
|
|
253
253
|
label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
|
|
254
254
|
authDir: runtime.authDir,
|
|
255
255
|
validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
|
|
256
|
-
notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator
|
|
256
|
+
notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator, {
|
|
257
|
+
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
258
|
+
}),
|
|
257
259
|
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
258
260
|
onSynced: async (event) => {
|
|
259
261
|
await authSync?.publishCandidate(event.record.candidateName);
|
|
@@ -324,7 +326,10 @@ async function runServeCli() {
|
|
|
324
326
|
canSelfUpdate: () => authSyncLocalIdle()
|
|
325
327
|
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
326
328
|
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
327
|
-
authCandidateDeleted: (_runtimeId, candidateName
|
|
329
|
+
authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
|
|
330
|
+
await mirror.deleteCandidate(candidateName);
|
|
331
|
+
await authSync?.publishCandidateDeletion(candidateName, reason);
|
|
332
|
+
},
|
|
328
333
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
329
334
|
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
330
335
|
if (local)
|
|
@@ -424,8 +429,22 @@ async function runServeCli() {
|
|
|
424
429
|
return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
425
430
|
},
|
|
426
431
|
importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
|
|
432
|
+
deleteLocalCandidate: async (candidateName, source) => {
|
|
433
|
+
if (!authSyncLocalIdle()) {
|
|
434
|
+
return { ok: false, deleted: false, reason: 'runtime is not idle' };
|
|
435
|
+
}
|
|
436
|
+
await mirror.deleteCandidate(candidateName);
|
|
437
|
+
store.deleteCodexAuthCandidate(candidateName);
|
|
438
|
+
await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
|
|
439
|
+
if (activeWeixinCore) {
|
|
440
|
+
await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
441
|
+
}
|
|
442
|
+
return { ok: true, deleted: true, reason: source.reason ?? null };
|
|
443
|
+
},
|
|
427
444
|
isIdle: authSyncLocalIdle,
|
|
428
|
-
notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator
|
|
445
|
+
notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
|
|
446
|
+
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
447
|
+
}),
|
|
429
448
|
});
|
|
430
449
|
await authSync.initialize();
|
|
431
450
|
activeAuthSync = authSync;
|
|
@@ -494,7 +513,10 @@ async function runServeCli() {
|
|
|
494
513
|
canSelfUpdate: () => singleAuthSyncLocalIdle()
|
|
495
514
|
&& (singleAuthSync ? singleAuthSync.isIdle() : singleLocalAuthRefreshLease.isIdle()),
|
|
496
515
|
authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
|
|
497
|
-
authCandidateDeleted: (_runtimeId, candidateName
|
|
516
|
+
authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
|
|
517
|
+
await singleMirror?.deleteCandidate(candidateName);
|
|
518
|
+
await singleAuthSync?.publishCandidateDeletion(candidateName, reason);
|
|
519
|
+
},
|
|
498
520
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
499
521
|
const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
|
|
500
522
|
if (local)
|
|
@@ -570,8 +592,19 @@ async function runServeCli() {
|
|
|
570
592
|
return core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
571
593
|
},
|
|
572
594
|
importCandidate: (candidateName, raw, source) => singleMirror.importExternalCandidate(candidateName, raw, source),
|
|
595
|
+
deleteLocalCandidate: async (candidateName, source) => {
|
|
596
|
+
if (!singleAuthSyncLocalIdle()) {
|
|
597
|
+
return { ok: false, deleted: false, reason: 'runtime is not idle' };
|
|
598
|
+
}
|
|
599
|
+
await singleMirror.deleteCandidate(candidateName);
|
|
600
|
+
store.deleteCodexAuthCandidate(candidateName);
|
|
601
|
+
await core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
602
|
+
return { ok: true, deleted: true, reason: source.reason ?? null };
|
|
603
|
+
},
|
|
573
604
|
isIdle: singleAuthSyncLocalIdle,
|
|
574
|
-
notify: createAuthSyncNotifier(store, bot, authNotificationAggregator
|
|
605
|
+
notify: createAuthSyncNotifier(store, bot, authNotificationAggregator, {
|
|
606
|
+
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
607
|
+
}),
|
|
575
608
|
});
|
|
576
609
|
await singleAuthSync.initialize();
|
|
577
610
|
activeAuthSync = singleAuthSync;
|
|
@@ -641,8 +674,10 @@ async function runServeCli() {
|
|
|
641
674
|
throw error;
|
|
642
675
|
}
|
|
643
676
|
}
|
|
644
|
-
function createAuthMirrorNotifier(store, botId, bot, aggregator) {
|
|
677
|
+
function createAuthMirrorNotifier(store, botId, bot, aggregator, options = {}) {
|
|
645
678
|
return async (event) => {
|
|
679
|
+
if (options.quietAuthPoolMode?.())
|
|
680
|
+
return;
|
|
646
681
|
const privateScope = store.getTelegramPrivateScope(botId);
|
|
647
682
|
if (!privateScope)
|
|
648
683
|
return;
|
|
@@ -654,7 +689,27 @@ function createAuthMirrorNotifier(store, botId, bot, aggregator) {
|
|
|
654
689
|
}, event);
|
|
655
690
|
};
|
|
656
691
|
}
|
|
657
|
-
function createAuthSyncNotifier(store, bot, aggregator) {
|
|
692
|
+
function createAuthSyncNotifier(store, bot, aggregator, options = {}) {
|
|
693
|
+
const poolSummaryQueues = new Map();
|
|
694
|
+
const enqueuePoolSummary = (destination) => {
|
|
695
|
+
const existing = poolSummaryQueues.get(destination.key);
|
|
696
|
+
if (existing) {
|
|
697
|
+
clearTimeout(existing.timer);
|
|
698
|
+
}
|
|
699
|
+
const timer = setTimeout(() => {
|
|
700
|
+
const queued = poolSummaryQueues.get(destination.key);
|
|
701
|
+
if (!queued)
|
|
702
|
+
return;
|
|
703
|
+
poolSummaryQueues.delete(destination.key);
|
|
704
|
+
void queued.sendMessage(formatAuthPoolSummary(queued.locale, store.getCodexAuthPoolStats())).catch(() => undefined);
|
|
705
|
+
}, 2_000);
|
|
706
|
+
timer.unref();
|
|
707
|
+
poolSummaryQueues.set(destination.key, {
|
|
708
|
+
locale: destination.locale,
|
|
709
|
+
sendMessage: destination.sendMessage,
|
|
710
|
+
timer,
|
|
711
|
+
});
|
|
712
|
+
};
|
|
658
713
|
return async (event) => {
|
|
659
714
|
if (!bot.identity)
|
|
660
715
|
return;
|
|
@@ -667,6 +722,12 @@ function createAuthSyncNotifier(store, bot, aggregator) {
|
|
|
667
722
|
locale,
|
|
668
723
|
sendMessage: (text) => bot.sendMessage(privateScope.chatId, text),
|
|
669
724
|
};
|
|
725
|
+
if (options.quietAuthPoolMode?.() && isQuietAuthPoolNotification(event)) {
|
|
726
|
+
if (shouldSendQuietAuthPoolSummary(event)) {
|
|
727
|
+
enqueuePoolSummary(destination);
|
|
728
|
+
}
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
670
731
|
if (aggregator.enqueueAuthSync(destination, event)) {
|
|
671
732
|
return;
|
|
672
733
|
}
|
|
@@ -676,6 +737,45 @@ function createAuthSyncNotifier(store, bot, aggregator) {
|
|
|
676
737
|
function authNotificationDestinationKey(botId, chatId) {
|
|
677
738
|
return `${botId}:${chatId}`;
|
|
678
739
|
}
|
|
740
|
+
function formatAuthPoolSummary(locale, stats) {
|
|
741
|
+
return locale === 'zh'
|
|
742
|
+
? `auth 池:历史 ${stats.totalSeen},存活 ${stats.alive},因失效剔除 ${stats.deletedInvalid}。`
|
|
743
|
+
: `Auth pool: total seen ${stats.totalSeen}, alive ${stats.alive}, invalid-deleted ${stats.deletedInvalid}.`;
|
|
744
|
+
}
|
|
745
|
+
function isQuietAuthPoolNotification(event) {
|
|
746
|
+
switch (event.kind) {
|
|
747
|
+
case 'candidate_publish_started':
|
|
748
|
+
case 'candidate_publish_completed':
|
|
749
|
+
case 'push_all_started':
|
|
750
|
+
case 'push_all_completed':
|
|
751
|
+
case 'remote_bundle_received':
|
|
752
|
+
case 'remote_import_imported':
|
|
753
|
+
case 'remote_import_skipped':
|
|
754
|
+
case 'candidate_delete_sent':
|
|
755
|
+
case 'remote_delete_received':
|
|
756
|
+
case 'remote_delete_deleted':
|
|
757
|
+
case 'remote_delete_skipped':
|
|
758
|
+
case 'recovery_started':
|
|
759
|
+
case 'recovery_peer_empty':
|
|
760
|
+
case 'recovery_peer_bundle_received':
|
|
761
|
+
case 'recovery_failed':
|
|
762
|
+
case 'pull_request_received':
|
|
763
|
+
case 'pull_response_sent':
|
|
764
|
+
return true;
|
|
765
|
+
default:
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function shouldSendQuietAuthPoolSummary(event) {
|
|
770
|
+
switch (event.kind) {
|
|
771
|
+
case 'candidate_delete_sent':
|
|
772
|
+
case 'remote_delete_deleted':
|
|
773
|
+
case 'remote_delete_skipped':
|
|
774
|
+
return true;
|
|
775
|
+
default:
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
679
779
|
function formatAuthSyncNotification(locale, event) {
|
|
680
780
|
const peers = 'peers' in event ? formatPeerList(event.peers, locale) : '';
|
|
681
781
|
if (locale === 'zh') {
|
|
@@ -704,6 +804,23 @@ function formatAuthSyncNotification(locale, event) {
|
|
|
704
804
|
return `${event.mode === 'pull' ? '跨节点拉取未改动本机文件' : '收到跨节点 auth 但未写盘'}:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}`;
|
|
705
805
|
case 'remote_import_failed':
|
|
706
806
|
return `${event.mode === 'pull' ? '跨节点拉取导入失败' : '跨节点 auth 导入失败'}:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}\n需要注意:如果其他候选也无法恢复,请人工介入重新登录或刷新这个 auth。`;
|
|
807
|
+
case 'candidate_delete_sent':
|
|
808
|
+
return `跨节点 auth 删除已发出:${event.candidateName}\nPeer:${peers}${event.reason ? `\n原因:${event.reason}` : ''}`;
|
|
809
|
+
case 'candidate_delete_failed':
|
|
810
|
+
return `跨节点 auth 删除发送失败:${event.candidateName}\nPeer:${peers}\n原因:${event.reason}`;
|
|
811
|
+
case 'remote_delete_received':
|
|
812
|
+
return [
|
|
813
|
+
`收到跨节点 auth 删除:${event.candidateName}`,
|
|
814
|
+
`来源:${formatSource(event.sourceLabel, event.sourceNodeId)},peer ${event.peer}`,
|
|
815
|
+
`处理:${event.queued ? `本机忙,已排队等待空闲后删除;当前待删除 ${event.queueLength}` : '本机空闲,正在删除同名候选'}`,
|
|
816
|
+
event.reason ? `原因:${event.reason}` : null,
|
|
817
|
+
].filter(Boolean).join('\n');
|
|
818
|
+
case 'remote_delete_deleted':
|
|
819
|
+
return `已执行跨节点 auth 删除:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}${event.reason ? `\n原因:${event.reason}` : ''}`;
|
|
820
|
+
case 'remote_delete_skipped':
|
|
821
|
+
return `跨节点 auth 删除未改动本机文件:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}`;
|
|
822
|
+
case 'remote_delete_failed':
|
|
823
|
+
return `跨节点 auth 删除失败:${event.candidateName}\n来源:${formatSource(event.sourceLabel, event.sourceNodeId)}\n原因:${event.reason}`;
|
|
707
824
|
case 'recovery_started':
|
|
708
825
|
return `auth 恢复开始:${event.candidateName}\nRequest:${event.requestId}\n处理:同节点没有可用较新副本,正在向跨节点 peer 查询:${peers},最长等待 ${event.timeoutMs}ms`;
|
|
709
826
|
case 'recovery_peer_empty':
|
|
@@ -753,6 +870,23 @@ function formatAuthSyncNotification(locale, event) {
|
|
|
753
870
|
return `${event.mode === 'pull' ? 'Cross-node pull did not change local files' : 'Received cross-node auth but did not write it'}: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}`;
|
|
754
871
|
case 'remote_import_failed':
|
|
755
872
|
return `${event.mode === 'pull' ? 'Cross-node pull import failed' : 'Cross-node auth import failed'}: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}\nAttention: if no other candidate can recover this account, run device login or refresh this auth manually.`;
|
|
873
|
+
case 'candidate_delete_sent':
|
|
874
|
+
return `Cross-node auth delete sent: ${event.candidateName}\nPeers: ${peers}${event.reason ? `\nReason: ${event.reason}` : ''}`;
|
|
875
|
+
case 'candidate_delete_failed':
|
|
876
|
+
return `Cross-node auth delete send failed: ${event.candidateName}\nPeers: ${peers}\nReason: ${event.reason}`;
|
|
877
|
+
case 'remote_delete_received':
|
|
878
|
+
return [
|
|
879
|
+
`Received cross-node auth delete: ${event.candidateName}`,
|
|
880
|
+
`Source: ${formatSource(event.sourceLabel, event.sourceNodeId)}, peer ${event.peer}`,
|
|
881
|
+
`Action: ${event.queued ? `queued until this node is idle; pending deletes ${event.queueLength}` : 'deleting the matching local candidate'}`,
|
|
882
|
+
event.reason ? `Reason: ${event.reason}` : null,
|
|
883
|
+
].filter(Boolean).join('\n');
|
|
884
|
+
case 'remote_delete_deleted':
|
|
885
|
+
return `Applied cross-node auth delete: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}${event.reason ? `\nReason: ${event.reason}` : ''}`;
|
|
886
|
+
case 'remote_delete_skipped':
|
|
887
|
+
return `Cross-node auth delete did not change local files: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}`;
|
|
888
|
+
case 'remote_delete_failed':
|
|
889
|
+
return `Cross-node auth delete failed: ${event.candidateName}\nSource: ${formatSource(event.sourceLabel, event.sourceNodeId)}\nReason: ${event.reason}`;
|
|
756
890
|
case 'recovery_started':
|
|
757
891
|
return `Auth recovery started: ${event.candidateName}\nRequest: ${event.requestId}\nAction: no newer same-node copy was available; querying cross-node peers: ${peers}; timeout ${event.timeoutMs}ms`;
|
|
758
892
|
case 'recovery_peer_empty':
|
package/dist/store/database.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export interface CodexAuthQuotaSnapshotRecord {
|
|
|
38
38
|
secondaryRemainingPercent: number | null;
|
|
39
39
|
updatedAt: number;
|
|
40
40
|
}
|
|
41
|
+
export interface CodexAuthPoolStats {
|
|
42
|
+
totalSeen: number;
|
|
43
|
+
alive: number;
|
|
44
|
+
deletedInvalid: number;
|
|
45
|
+
}
|
|
41
46
|
export type CodexAuthCandidateState = 'active' | 'needs_repair';
|
|
42
47
|
export declare class BridgeStore {
|
|
43
48
|
private db;
|
|
@@ -125,6 +130,10 @@ export declare class BridgeStore {
|
|
|
125
130
|
setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
|
|
126
131
|
setCodexAuthCandidateState(name: string, state: CodexAuthCandidateState, runtimeId?: string): void;
|
|
127
132
|
deleteCodexAuthCandidate(name: string): void;
|
|
133
|
+
recordCodexAuthPoolInventory(names: string[]): void;
|
|
134
|
+
recordCodexAuthCandidateInvalidDelete(name: string, reason?: string | null): void;
|
|
135
|
+
recordCodexAuthCandidateRemoved(name: string, reason?: string | null): void;
|
|
136
|
+
getCodexAuthPoolStats(): CodexAuthPoolStats;
|
|
128
137
|
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
|
|
129
138
|
listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
|
|
130
139
|
private ensureColumn;
|
package/dist/store/database.js
CHANGED
|
@@ -197,6 +197,14 @@ export class BridgeStore {
|
|
|
197
197
|
);
|
|
198
198
|
CREATE INDEX IF NOT EXISTS codex_auth_quota_snapshots_account_idx
|
|
199
199
|
ON codex_auth_quota_snapshots(account_id);
|
|
200
|
+
CREATE TABLE IF NOT EXISTS codex_auth_pool_history (
|
|
201
|
+
name TEXT PRIMARY KEY,
|
|
202
|
+
first_seen_at INTEGER NOT NULL,
|
|
203
|
+
last_seen_at INTEGER NOT NULL,
|
|
204
|
+
deleted_at INTEGER,
|
|
205
|
+
delete_reason TEXT,
|
|
206
|
+
invalid_delete_count INTEGER NOT NULL DEFAULT 0
|
|
207
|
+
);
|
|
200
208
|
`);
|
|
201
209
|
this.ensureColumn('thread_cache', 'name', 'TEXT');
|
|
202
210
|
this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
|
|
@@ -217,6 +225,7 @@ export class BridgeStore {
|
|
|
217
225
|
this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
|
|
218
226
|
this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
|
|
219
227
|
this.ensureColumn('codex_auth_quota_snapshots', 'quota_identity_id', "TEXT NOT NULL DEFAULT ''");
|
|
228
|
+
this.ensureColumn('codex_auth_pool_history', 'invalid_delete_count', 'INTEGER NOT NULL DEFAULT 0');
|
|
220
229
|
this.db.prepare(`
|
|
221
230
|
UPDATE codex_auth_quota_snapshots
|
|
222
231
|
SET quota_identity_id = account_id
|
|
@@ -896,6 +905,84 @@ export class BridgeStore {
|
|
|
896
905
|
this.db.prepare('DELETE FROM codex_auth_candidate_runtime WHERE name = ?').run(name);
|
|
897
906
|
this.db.prepare('DELETE FROM codex_auth_quota_snapshots WHERE candidate_name = ?').run(name);
|
|
898
907
|
}
|
|
908
|
+
recordCodexAuthPoolInventory(names) {
|
|
909
|
+
if (names.length === 0) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const now = Date.now();
|
|
913
|
+
const upsert = this.db.prepare(`
|
|
914
|
+
INSERT INTO codex_auth_pool_history (name, first_seen_at, last_seen_at, deleted_at, delete_reason, invalid_delete_count)
|
|
915
|
+
VALUES (?, ?, ?, NULL, NULL, 0)
|
|
916
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
917
|
+
last_seen_at = excluded.last_seen_at,
|
|
918
|
+
deleted_at = NULL,
|
|
919
|
+
delete_reason = NULL
|
|
920
|
+
`);
|
|
921
|
+
const seen = new Set(names);
|
|
922
|
+
this.db.exec('BEGIN');
|
|
923
|
+
try {
|
|
924
|
+
for (const name of seen) {
|
|
925
|
+
upsert.run(name, now, now);
|
|
926
|
+
}
|
|
927
|
+
this.db.exec('COMMIT');
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
this.db.exec('ROLLBACK');
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
recordCodexAuthCandidateInvalidDelete(name, reason = null) {
|
|
935
|
+
const now = Date.now();
|
|
936
|
+
this.db.prepare(`
|
|
937
|
+
INSERT INTO codex_auth_pool_history (
|
|
938
|
+
name,
|
|
939
|
+
first_seen_at,
|
|
940
|
+
last_seen_at,
|
|
941
|
+
deleted_at,
|
|
942
|
+
delete_reason,
|
|
943
|
+
invalid_delete_count
|
|
944
|
+
)
|
|
945
|
+
VALUES (?, ?, ?, ?, ?, 1)
|
|
946
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
947
|
+
last_seen_at = excluded.last_seen_at,
|
|
948
|
+
deleted_at = excluded.deleted_at,
|
|
949
|
+
delete_reason = excluded.delete_reason,
|
|
950
|
+
invalid_delete_count = codex_auth_pool_history.invalid_delete_count
|
|
951
|
+
+ CASE WHEN codex_auth_pool_history.deleted_at IS NULL THEN 1 ELSE 0 END
|
|
952
|
+
`).run(name, now, now, now, reason);
|
|
953
|
+
}
|
|
954
|
+
recordCodexAuthCandidateRemoved(name, reason = null) {
|
|
955
|
+
const now = Date.now();
|
|
956
|
+
this.db.prepare(`
|
|
957
|
+
INSERT INTO codex_auth_pool_history (
|
|
958
|
+
name,
|
|
959
|
+
first_seen_at,
|
|
960
|
+
last_seen_at,
|
|
961
|
+
deleted_at,
|
|
962
|
+
delete_reason,
|
|
963
|
+
invalid_delete_count
|
|
964
|
+
)
|
|
965
|
+
VALUES (?, ?, ?, ?, ?, 0)
|
|
966
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
967
|
+
last_seen_at = excluded.last_seen_at,
|
|
968
|
+
deleted_at = excluded.deleted_at,
|
|
969
|
+
delete_reason = excluded.delete_reason
|
|
970
|
+
`).run(name, now, now, now, reason);
|
|
971
|
+
}
|
|
972
|
+
getCodexAuthPoolStats() {
|
|
973
|
+
const row = this.db.prepare(`
|
|
974
|
+
SELECT
|
|
975
|
+
COUNT(*) AS total_seen,
|
|
976
|
+
COALESCE(SUM(CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END), 0) AS alive,
|
|
977
|
+
COALESCE(SUM(invalid_delete_count), 0) AS deleted_invalid
|
|
978
|
+
FROM codex_auth_pool_history
|
|
979
|
+
`).get();
|
|
980
|
+
return {
|
|
981
|
+
totalSeen: Number(row?.total_seen ?? 0),
|
|
982
|
+
alive: Number(row?.alive ?? 0),
|
|
983
|
+
deletedInvalid: Number(row?.deleted_invalid ?? 0),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
899
986
|
setCodexAuthQuotaSnapshot(runtimeId, candidateName, accountId, quotaIdentityId, snapshot) {
|
|
900
987
|
this.db.prepare(`
|
|
901
988
|
INSERT INTO codex_auth_quota_snapshots (
|
|
@@ -92,6 +92,7 @@ AUTH_SYNC_KEY=<shared key with at least 32 bytes>
|
|
|
92
92
|
AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
|
|
93
93
|
AUTH_SYNC_NODE_ID=workstation-a
|
|
94
94
|
AUTH_SYNC_PEERS=@foxclaw_node_b_bot
|
|
95
|
+
AUTH_AUTO_DELETE_NEEDS_REPAIR=false
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
Node B:
|
|
@@ -103,6 +104,7 @@ AUTH_SYNC_KEY=<shared key with at least 32 bytes>
|
|
|
103
104
|
AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
|
|
104
105
|
AUTH_SYNC_NODE_ID=workstation-b
|
|
105
106
|
AUTH_SYNC_PEERS=@foxclaw_node_a_bot
|
|
107
|
+
AUTH_AUTO_DELETE_NEEDS_REPAIR=false
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
For more peers, separate bot usernames with commas:
|
|
@@ -166,7 +168,9 @@ Confirm that pending imports were processed, or that the candidate exists or has
|
|
|
166
168
|
|
|
167
169
|
Note: `/auth sync push all` saying “sent” only means this node successfully handed encrypted packages to Telegram. It does not prove the peer wrote files. The peer imports only when it is globally idle, usage validation succeeds, same-name candidates belong to the same account id and compatible ChatGPT user/email identity, and the remote `last_refresh` is newer than the local copy. If the local file is already equal or newer, it will not change and `Last import` may remain empty.
|
|
168
170
|
|
|
169
|
-
When cross-node sync is enabled, the contact bot private chat receives node-level notifications: local auth updates and the peers being contacted, received remote bundles and whether they were queued or immediately validated, import success/skip/failure reasons, recovery peer queries and peer replies, and a manual-intervention notice when every peer lacks an importable copy. Refresh/send/import bursts are grouped into short summaries so one candidate update does not produce separate start, receive, mirror-write, and completion messages. Recovery and manual-intervention notices remain explicit. Notifications never include auth contents, tokens, or encrypted bundle payloads.
|
|
171
|
+
When cross-node sync is enabled, the contact bot private chat receives node-level notifications: local auth updates and the peers being contacted, received remote bundles and whether they were queued or immediately validated, import success/skip/failure reasons, recovery peer queries and peer replies, and a manual-intervention notice when every peer lacks an importable copy. Refresh/send/import bursts are grouped into short summaries so one candidate update does not produce separate start, receive, mirror-write, and completion messages. Recovery and manual-intervention notices remain explicit. Remote import validation temporarily restarts the local Codex app-server; during that restart window FoxClaw treats the runtime as non-idle and asks ordinary messages to be resent shortly instead of running them against a restarting bridge. Notifications never include auth contents, tokens, or encrypted bundle payloads.
|
|
172
|
+
|
|
173
|
+
For a resource-rich pool where individual candidate maintenance is not important, enable `AUTH_AUTO_DELETE_NEEDS_REPAIR=true` or toggle it with `/config auth_auto_delete on`. Candidates that cannot be recovered and would otherwise be marked for repair are deleted locally, a delete tombstone is sent to peers, and candidate-level sync/delete chatter is collapsed into an auth-pool summary.
|
|
170
174
|
|
|
171
175
|
Starting in 0.5.2, `/auth sync status` separates sync-system `Last error` from per-auth `Candidate failures`. For example, a remote candidate that returns `token_invalidated` or has an expired access token is recorded under that candidate name only; current `auth.json` health is still determined by validating the current auth usage. `local candidate is already newer or equal` is a normal skip, not an error.
|
|
172
176
|
|