@foxden-app/foxclaw 0.5.10 → 0.5.11
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 +12 -0
- package/README.md +1 -0
- package/README_EN.md +1 -0
- package/dist/auth/mirror.d.ts +6 -0
- package/dist/auth/mirror.js +49 -0
- package/dist/controller/controller.d.ts +7 -0
- package/dist/controller/controller.js +54 -8
- package/dist/i18n.d.ts +10 -4
- package/dist/i18n.js +10 -4
- package/dist/main.js +20 -0
- package/docs/release.md +102 -0
- package/docs/zh/release.md +102 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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.11 - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- `/auth` 面板的 `Bot runtime` 现在显示 Telegram bot id,例如 `@WuguiAI2_Bot (bot8949529424)`,便于和 `~/.foxclaw/codex/telegram/<botid>/home` 对应。
|
|
9
|
+
- `/auth` 面板新增“安全同步”按钮,并支持 `/auth sync safe`,可在全局空闲时安全打平本机多 bot auth,并把已校验的候选推送到跨节点 peer。
|
|
10
|
+
- 本机 auth mirror 新增全量安全同步路径:只传播通过既有在线校验的刷新候选,同时补齐 canonical 中已知、同账号且更新的 runtime 副本。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- The `/auth` panel now shows the Telegram bot id in `Bot runtime`, for example `@WuguiAI2_Bot (bot8949529424)`, making it easy to match the runtime with `~/.foxclaw/codex/telegram/<botid>/home`.
|
|
14
|
+
- Added a Safe sync button to the `/auth` panel, plus `/auth sync safe`, to flatten same-node multi-bot auth while globally idle and push validated candidates to cross-node peers.
|
|
15
|
+
- Added a full safe-sync path for the local auth mirror: it only propagates candidates that pass the existing online validation and fills runtime copies from newer same-account canonical candidates.
|
|
16
|
+
|
|
5
17
|
## 0.5.10 - 2026-06-08
|
|
6
18
|
|
|
7
19
|
### 中文
|
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex,
|
|
|
39
39
|
- 已经装好,想系统了解 `/help`、`/setup`、`/threads`、`/watch`、`/auth` 和账号轮转?看 [用户手册](./docs/zh/user-manual.md)。
|
|
40
40
|
- 想把同一组合法 ChatGPT auth 候选同步到多台机器?看 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
|
|
41
41
|
- 想了解每个版本改了什么?看 [更新日志](./CHANGELOG.md)。
|
|
42
|
+
- 维护者准备发版?看 [发布 runbook](./docs/zh/release.md)。
|
|
42
43
|
- Git、Node、`.env` 都玩得转?直接往下看快速设置。
|
|
43
44
|
- 卡住了?看 [故障排查](./docs/zh/troubleshooting.md)。
|
|
44
45
|
|
package/README_EN.md
CHANGED
|
@@ -39,6 +39,7 @@ FoxClaw is more than message forwarding. It provides Telegram panels for Codex w
|
|
|
39
39
|
- Already installed and want the full command guide for `/help`, `/setup`, `/threads`, `/watch`, `/auth`, and auth rotation? Read the [User Manual](./docs/user-manual.md).
|
|
40
40
|
- Want to sync the same legally owned ChatGPT auth candidate pool across multiple machines? Read the [Cross-Node Auth Sync Setup Guide](./docs/cross-node-auth-sync.md).
|
|
41
41
|
- Want to see what changed in each release? Read the [Changelog](./CHANGELOG.md).
|
|
42
|
+
- Maintaining a release? Use the [Release Runbook](./docs/release.md).
|
|
42
43
|
- Already comfortable with Git, Node, and `.env` files? Use the quick setup below.
|
|
43
44
|
- Something failed? Check [Troubleshooting](./docs/troubleshooting.md).
|
|
44
45
|
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface AuthMirrorSyncedEvent {
|
|
|
52
52
|
status: AuthMirrorStatus;
|
|
53
53
|
record: AuthMirrorCandidateRecord;
|
|
54
54
|
}
|
|
55
|
+
export interface AuthMirrorSyncAllResult {
|
|
56
|
+
synced: number;
|
|
57
|
+
skipped: number;
|
|
58
|
+
}
|
|
55
59
|
export interface AuthMirrorHooks {
|
|
56
60
|
onSynced?: (event: AuthMirrorSyncedEvent) => Promise<void> | void;
|
|
57
61
|
}
|
|
@@ -77,8 +81,10 @@ export declare class AuthCandidateMirror {
|
|
|
77
81
|
readRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorCandidateRecord | null>;
|
|
78
82
|
listNewestCandidates(): Promise<AuthMirrorCandidateRecord[]>;
|
|
79
83
|
syncRuntimeCandidate(runtimeId: string, candidateName: string): Promise<boolean>;
|
|
84
|
+
syncAllRuntimeCandidates(): Promise<AuthMirrorSyncAllResult>;
|
|
80
85
|
recoverRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorRecovery | null>;
|
|
81
86
|
private scan;
|
|
87
|
+
private distributeCanonicalCandidates;
|
|
82
88
|
importExternalCandidate(candidateName: string, raw: string, source: {
|
|
83
89
|
nodeId: string;
|
|
84
90
|
label?: string | null;
|
package/dist/auth/mirror.js
CHANGED
|
@@ -126,6 +126,28 @@ export class AuthCandidateMirror {
|
|
|
126
126
|
return false;
|
|
127
127
|
return this.withActivity(() => this.propagateValidatedCandidate(runtime, candidateName));
|
|
128
128
|
}
|
|
129
|
+
async syncAllRuntimeCandidates() {
|
|
130
|
+
return this.withActivity(async () => {
|
|
131
|
+
let synced = 0;
|
|
132
|
+
let skipped = 0;
|
|
133
|
+
for (const runtime of this.runtimes) {
|
|
134
|
+
const names = await listAuthCandidateNames(runtime.authDir);
|
|
135
|
+
for (const name of names) {
|
|
136
|
+
if (await this.propagateValidatedCandidate(runtime, name)) {
|
|
137
|
+
synced += 1;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
skipped += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const distributed = await this.distributeCanonicalCandidates();
|
|
145
|
+
return {
|
|
146
|
+
synced: synced + distributed.synced,
|
|
147
|
+
skipped: skipped + distributed.skipped,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
129
151
|
async recoverRuntimeCandidate(runtimeId, candidateName) {
|
|
130
152
|
if (!isAuthCandidateName(candidateName))
|
|
131
153
|
return null;
|
|
@@ -169,6 +191,33 @@ export class AuthCandidateMirror {
|
|
|
169
191
|
}
|
|
170
192
|
});
|
|
171
193
|
}
|
|
194
|
+
async distributeCanonicalCandidates() {
|
|
195
|
+
let synced = 0;
|
|
196
|
+
let skipped = 0;
|
|
197
|
+
for (const name of await listAuthCandidateNames(this.canonicalDir)) {
|
|
198
|
+
const canonical = await readChatGptAuthRecord(path.join(this.canonicalDir, name));
|
|
199
|
+
if (!canonical) {
|
|
200
|
+
skipped += this.runtimes.length;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const runtime of this.runtimes) {
|
|
204
|
+
const destinationPath = path.join(runtime.authDir, name);
|
|
205
|
+
const destination = await readChatGptAuthRecord(destinationPath);
|
|
206
|
+
if (destination && destination.accountId !== canonical.accountId) {
|
|
207
|
+
skipped += 1;
|
|
208
|
+
this.logger.warn('auth.mirror.distribution_conflict', { runtimeId: runtime.id, name });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (destination && destination.lastRefreshMs >= canonical.lastRefreshMs) {
|
|
212
|
+
skipped += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
await atomicWrite(destinationPath, canonical.raw);
|
|
216
|
+
synced += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { synced, skipped };
|
|
220
|
+
}
|
|
172
221
|
async importExternalCandidate(candidateName, raw, source) {
|
|
173
222
|
if (!isAuthCandidateName(candidateName)) {
|
|
174
223
|
return { ok: false, imported: false, reason: 'invalid candidate name' };
|
|
@@ -19,6 +19,12 @@ export interface CoreCoordinator {
|
|
|
19
19
|
}>;
|
|
20
20
|
releaseAuthRefreshLease?: (leaseId: string | null) => Promise<void>;
|
|
21
21
|
getAuthSyncStatus?: () => RuntimeStatus['authSync'];
|
|
22
|
+
authSyncSafeAll?: () => Promise<{
|
|
23
|
+
localSynced: number;
|
|
24
|
+
localSkipped: number;
|
|
25
|
+
sent: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
}>;
|
|
22
28
|
authSyncPushAll?: () => Promise<{
|
|
23
29
|
sent: number;
|
|
24
30
|
skipped: number;
|
|
@@ -283,6 +289,7 @@ export declare class BridgeSessionCore {
|
|
|
283
289
|
private notifyProactiveAuthRefresh;
|
|
284
290
|
private handleAuthCommand;
|
|
285
291
|
private handleAuthSyncCommand;
|
|
292
|
+
private runAuthSafeSyncAll;
|
|
286
293
|
private handleAuthRefreshAllCommand;
|
|
287
294
|
private handleAuthUseCommand;
|
|
288
295
|
private handleAuthToggleCommand;
|
|
@@ -1065,7 +1065,7 @@ export class BridgeSessionCore {
|
|
|
1065
1065
|
await this.handleAuthListViewCallback(event, authClearSearchMatch[1], 'clear_search', locale);
|
|
1066
1066
|
return;
|
|
1067
1067
|
}
|
|
1068
|
-
const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|refresh_all_confirm|refresh_all_cancel|refresh_all)$/.exec(event.data);
|
|
1068
|
+
const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|safe_sync|refresh_all_confirm|refresh_all_cancel|refresh_all)$/.exec(event.data);
|
|
1069
1069
|
if (authActionMatch) {
|
|
1070
1070
|
await this.handleAuthPanelActionCallback(event, authActionMatch[1], authActionMatch[2], locale);
|
|
1071
1071
|
return;
|
|
@@ -2956,7 +2956,7 @@ export class BridgeSessionCore {
|
|
|
2956
2956
|
authDisplayBotLabel() {
|
|
2957
2957
|
if (!this.config.tgScopeBotId)
|
|
2958
2958
|
return null;
|
|
2959
|
-
return this.botUsername ? `@${this.botUsername}` : this.config.tgScopeBotId;
|
|
2959
|
+
return this.botUsername ? `@${this.botUsername} (${this.config.tgScopeBotId})` : this.config.tgScopeBotId;
|
|
2960
2960
|
}
|
|
2961
2961
|
ownsScope(scopeId) {
|
|
2962
2962
|
if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
|
|
@@ -4467,17 +4467,19 @@ export class BridgeSessionCore {
|
|
|
4467
4467
|
await this.sendMessage(scopeId, message);
|
|
4468
4468
|
return;
|
|
4469
4469
|
}
|
|
4470
|
-
if (action === 'push' && args[1]?.toLowerCase() === 'all') {
|
|
4470
|
+
if (action === 'safe' || (action === 'push' && args[1]?.toLowerCase() === 'all')) {
|
|
4471
4471
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4472
4472
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_push_blocked_active'));
|
|
4473
4473
|
return;
|
|
4474
4474
|
}
|
|
4475
|
-
const result = await this.
|
|
4475
|
+
const result = await this.runAuthSafeSyncAll();
|
|
4476
4476
|
if (!result) {
|
|
4477
4477
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
|
|
4478
4478
|
return;
|
|
4479
4479
|
}
|
|
4480
|
-
await this.sendMessage(scopeId, t(locale, '
|
|
4480
|
+
await this.sendMessage(scopeId, t(locale, 'auth_sync_safe_done', {
|
|
4481
|
+
localSynced: result.localSynced,
|
|
4482
|
+
localSkipped: result.localSkipped,
|
|
4481
4483
|
sent: result.sent,
|
|
4482
4484
|
skipped: result.skipped,
|
|
4483
4485
|
}));
|
|
@@ -4485,6 +4487,16 @@ export class BridgeSessionCore {
|
|
|
4485
4487
|
}
|
|
4486
4488
|
await this.sendMessage(scopeId, t(locale, 'usage_auth_sync'));
|
|
4487
4489
|
}
|
|
4490
|
+
async runAuthSafeSyncAll() {
|
|
4491
|
+
const safeResult = await this.coordinator?.authSyncSafeAll?.();
|
|
4492
|
+
if (safeResult) {
|
|
4493
|
+
return safeResult;
|
|
4494
|
+
}
|
|
4495
|
+
const pushResult = await this.coordinator?.authSyncPushAll?.();
|
|
4496
|
+
return pushResult
|
|
4497
|
+
? { localSynced: 0, localSkipped: 0, sent: pushResult.sent, skipped: pushResult.skipped }
|
|
4498
|
+
: null;
|
|
4499
|
+
}
|
|
4488
4500
|
async handleAuthRefreshAllCommand(scopeId, locale, confirmed = false) {
|
|
4489
4501
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4490
4502
|
await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_blocked_active'));
|
|
@@ -4993,6 +5005,37 @@ export class BridgeSessionCore {
|
|
|
4993
5005
|
await this.handleLoginDeviceCommand(event.scopeId, locale);
|
|
4994
5006
|
return;
|
|
4995
5007
|
}
|
|
5008
|
+
if (action === 'safe_sync') {
|
|
5009
|
+
if (!this.canRunGlobalAuthRefresh()) {
|
|
5010
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_push_blocked_active'));
|
|
5011
|
+
return;
|
|
5012
|
+
}
|
|
5013
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_safe_starting'));
|
|
5014
|
+
if (record.messageId !== null) {
|
|
5015
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_safe_starting'), []);
|
|
5016
|
+
}
|
|
5017
|
+
const result = await this.runAuthSafeSyncAll();
|
|
5018
|
+
if (!result) {
|
|
5019
|
+
if (record.messageId !== null) {
|
|
5020
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_disabled'), authChoiceKeyboard(locale, record));
|
|
5021
|
+
}
|
|
5022
|
+
return;
|
|
5023
|
+
}
|
|
5024
|
+
const state = await this.listCodexAuthState();
|
|
5025
|
+
await this.applySharedCodexAuthQuotaSnapshots(state);
|
|
5026
|
+
record.candidates = state.candidates;
|
|
5027
|
+
record.createdAt = Date.now();
|
|
5028
|
+
clampCodexAuthListOffset(record);
|
|
5029
|
+
if (record.messageId !== null) {
|
|
5030
|
+
await this.editMessage(event.scopeId, record.messageId, `${t(locale, 'auth_sync_safe_done', {
|
|
5031
|
+
localSynced: result.localSynced,
|
|
5032
|
+
localSkipped: result.localSkipped,
|
|
5033
|
+
sent: result.sent,
|
|
5034
|
+
skipped: result.skipped,
|
|
5035
|
+
})}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
5036
|
+
}
|
|
5037
|
+
return;
|
|
5038
|
+
}
|
|
4996
5039
|
if (action === 'refresh_all') {
|
|
4997
5040
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4998
5041
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_blocked_active'));
|
|
@@ -8409,7 +8452,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
|
|
|
8409
8452
|
if (state.candidates.length === 0) {
|
|
8410
8453
|
lines.push(t(locale, 'auth_no_candidates'));
|
|
8411
8454
|
if (includeWeixinCopyPaste) {
|
|
8412
|
-
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth reload', '/permissions');
|
|
8455
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth sync safe', '/auth reload', '/permissions');
|
|
8413
8456
|
}
|
|
8414
8457
|
return lines.join('\n');
|
|
8415
8458
|
}
|
|
@@ -8442,7 +8485,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
|
|
|
8442
8485
|
if (includeWeixinCopyPaste) {
|
|
8443
8486
|
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...page.visible.map(({ index }) => `/auth use ${index + 1}`), ...page.visible.map(({ candidate, index }) => candidate.disabled
|
|
8444
8487
|
? `/auth enable ${index + 1}`
|
|
8445
|
-
: `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth reload', '/permissions');
|
|
8488
|
+
: `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth sync safe', '/auth reload', '/permissions');
|
|
8446
8489
|
}
|
|
8447
8490
|
return lines.join('\n');
|
|
8448
8491
|
}
|
|
@@ -8480,7 +8523,10 @@ function authChoiceKeyboard(locale, record) {
|
|
|
8480
8523
|
{ text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
|
|
8481
8524
|
{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
|
|
8482
8525
|
]);
|
|
8483
|
-
rows.push([
|
|
8526
|
+
rows.push([
|
|
8527
|
+
{ text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` },
|
|
8528
|
+
{ text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` },
|
|
8529
|
+
]);
|
|
8484
8530
|
return rows;
|
|
8485
8531
|
}
|
|
8486
8532
|
function formatCodexAuthCandidateDisplayName(name) {
|
package/dist/i18n.d.ts
CHANGED
|
@@ -143,8 +143,8 @@ declare const MESSAGES: {
|
|
|
143
143
|
readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
|
|
144
144
|
readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
|
|
145
145
|
readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
|
|
146
|
-
readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]";
|
|
147
|
-
readonly usage_auth_sync: "Usage: /auth sync <status|events [filter]|trace <requestId>|test|push all>";
|
|
146
|
+
readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <name>]";
|
|
147
|
+
readonly usage_auth_sync: "Usage: /auth sync <status|events [filter]|trace <requestId>|test|safe|push all>";
|
|
148
148
|
readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
|
|
149
149
|
readonly auth_list_title: "Codex auth files:";
|
|
150
150
|
readonly auth_bot: "Bot runtime: {value}";
|
|
@@ -212,6 +212,8 @@ declare const MESSAGES: {
|
|
|
212
212
|
readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
|
|
213
213
|
readonly auth_sync_test_missing: "Missing replies: {value}";
|
|
214
214
|
readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
|
|
215
|
+
readonly auth_sync_safe_starting: "Safely syncing auth across local bot runtimes and cross-node peers...";
|
|
216
|
+
readonly auth_sync_safe_done: "Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.";
|
|
215
217
|
readonly auth_sync_push_done: "Auth sync push complete: sent {sent}, skipped {skipped}.";
|
|
216
218
|
readonly auth_sync_status_title: "Cross-node auth sync:";
|
|
217
219
|
readonly auth_sync_status_node: "Node: {value}";
|
|
@@ -235,6 +237,7 @@ declare const MESSAGES: {
|
|
|
235
237
|
readonly auth_sync_trace_missing: "Usage: /auth sync trace <requestId>";
|
|
236
238
|
readonly button_login_device: "🔑 Login";
|
|
237
239
|
readonly button_auth_reload: "🔄 Reload auth";
|
|
240
|
+
readonly button_auth_safe_sync: "🧷 Safe sync";
|
|
238
241
|
readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
|
|
239
242
|
readonly button_auth_enable: "✅";
|
|
240
243
|
readonly button_auth_disable: "⏸️";
|
|
@@ -796,8 +799,8 @@ declare const MESSAGES: {
|
|
|
796
799
|
readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
|
|
797
800
|
readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
|
|
798
801
|
readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
|
|
799
|
-
readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]";
|
|
800
|
-
readonly usage_auth_sync: "用法:/auth sync <status|events [过滤]|trace <requestId>|test|push all>";
|
|
802
|
+
readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <名称>]";
|
|
803
|
+
readonly usage_auth_sync: "用法:/auth sync <status|events [过滤]|trace <requestId>|test|safe|push all>";
|
|
801
804
|
readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
|
|
802
805
|
readonly auth_list_title: "Codex auth 文件:";
|
|
803
806
|
readonly auth_bot: "Bot runtime:{value}";
|
|
@@ -865,6 +868,8 @@ declare const MESSAGES: {
|
|
|
865
868
|
readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
|
|
866
869
|
readonly auth_sync_test_missing: "未回应:{value}";
|
|
867
870
|
readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
|
|
871
|
+
readonly auth_sync_safe_starting: "正在安全同步本机多 bot runtime 和跨节点 auth...";
|
|
872
|
+
readonly auth_sync_safe_done: "安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。";
|
|
868
873
|
readonly auth_sync_push_done: "auth 同步推送完成:已发送 {sent},已跳过 {skipped}。";
|
|
869
874
|
readonly auth_sync_status_title: "跨节点 auth 同步:";
|
|
870
875
|
readonly auth_sync_status_node: "节点:{value}";
|
|
@@ -888,6 +893,7 @@ declare const MESSAGES: {
|
|
|
888
893
|
readonly auth_sync_trace_missing: "用法:/auth sync trace <requestId>";
|
|
889
894
|
readonly button_login_device: "🔑 设备登录";
|
|
890
895
|
readonly button_auth_reload: "🔄 重载 auth";
|
|
896
|
+
readonly button_auth_safe_sync: "🧷 安全同步";
|
|
891
897
|
readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
|
|
892
898
|
readonly button_auth_enable: "✅";
|
|
893
899
|
readonly button_auth_disable: "⏸️";
|
package/dist/i18n.js
CHANGED
|
@@ -141,8 +141,8 @@ const MESSAGES = {
|
|
|
141
141
|
auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
|
|
142
142
|
auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
|
|
143
143
|
auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
|
|
144
|
-
usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]',
|
|
145
|
-
usage_auth_sync: 'Usage: /auth sync <status|events [filter]|trace <requestId>|test|push all>',
|
|
144
|
+
usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <name>]',
|
|
145
|
+
usage_auth_sync: 'Usage: /auth sync <status|events [filter]|trace <requestId>|test|safe|push all>',
|
|
146
146
|
usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
|
|
147
147
|
auth_list_title: 'Codex auth files:',
|
|
148
148
|
auth_bot: 'Bot runtime: {value}',
|
|
@@ -210,6 +210,8 @@ const MESSAGES = {
|
|
|
210
210
|
auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
|
|
211
211
|
auth_sync_test_missing: 'Missing replies: {value}',
|
|
212
212
|
auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
|
|
213
|
+
auth_sync_safe_starting: 'Safely syncing auth across local bot runtimes and cross-node peers...',
|
|
214
|
+
auth_sync_safe_done: 'Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.',
|
|
213
215
|
auth_sync_push_done: 'Auth sync push complete: sent {sent}, skipped {skipped}.',
|
|
214
216
|
auth_sync_status_title: 'Cross-node auth sync:',
|
|
215
217
|
auth_sync_status_node: 'Node: {value}',
|
|
@@ -233,6 +235,7 @@ const MESSAGES = {
|
|
|
233
235
|
auth_sync_trace_missing: 'Usage: /auth sync trace <requestId>',
|
|
234
236
|
button_login_device: '🔑 Login',
|
|
235
237
|
button_auth_reload: '🔄 Reload auth',
|
|
238
|
+
button_auth_safe_sync: '🧷 Safe sync',
|
|
236
239
|
button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
|
|
237
240
|
button_auth_enable: '✅',
|
|
238
241
|
button_auth_disable: '⏸️',
|
|
@@ -794,8 +797,8 @@ const MESSAGES = {
|
|
|
794
797
|
auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
|
|
795
798
|
auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
|
|
796
799
|
auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
|
|
797
|
-
usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]',
|
|
798
|
-
usage_auth_sync: '用法:/auth sync <status|events [过滤]|trace <requestId>|test|push all>',
|
|
800
|
+
usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <名称>]',
|
|
801
|
+
usage_auth_sync: '用法:/auth sync <status|events [过滤]|trace <requestId>|test|safe|push all>',
|
|
799
802
|
usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
|
|
800
803
|
auth_list_title: 'Codex auth 文件:',
|
|
801
804
|
auth_bot: 'Bot runtime:{value}',
|
|
@@ -863,6 +866,8 @@ const MESSAGES = {
|
|
|
863
866
|
auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
|
|
864
867
|
auth_sync_test_missing: '未回应:{value}',
|
|
865
868
|
auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
|
|
869
|
+
auth_sync_safe_starting: '正在安全同步本机多 bot runtime 和跨节点 auth...',
|
|
870
|
+
auth_sync_safe_done: '安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。',
|
|
866
871
|
auth_sync_push_done: 'auth 同步推送完成:已发送 {sent},已跳过 {skipped}。',
|
|
867
872
|
auth_sync_status_title: '跨节点 auth 同步:',
|
|
868
873
|
auth_sync_status_node: '节点:{value}',
|
|
@@ -886,6 +891,7 @@ const MESSAGES = {
|
|
|
886
891
|
auth_sync_trace_missing: '用法:/auth sync trace <requestId>',
|
|
887
892
|
button_login_device: '🔑 设备登录',
|
|
888
893
|
button_auth_reload: '🔄 重载 auth',
|
|
894
|
+
button_auth_safe_sync: '🧷 安全同步',
|
|
889
895
|
button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
|
|
890
896
|
button_auth_enable: '✅',
|
|
891
897
|
button_auth_disable: '⏸️',
|
package/dist/main.js
CHANGED
|
@@ -342,6 +342,16 @@ async function runServeCli() {
|
|
|
342
342
|
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
343
343
|
?? localAuthRefreshLease.release(leaseId),
|
|
344
344
|
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
345
|
+
authSyncSafeAll: async () => {
|
|
346
|
+
const local = await mirror.syncAllRuntimeCandidates();
|
|
347
|
+
const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
348
|
+
return {
|
|
349
|
+
localSynced: local.synced,
|
|
350
|
+
localSkipped: local.skipped,
|
|
351
|
+
sent: remote.sent,
|
|
352
|
+
skipped: remote.skipped,
|
|
353
|
+
};
|
|
354
|
+
},
|
|
345
355
|
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
346
356
|
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
347
357
|
statusUpdated: () => writeAggregateStatus(),
|
|
@@ -500,6 +510,16 @@ async function runServeCli() {
|
|
|
500
510
|
releaseAuthRefreshLease: (leaseId) => singleAuthSync?.releaseRefreshLease(leaseId)
|
|
501
511
|
?? singleLocalAuthRefreshLease.release(leaseId),
|
|
502
512
|
getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
|
|
513
|
+
authSyncSafeAll: async () => {
|
|
514
|
+
const local = await singleMirror?.syncAllRuntimeCandidates() ?? { synced: 0, skipped: 0 };
|
|
515
|
+
const remote = await singleAuthSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
516
|
+
return {
|
|
517
|
+
localSynced: local.synced,
|
|
518
|
+
localSkipped: local.skipped,
|
|
519
|
+
sent: remote.sent,
|
|
520
|
+
skipped: remote.skipped,
|
|
521
|
+
};
|
|
522
|
+
},
|
|
503
523
|
authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
504
524
|
authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
505
525
|
statusUpdated: (status) => {
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# FoxClaw Release Runbook
|
|
2
|
+
|
|
3
|
+
This file is for maintainers. User upgrades are driven by `/update` or `foxclaw update`, but those commands install `@foxden-app/foxclaw@latest` from the npm registry. Pushing a PR branch or a normal commit does not make installed nodes receive a new version.
|
|
4
|
+
|
|
5
|
+
## Release Rules
|
|
6
|
+
|
|
7
|
+
- npm `latest` is the source of truth for `/update`.
|
|
8
|
+
- The GitHub `Publish` workflow is triggered by `v*` tags and also supports manual dispatch; normal releases should use tags.
|
|
9
|
+
- The tag name must match `v<package.json version>`. For example, `package.json` version `0.5.10` must be released with `v0.5.10`.
|
|
10
|
+
- Versions already present on npm are not published again. Reinstalling the same version may report `0.5.10 -> 0.5.10` in Telegram; that means registry latest did not move.
|
|
11
|
+
- `CHANGELOG.md` must contain the target version entry. `/update` reads that entry from the installed package and shows it as the upgrade notes.
|
|
12
|
+
|
|
13
|
+
## Preflight
|
|
14
|
+
|
|
15
|
+
Check the worktree and remote state:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git status --short --branch
|
|
19
|
+
npm pkg get name version
|
|
20
|
+
npm view @foxden-app/foxclaw version
|
|
21
|
+
git tag --list 'v*' --sort=-v:refname | head
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Confirm the target version and remote tag do not already exist:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm view @foxden-app/foxclaw@0.5.10 version 2>/dev/null || true
|
|
28
|
+
git ls-remote --tags origin refs/tags/v0.5.10
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run the same local checks as the publish workflow:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run lint
|
|
35
|
+
npm run typecheck
|
|
36
|
+
npm test
|
|
37
|
+
npm pack --dry-run
|
|
38
|
+
git diff --check
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`npm pack --dry-run` runs `prepack` and lists the package contents. Confirm `CHANGELOG.md` is included.
|
|
42
|
+
|
|
43
|
+
## Prepare The Version
|
|
44
|
+
|
|
45
|
+
1. Update `package.json` and `package-lock.json`.
|
|
46
|
+
2. Add a new top entry to `CHANGELOG.md` with both `### 中文` and `### English` sections.
|
|
47
|
+
3. Commit the release metadata:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git add package.json package-lock.json CHANGELOG.md
|
|
51
|
+
git commit -m "发布 0.5.10:short release summary"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Push And Publish
|
|
55
|
+
|
|
56
|
+
This repository uses lightweight tags:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git tag v0.5.10
|
|
60
|
+
git push origin <branch>
|
|
61
|
+
git push origin v0.5.10
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Watch the publish workflow:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
gh run list --repo foxden-app/foxclaw --workflow Publish --limit 5
|
|
68
|
+
gh run watch <run-id> --repo foxden-app/foxclaw --exit-status
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
After success, verify npm and GitHub Releases:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm view @foxden-app/foxclaw version
|
|
75
|
+
gh release view v0.5.10 --repo foxden-app/foxclaw --json tagName,name,url,publishedAt,isDraft,isPrerelease
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Verify `/update`
|
|
79
|
+
|
|
80
|
+
On a node still running the previous version, send `/update`. Expected:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
FoxClaw upgraded and restarted: 0.5.9 -> 0.5.10.
|
|
84
|
+
|
|
85
|
+
What changed:
|
|
86
|
+
- ...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If the node is already on the latest version, another `/update` may report:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
FoxClaw upgraded and restarted: 0.5.10 -> 0.5.10.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
That means it reinstalled the current npm latest, not that a newer version was available.
|
|
96
|
+
|
|
97
|
+
## Failure Handling
|
|
98
|
+
|
|
99
|
+
- If the workflow fails during version validation, check that the tag and `package.json` version match.
|
|
100
|
+
- If npm says the version already exists, do not reuse that version; publish the next patch after fixing the metadata.
|
|
101
|
+
- If npm publish succeeds but GitHub Release creation fails, rerun the workflow or repair the release notes manually.
|
|
102
|
+
- Do not delete and republish npm versions. Prefer publishing a fixed patch release; use npm dist-tags for emergency latest rollback only when necessary.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# FoxClaw 发布 runbook
|
|
2
|
+
|
|
3
|
+
本文件给维护者使用。用户侧升级由 `/update` 或 `foxclaw update` 完成,但它们只会安装 npm registry 上的 `@foxden-app/foxclaw@latest`。推 PR、推普通分支或本地提交都不会让已安装节点收到新版。
|
|
4
|
+
|
|
5
|
+
## 发布规则
|
|
6
|
+
|
|
7
|
+
- npm `latest` 是 `/update` 的事实来源。
|
|
8
|
+
- GitHub `Publish` workflow 由 `v*` tag 触发,也支持手动 dispatch;正常发布只使用 tag。
|
|
9
|
+
- tag 名必须等于 `v<package.json version>`,例如 `package.json` 是 `0.5.10` 时只能推 `v0.5.10`。
|
|
10
|
+
- npm 上已经存在的版本不会再次发布。重新安装同版本时,Telegram 可能显示 `0.5.10 -> 0.5.10`,这表示 registry latest 没有前进。
|
|
11
|
+
- `CHANGELOG.md` 必须包含目标版本条目。`/update` 完成回报会从已安装包读取这个条目展示“更新内容”。
|
|
12
|
+
|
|
13
|
+
## 发布前检查
|
|
14
|
+
|
|
15
|
+
确认工作区和远端状态:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git status --short --branch
|
|
19
|
+
npm pkg get name version
|
|
20
|
+
npm view @foxden-app/foxclaw version
|
|
21
|
+
git tag --list 'v*' --sort=-v:refname | head
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
确认目标版本还没有发布,也没有远端 tag:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm view @foxden-app/foxclaw@0.5.10 version 2>/dev/null || true
|
|
28
|
+
git ls-remote --tags origin refs/tags/v0.5.10
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
运行与发布 workflow 对齐的本地校验:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run lint
|
|
35
|
+
npm run typecheck
|
|
36
|
+
npm test
|
|
37
|
+
npm pack --dry-run
|
|
38
|
+
git diff --check
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`npm pack --dry-run` 会执行 `prepack` 构建,并列出 npm 包内容。确认输出里有 `CHANGELOG.md`。
|
|
42
|
+
|
|
43
|
+
## 准备版本
|
|
44
|
+
|
|
45
|
+
1. 更新 `package.json` 和 `package-lock.json` 的版本号。
|
|
46
|
+
2. 在 `CHANGELOG.md` 顶部新增版本条目,包含 `### 中文` 和 `### English` 小节。
|
|
47
|
+
3. 提交发布 commit,推荐格式:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git add package.json package-lock.json CHANGELOG.md
|
|
51
|
+
git commit -m "发布 0.5.10:一句话说明"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 推送并触发发布
|
|
55
|
+
|
|
56
|
+
当前仓库使用 lightweight tag:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git tag v0.5.10
|
|
60
|
+
git push origin <branch>
|
|
61
|
+
git push origin v0.5.10
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
观察发布 workflow:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
gh run list --repo foxden-app/foxclaw --workflow Publish --limit 5
|
|
68
|
+
gh run watch <run-id> --repo foxden-app/foxclaw --exit-status
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
成功后确认 npm 和 GitHub Release:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm view @foxden-app/foxclaw version
|
|
75
|
+
gh release view v0.5.10 --repo foxden-app/foxclaw --json tagName,name,url,publishedAt,isDraft,isPrerelease
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 验收 `/update`
|
|
79
|
+
|
|
80
|
+
在仍安装旧版本的节点上发送 `/update`,期望看到:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
FoxClaw 已升级并重启:0.5.9 -> 0.5.10。
|
|
84
|
+
|
|
85
|
+
更新内容:
|
|
86
|
+
- ...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
如果节点已经是最新版本,再次 `/update` 可能显示:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
FoxClaw 已升级并重启:0.5.10 -> 0.5.10。
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
这只表示重新安装了当前 npm latest,并不代表有新版本。
|
|
96
|
+
|
|
97
|
+
## 发布失败处理
|
|
98
|
+
|
|
99
|
+
- 如果 workflow 在版本校验失败,先检查 tag 和 `package.json` 是否一致。
|
|
100
|
+
- 如果 npm 显示版本已存在,不要复用同一个版本号;修正后发布下一个 patch 版本。
|
|
101
|
+
- 如果 npm publish 成功但 GitHub Release 创建失败,可以重新运行 workflow 或手动修复 release notes。
|
|
102
|
+
- 已发布到 npm 的版本不要删除重发。需要撤回线上 latest 时,优先发布修复版;只有紧急情况下才考虑调整 npm dist-tag。
|