@foxden-app/foxclaw 0.5.77 → 0.6.3

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.
@@ -0,0 +1,61 @@
1
+ import type { Event, PermissionRequest, QuestionRequest, SessionStatus } from '@opencode-ai/sdk/v2';
2
+ export interface OpencodeTextEvent {
3
+ kind: 'text';
4
+ sessionId: string;
5
+ messageId: string;
6
+ partId: string;
7
+ text: string;
8
+ delta: string | null;
9
+ }
10
+ export interface OpencodeToolEvent {
11
+ kind: 'tool';
12
+ sessionId: string;
13
+ messageId: string;
14
+ partId: string;
15
+ callId: string;
16
+ tool: string;
17
+ status: string;
18
+ title: string | null;
19
+ error: string | null;
20
+ }
21
+ export type OpencodeBridgeEvent = OpencodeTextEvent | OpencodeToolEvent | {
22
+ kind: 'permission';
23
+ request: PermissionRequest;
24
+ } | {
25
+ kind: 'permissionResolved';
26
+ sessionId: string;
27
+ requestId: string;
28
+ } | {
29
+ kind: 'question';
30
+ request: QuestionRequest;
31
+ } | {
32
+ kind: 'questionResolved';
33
+ sessionId: string;
34
+ requestId: string;
35
+ } | {
36
+ kind: 'status';
37
+ sessionId: string;
38
+ status: SessionStatus;
39
+ } | {
40
+ kind: 'idle';
41
+ sessionId: string;
42
+ } | {
43
+ kind: 'error';
44
+ sessionId: string | null;
45
+ message: string;
46
+ };
47
+ /**
48
+ * Turns OpenCode's public SSE events into the small, stable event surface used
49
+ * by the Telegram bridge. State is intentionally kept here so reconnect and
50
+ * delta handling can be tested without starting a real server.
51
+ */
52
+ export declare class OpencodeEventNormalizer {
53
+ private readonly roles;
54
+ private readonly textByPart;
55
+ private readonly sessionByMessage;
56
+ reset(): void;
57
+ accept(event: Event): OpencodeBridgeEvent[];
58
+ private normalizePart;
59
+ private clearSession;
60
+ }
61
+ export declare function formatOpencodeError(error: unknown): string;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Turns OpenCode's public SSE events into the small, stable event surface used
3
+ * by the Telegram bridge. State is intentionally kept here so reconnect and
4
+ * delta handling can be tested without starting a real server.
5
+ */
6
+ export class OpencodeEventNormalizer {
7
+ roles = new Map();
8
+ textByPart = new Map();
9
+ sessionByMessage = new Map();
10
+ reset() {
11
+ this.roles.clear();
12
+ this.textByPart.clear();
13
+ this.sessionByMessage.clear();
14
+ }
15
+ accept(event) {
16
+ switch (event.type) {
17
+ case 'message.updated': {
18
+ this.roles.set(event.properties.info.id, event.properties.info.role);
19
+ this.sessionByMessage.set(event.properties.info.id, event.properties.info.sessionID);
20
+ return [];
21
+ }
22
+ case 'message.part.delta': {
23
+ if (event.properties.field !== 'text' || this.roles.get(event.properties.messageID) !== 'assistant') {
24
+ return [];
25
+ }
26
+ const key = partKey(event.properties.messageID, event.properties.partID);
27
+ const text = `${this.textByPart.get(key) ?? ''}${event.properties.delta}`;
28
+ this.textByPart.set(key, text);
29
+ return [{
30
+ kind: 'text',
31
+ sessionId: event.properties.sessionID,
32
+ messageId: event.properties.messageID,
33
+ partId: event.properties.partID,
34
+ text,
35
+ delta: event.properties.delta,
36
+ }];
37
+ }
38
+ case 'message.part.updated':
39
+ return this.normalizePart(event.properties.part);
40
+ case 'permission.asked':
41
+ return [{ kind: 'permission', request: event.properties }];
42
+ case 'permission.replied':
43
+ return [{
44
+ kind: 'permissionResolved',
45
+ sessionId: event.properties.sessionID,
46
+ requestId: event.properties.requestID,
47
+ }];
48
+ case 'question.asked':
49
+ return [{ kind: 'question', request: event.properties }];
50
+ case 'question.replied':
51
+ case 'question.rejected':
52
+ return [{
53
+ kind: 'questionResolved',
54
+ sessionId: event.properties.sessionID,
55
+ requestId: event.properties.requestID,
56
+ }];
57
+ case 'session.status':
58
+ if (event.properties.status.type === 'idle')
59
+ this.clearSession(event.properties.sessionID);
60
+ return [{ kind: 'status', sessionId: event.properties.sessionID, status: event.properties.status }];
61
+ case 'session.idle':
62
+ this.clearSession(event.properties.sessionID);
63
+ return [{ kind: 'idle', sessionId: event.properties.sessionID }];
64
+ case 'session.error':
65
+ if (event.properties.sessionID)
66
+ this.clearSession(event.properties.sessionID);
67
+ return [{
68
+ kind: 'error',
69
+ sessionId: event.properties.sessionID ?? null,
70
+ message: formatOpencodeError(event.properties.error),
71
+ }];
72
+ default:
73
+ return [];
74
+ }
75
+ }
76
+ normalizePart(part) {
77
+ if (part.type === 'text') {
78
+ if (this.roles.get(part.messageID) !== 'assistant')
79
+ return [];
80
+ const key = partKey(part.messageID, part.id);
81
+ const previous = this.textByPart.get(key) ?? '';
82
+ this.textByPart.set(key, part.text);
83
+ if (part.text === previous)
84
+ return [];
85
+ return [{
86
+ kind: 'text',
87
+ sessionId: part.sessionID,
88
+ messageId: part.messageID,
89
+ partId: part.id,
90
+ text: part.text,
91
+ delta: part.text.startsWith(previous) ? part.text.slice(previous.length) : null,
92
+ }];
93
+ }
94
+ if (part.type !== 'tool')
95
+ return [];
96
+ const state = part.state;
97
+ return [{
98
+ kind: 'tool',
99
+ sessionId: part.sessionID,
100
+ messageId: part.messageID,
101
+ partId: part.id,
102
+ callId: part.callID,
103
+ tool: part.tool,
104
+ status: state.status,
105
+ title: 'title' in state && typeof state.title === 'string' ? state.title : null,
106
+ error: state.status === 'error' ? state.error : null,
107
+ }];
108
+ }
109
+ clearSession(sessionId) {
110
+ for (const [messageId, owner] of this.sessionByMessage) {
111
+ if (owner !== sessionId)
112
+ continue;
113
+ this.sessionByMessage.delete(messageId);
114
+ this.roles.delete(messageId);
115
+ for (const key of this.textByPart.keys()) {
116
+ if (key.startsWith(`${messageId}:`))
117
+ this.textByPart.delete(key);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ function partKey(messageId, partId) {
123
+ return `${messageId}:${partId}`;
124
+ }
125
+ export function formatOpencodeError(error) {
126
+ if (!error)
127
+ return 'OpenCode session failed';
128
+ if (typeof error === 'string')
129
+ return error;
130
+ if (typeof error === 'object') {
131
+ const record = error;
132
+ if (typeof record.data?.message === 'string')
133
+ return record.data.message;
134
+ if (typeof record.message === 'string')
135
+ return record.message;
136
+ if (typeof record.name === 'string')
137
+ return record.name;
138
+ }
139
+ try {
140
+ return JSON.stringify(error);
141
+ }
142
+ catch {
143
+ return String(error);
144
+ }
145
+ }
@@ -0,0 +1,14 @@
1
+ import type { AppConfig } from '../config.js';
2
+ import type { Logger } from '../logger.js';
3
+ import type { BridgeStore } from '../store/database.js';
4
+ import { OpencodeBridgeCore } from './controller.js';
5
+ /** Keeps the optional OpenCode Telegram bot lifecycle out of the Codex runtime branches. */
6
+ export declare class OpencodeTelegramRuntime {
7
+ private readonly bot;
8
+ private readonly app;
9
+ private readonly core;
10
+ constructor(config: AppConfig, store: BridgeStore, logger: Logger);
11
+ start(): Promise<void>;
12
+ stop(): Promise<void>;
13
+ getRuntimeStatus(): ReturnType<OpencodeBridgeCore['getRuntimeStatus']>;
14
+ }
@@ -0,0 +1,29 @@
1
+ import { TelegramMessagingPort } from '../channels/telegram/telegram_messaging_port.js';
2
+ import { getOpencodeTelegramCommands } from '../i18n.js';
3
+ import { TelegramGateway } from '../telegram/gateway.js';
4
+ import { OpencodeAppClient } from './client.js';
5
+ import { OpencodeBridgeCore } from './controller.js';
6
+ /** Keeps the optional OpenCode Telegram bot lifecycle out of the Codex runtime branches. */
7
+ export class OpencodeTelegramRuntime {
8
+ bot;
9
+ app;
10
+ core;
11
+ constructor(config, store, logger) {
12
+ if (!config.opencodeBotToken)
13
+ throw new Error('OPENCODE_BOT_TOKEN is required for the OpenCode runtime');
14
+ this.bot = new TelegramGateway(config.opencodeBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true, getOpencodeTelegramCommands);
15
+ this.app = new OpencodeAppClient(config.opencodeCliBin, config.opencodeServerPassword, config.opencodeServerStatePath, config.opencodeServerLogPath, logger);
16
+ this.core = new OpencodeBridgeCore(config, store, logger, this.bot, this.app, new TelegramMessagingPort(this.bot));
17
+ this.core.registerInboundHandlers();
18
+ }
19
+ async start() {
20
+ await this.bot.initializeIdentity();
21
+ await this.core.start();
22
+ }
23
+ async stop() {
24
+ await this.core.stop();
25
+ }
26
+ getRuntimeStatus() {
27
+ return this.core.getRuntimeStatus();
28
+ }
29
+ }
@@ -60,6 +60,7 @@ export declare class BridgeStore {
60
60
  } | null;
61
61
  getBinding(chatId: string): ThreadBinding | null;
62
62
  setBinding(chatId: string, threadId: string, cwd: string | null): void;
63
+ listBindings(): ThreadBinding[];
63
64
  clearBinding(chatId: string): void;
64
65
  getChatSettings(chatId: string): ChatSessionSettings | null;
65
66
  setChatSettings(chatId: string, model: string | null, reasoningEffort: ReasoningEffortValue | null, locale?: AppLocale | null): void;
@@ -287,6 +287,15 @@ export class BridgeStore {
287
287
  ON CONFLICT(chat_id) DO UPDATE SET thread_id = excluded.thread_id, cwd = excluded.cwd, updated_at = excluded.updated_at
288
288
  `).run(chatId, threadId, cwd, Date.now());
289
289
  }
290
+ listBindings() {
291
+ const rows = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings ORDER BY updated_at DESC').all();
292
+ return rows.map((row) => ({
293
+ chatId: String(row.chat_id),
294
+ threadId: String(row.thread_id),
295
+ cwd: row.cwd === null ? null : String(row.cwd),
296
+ updatedAt: Number(row.updated_at),
297
+ }));
298
+ }
290
299
  clearBinding(chatId) {
291
300
  this.db.prepare('DELETE FROM chat_bindings WHERE chat_id = ?').run(chatId);
292
301
  }
@@ -5,6 +5,10 @@ import type { Logger } from '../logger.js';
5
5
  import type { TelegramMessageEntity } from './addressing.js';
6
6
  import type { TelegramInboundAttachment } from './media.js';
7
7
  import type { TelegramInputRichMessage } from './rich.js';
8
+ export type TelegramCommandProvider = (locale: 'en' | 'zh') => Array<{
9
+ command: string;
10
+ description: string;
11
+ }>;
8
12
  export interface TelegramTextEvent {
9
13
  chatId: string;
10
14
  topicId: number | null;
@@ -45,11 +49,12 @@ export declare class TelegramGateway extends EventEmitter {
45
49
  private readonly store;
46
50
  private readonly logger;
47
51
  private readonly namespacedScopes;
52
+ private readonly commandProvider;
48
53
  private running;
49
54
  private botKey;
50
55
  private botUsername;
51
56
  private botUserId;
52
- constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger, namespacedScopes?: boolean);
57
+ constructor(botToken: string, allowedUserId: string, allowedChatId: string | null, pollIntervalMs: number, store: BridgeStore, logger: Logger, namespacedScopes?: boolean, commandProvider?: TelegramCommandProvider);
53
58
  get username(): string | null;
54
59
  get identity(): string | null;
55
60
  initializeIdentity(): Promise<string>;
@@ -12,11 +12,12 @@ export class TelegramGateway extends EventEmitter {
12
12
  store;
13
13
  logger;
14
14
  namespacedScopes;
15
+ commandProvider;
15
16
  running = false;
16
17
  botKey;
17
18
  botUsername = null;
18
19
  botUserId = null;
19
- constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger, namespacedScopes = false) {
20
+ constructor(botToken, allowedUserId, allowedChatId, pollIntervalMs, store, logger, namespacedScopes = false, commandProvider = getTelegramCommands) {
20
21
  super();
21
22
  this.botToken = botToken;
22
23
  this.allowedUserId = allowedUserId;
@@ -25,6 +26,7 @@ export class TelegramGateway extends EventEmitter {
25
26
  this.store = store;
26
27
  this.logger = logger;
27
28
  this.namespacedScopes = namespacedScopes;
29
+ this.commandProvider = commandProvider;
28
30
  this.botKey = `telegram:${crypto.createHash('sha256').update(this.botToken).digest('hex').slice(0, 8)}`;
29
31
  }
30
32
  get username() {
@@ -224,14 +226,14 @@ export class TelegramGateway extends EventEmitter {
224
226
  }
225
227
  async registerCommands() {
226
228
  await callTelegramApi(this.botToken, 'setMyCommands', {
227
- commands: getTelegramCommands('zh'),
229
+ commands: this.commandProvider('zh'),
228
230
  });
229
231
  await callTelegramApi(this.botToken, 'setMyCommands', {
230
- commands: getTelegramCommands('en'),
232
+ commands: this.commandProvider('en'),
231
233
  language_code: 'en',
232
234
  });
233
235
  await callTelegramApi(this.botToken, 'setMyCommands', {
234
- commands: getTelegramCommands('zh'),
236
+ commands: this.commandProvider('zh'),
235
237
  language_code: 'zh',
236
238
  });
237
239
  }
package/dist/types.d.ts CHANGED
@@ -11,7 +11,7 @@ export type SandboxModeValue = 'read-only' | 'workspace-write' | 'danger-full-ac
11
11
  export type AccessPresetValue = 'read-only' | 'default' | 'full-access';
12
12
  export type CollaborationModeValue = 'default' | 'plan';
13
13
  export type ActiveTurnMessageMode = 'steer' | 'queue';
14
- export type ReasoningEffortValue = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
14
+ export type ReasoningEffortValue = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra';
15
15
  export type ThreadStatusKind = 'active' | 'idle' | 'notLoaded' | 'systemError';
16
16
  export interface ChatSessionSettings {
17
17
  /** Bridge scope id (e.g. `telegram:…`). */
@@ -153,11 +153,11 @@ Auth sync test complete: sent 1, replies 1.
153
153
 
154
154
  If it shows `Missing replies: @peer_bot`, Telegram delivery may have succeeded, but the peer did not receive, decrypt, pass allowlist validation, or run the same auth sync configuration.
155
155
 
156
- The `/auth` panel also provides **Check all nodes and reconcile auth** (`/auth sync audit`). It asks every configured peer to validate every local candidate against the usage endpoint, then:
156
+ The `/auth` panel's **Safe sync** action (`/auth sync safe`; `/auth sync audit` remains a compatibility alias) performs the complete all-node audit and reconciliation flow. It asks every configured peer to validate every local candidate against the usage endpoint, then:
157
157
 
158
158
  - selects the newest usage-validated copy for each same-name, same-account and same-user identity and distributes it to every peer;
159
159
  - allows a validated older copy to replace a newer timestamped copy only when the newer copy failed this explicit audit;
160
- - marks a candidate `?` only when every peer responded, no node has a valid copy, and at least two nodes independently reported it invalid;
160
+ - sends a single-node invalid candidate through the encrypted channel for validation-only checks on other nodes, without importing it, and marks it `?` only when no valid result exists and at least two nodes independently report it invalid;
161
161
  - leaves candidates unchanged when any peer is missing or busy, or when account/user identities conflict;
162
162
  - refreshes enabled ChatGPT candidates whose `last_refresh` is at least 8 days old on the initiating node, then distributes the refreshed copies.
163
163
 
@@ -268,7 +268,7 @@ foxclaw send-media /absolute/path/output.mp4 "caption"
268
268
  It controls:
269
269
 
270
270
  - Model: server default, or a model returned by app-server.
271
- - Reasoning effort: for example `low`, `medium`, `high`, or `xhigh`, depending on model support.
271
+ - Reasoning effort: for example `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`, depending on model support.
272
272
  - Fast tier: available when supported by the selected model.
273
273
  - Access: `read-only`, `default`, or `full-access`.
274
274
  - Mode: `Agent` or `Plan`.
@@ -433,13 +433,13 @@ Quota remaining: window:percent|auth
433
433
  [✅ 20|25|personal] [✅]
434
434
  [🔐 —|—|team] [✅]
435
435
  [☑️ All] [Enabled] [Attention]
436
- [🛡️ Access] [🔑 Login]
437
- [🔄 Reload auth]
436
+ [🔑 Login]
437
+ [🩺 Safe sync]
438
438
  ```
439
439
 
440
440
  The right-side `✅` / `⏸️` button controls whether the candidate participates in auto-rotation. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. Tapping a candidate switches auth, restarts that runtime, and refreshes the same panel with its buttons intact so you can switch again immediately. `--` means no quota snapshot has been observed for that candidate yet. Health summaries distinguish ready, low quota, quota exhausted, quota unknown, not recently refreshed, API key, invalid auth file, and needs login repair states.
441
441
 
442
- The **Check all nodes and reconcile auth** button asks every configured auth-sync peer to validate its candidates. FoxClaw adopts the newest valid copy, distributes it, marks an account `?` only after complete multi-node invalid consensus, and has the initiating node refresh enabled ChatGPT credentials last refreshed at least 8 days ago. Missing, busy, or identity-conflicting peers prevent invalid marking and stale refresh for that run.
442
+ The **Safe sync** button asks every configured auth-sync peer to validate candidates, adopts the newest valid same-account copy, and distributes it. A candidate that exists on only one node is sent to other nodes for validation only, without being imported; it is marked `?` only when no node validates it and at least two nodes independently reject it. The initiating node also refreshes enabled ChatGPT credentials last refreshed at least 8 days ago. Missing, busy, or identity-conflicting peers prevent stale refresh for that run.
443
443
 
444
444
  When an auth candidate has already failed while in use and FoxClaw cannot recover a newer same-account credential from local mirror or cross-node sync, it is marked as `needs login repair`. These candidates are skipped by auto-rotation and proactive refresh, and are hidden from the `Enabled` filter. Their row shows a `?` action. Tapping it opens two choices: Login repair starts device-code login with that candidate selected; Delete removes the candidate from canonical storage and all local bot runtimes, and clears cached quota for it.
445
445
 
@@ -497,7 +497,7 @@ Commands:
497
497
  - `/auth sync events [filter]`: show recent sync event records, optionally filtered by candidate, peer, request id, kind, stage, or detail.
498
498
  - `/auth sync trace <requestId>`: show recent records for one request id or event id.
499
499
  - `/auth sync test`: send an encrypted ping and wait for peer pong replies to verify peer config, shared key, and Bot-to-Bot private messages.
500
- - `/auth sync audit`: run the same validate, reconcile, consensus, stale-refresh, and distribution flow as the `/auth` cluster-check button.
500
+ - `/auth sync safe`: run the same all-node validation, reconciliation, invalid verification, stale-refresh, and distribution flow as the `/auth` Safe sync button. `/auth sync audit` remains a compatibility alias.
501
501
  - `/auth sync push all`: manually broadcast all locally verified candidates without refreshing tokens. “Sent” does not mean the peer imported files; check `/auth sync status` and `/auth` on the peer.
502
502
 
503
503
  Equivalent commands:
@@ -153,11 +153,11 @@ auth sync 测试完成:已发送 1,收到回应 1。
153
153
 
154
154
  如果显示 `未回应:@peer_bot`,说明 Telegram 发送可能成功,但对方没有成功接收、解密、通过 allowlist,或没有运行同一组 auth sync 配置。
155
155
 
156
- `/auth` 面板还提供 **全节点自检并同步**(命令等价入口是 `/auth sync audit`)。它会让所有已配置 peer 用 usage 接口逐个验证本机候选,然后:
156
+ `/auth` 面板的 **安全同步**(命令等价入口是 `/auth sync safe`,旧的 `/auth sync audit` 仍兼容)会执行完整的全节点自检与同步。它会让所有已配置 peer 用 usage 接口逐个验证本机候选,然后:
157
157
 
158
158
  - 对同名、同 account、同 ChatGPT 用户身份的候选,选择最新且验证有效的副本并分发给所有 peer;
159
159
  - 只有显式审计已经证明时间戳更晚的副本无效时,才允许经过验证的较旧有效副本替换它;
160
- - 仅当所有 peer 都回应、没有任何有效副本,且至少两个节点独立判定无效时,才把候选标为 `?`;
160
+ - 对只存在于单个节点的无效候选,会通过加密通道交给其他节点做只验证、不导入的复核;没有任何有效结果且至少两个节点独立判定无效时,才把候选标为 `?`;
161
161
  - 任一 peer 未回应、忙碌或账号/用户身份冲突时,不做无效裁决;
162
162
  - 由发起节点刷新 `last_refresh` 已满 8 天的已启用 ChatGPT 候选,再把刷新结果分发给所有 peer。
163
163
 
@@ -268,7 +268,7 @@ foxclaw send-media /absolute/path/output.mp4 "说明"
268
268
  它能配置:
269
269
 
270
270
  - 模型:使用服务端默认模型,或选择 app-server 返回的模型。
271
- - reasoning effort:例如 `low`、`medium`、`high`、`xhigh`,取决于模型支持情况。
271
+ - reasoning effort:例如 `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`,取决于模型支持情况。
272
272
  - Fast tier:模型支持时可开关 fast 服务档。
273
273
  - Access:`read-only`、`default`、`full-access`。
274
274
  - Mode:`Agent` 或 `Plan`。
@@ -433,13 +433,13 @@ Candidates: 2
433
433
  [✅ 20|25|personal] [✅]
434
434
  [🔐 —|—|team] [✅]
435
435
  [☑️ 全部] [已启用] [需关注]
436
- [🛡️ Access] [🔑 设备登录]
437
- [🔄 Reload auth]
436
+ [🔑 设备登录]
437
+ [🩺 安全同步]
438
438
  ```
439
439
 
440
440
  右侧 `✅` / `⏸️` 表示当前是否参与自动轮转。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--` 表示该候选还没有额度历史快照。健康摘要会区分正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key、无效 auth 文件和需要登录修复。
441
441
 
442
- **全节点自检并同步** 按钮会通知所有已配置 auth-sync peer 验证本机候选。FoxClaw 会采用并分发最新有效副本;只有完整的多节点无效共识才会标记 `?`;`last_refresh` 已满 8 天的已启用 ChatGPT auth 由发起节点刷新后再分发。存在未回应、忙碌或身份冲突节点时,本轮不会做无效裁决和临期刷新。
442
+ **安全同步** 按钮会通知所有已配置 auth-sync peer 验证候选,采用并分发同账号最新有效副本。只存在于一台机器的无效候选也会交给其他节点做只验证、不导入的复核;没有任何节点验证有效且至少两个节点独立确认无效后才标记 `?`。`last_refresh` 已满 8 天的已启用 ChatGPT auth 由发起节点刷新后再分发。存在未回应、忙碌或身份冲突节点时,本轮不会做临期刷新。
443
443
 
444
444
  当某个候选在实际使用中已经失败,并且 FoxClaw 无法从本机 mirror 或跨节点同步恢复同账号较新凭据时,会标为“需要登录修复”。这类候选不会参与自动轮转和后台主动刷新,也不会出现在“已启用”筛选里。它的按钮会显示 `?`。点击后有两个选择:`登录修复` 会在选中该候选的状态下启动设备码登录;`删除` 会从 canonical 和所有本机 bot runtime 中删除这个候选,并清理它的额度缓存。
445
445
 
@@ -497,7 +497,7 @@ AUTH_AUTO_DELETE_NEEDS_REPAIR=false
497
497
  - `/auth sync events [过滤]`:查看最近同步事件,可按候选名、peer、request id、事件类型、阶段或详情过滤。
498
498
  - `/auth sync trace <requestId>`:查看某个 request id 或事件 id 的最近流水。
499
499
  - `/auth sync test`:发送加密 ping 并等待 peer 返回 pong,确认 peer、共享密钥和 Bot-to-Bot 私聊可用。
500
- - `/auth sync audit`:执行与 `/auth` 面板“全节点自检并同步”按钮相同的验证、协商、无效共识、临期刷新和分发流程。
500
+ - `/auth sync safe`:执行与 `/auth` 面板“安全同步”按钮相同的全节点验证、协商、无效复核、临期刷新和分发流程。`/auth sync audit` 保留为兼容别名。
501
501
  - `/auth sync push all`:手动广播当前节点已验证的全部候选,不刷新 token;“已发送”不等于对端已经导入,需要在 peer 上看 `/auth sync status` 和 `/auth`。
502
502
 
503
503
  命令等价用法:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.77",
4
- "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
3
+ "version": "0.6.3",
4
+ "description": "Foxden local execution claw for controlling Codex and OpenCode from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
7
7
  "bin": {
@@ -55,6 +55,7 @@
55
55
  "prepack": "npm run build"
56
56
  },
57
57
  "dependencies": {
58
+ "@opencode-ai/sdk": "1.18.15",
58
59
  "dotenv": "^16.6.1",
59
60
  "qrcode-terminal": "^0.12.0"
60
61
  },