@foxden-app/foxclaw 0.11.0 → 0.12.0

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 CHANGED
@@ -1,4 +1,5 @@
1
- # Required: one or more Telegram bot tokens for Codex execution.
1
+ # Configure at least one Codex, Antigravity, or DSH Telegram bot below.
2
+ # For DSH-only execution, leave CODEX_BOT_TOKENS empty.
2
3
  # Comma-separated bot tokens. The 1st bot shares the default terminal CODEX_HOME (~/.codex).
3
4
  # Additional bots receive independent isolated homes (~/.foxclaw/codex/telegram/@TelegramUsername/home).
4
5
  CODEX_BOT_TOKENS=<codex_telegram_bot_token>
@@ -7,6 +8,22 @@ CODEX_BOT_TOKENS=<codex_telegram_bot_token>
7
8
  # Comma-separated bot tokens. The 1st bot shares the default Antigravity home (~/.gemini/antigravity-cli).
8
9
  # Additional bots receive independent isolated homes (~/.foxclaw/antigravity/telegram/@TelegramUsername/home).
9
10
  # ANTIGRAVITY_BOT_TOKENS=<antigravity_telegram_bot_token>
11
+
12
+ # Optional DeepSeek Harness backend for existing Codex/Antigravity bots.
13
+ # After enabling it, use /backend dsh in Telegram.
14
+ # DSH_ENABLED=true
15
+ # DSH_SOURCE_DIR=/absolute/path/to/deepseek-harness
16
+ # Alternatively, use an installed CLI or its .js entry point:
17
+ # DSH_CLI_BIN=/absolute/path/to/dsh
18
+ # Optional independent DSH bot; also enables the backend automatically.
19
+ # DSH_BOT_TOKEN=<different_telegram_bot_token>
20
+ # DSH_HOME=/absolute/path/to/.dsh
21
+ # DSH_PROFILE=acp
22
+ # DSH_PATCHES=["/absolute/path/to/patch.yml"]
23
+ # DSH_STARTUP_TIMEOUT_MS=60000
24
+ # Provider credentials are managed by DSH. Environment-based keys can be set here.
25
+ # DEEPSEEK_API_KEY=
26
+ # See docs/dsh.md for setup, permissions, and plugin management.
10
27
 
11
28
  # Required: only this Telegram user can control the bridge
12
29
  TG_ALLOWED_USER_ID=<telegram_user_id>
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
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.12.0 - 2026-10-05
6
+
7
+ ### 中文
8
+
9
+ - 新增 **DeepSeek Harness (DSH) 后端**:通过 DSH 原生 ACP profile 接入,现有 Codex/Antigravity Bot 可使用 `/backend dsh` 切换,也支持 `DSH_BOT_TOKEN` 独立运行。
10
+ - 新增 DSH 专用 Telegram 设置面板:原生模型与推理档位、只读/工作区写入/完全访问、工具审批、持久会话列表与恢复;隐藏 Codex 多账号、认证同步和 Fast tier 控件。
11
+ - 支持 DSH 工具进度、最终回答、图片与 JSON 文件附件、排队、中断接管和新建会话。修复中断后的队列抢跑,以及服务重启时跨 Bot 恢复会话和队列的问题。
12
+ - 新增 `/plugins` 配置查看入口及中英文接入文档;插件安装/移除沿用 DSH 终端命令。状态页明确标注 ACP 尚未提供的逐轮 token 统计,现有统一 Bot 的 `/update` 在 DSH 模式下继续可用。
13
+
14
+ ### English
15
+
16
+ - Added a **DeepSeek Harness (DSH) backend** using the native ACP profile. Existing Codex/Antigravity bots can switch with `/backend dsh`; a dedicated bot can run with `DSH_BOT_TOKEN`.
17
+ - Added DSH-specific Telegram settings for native models and reasoning levels, read-only/workspace-write/full-access presets, tool approvals, and persistent session listing/resume. Codex account, auth-sync, and Fast tier controls are omitted.
18
+ - Added tool progress, final answers, image/JSON attachments, queueing, interruption, takeover, and new sessions. Fixed queue races after interruption and cross-bot session/queue recovery on service restart.
19
+ - Added `/plugins` configuration inspection and bilingual setup documentation; package installation/removal stays in the DSH CLI. Status explicitly marks per-turn token counts as unavailable, and unified bots retain `/update` while using DSH.
20
+
21
+ ## 0.11.1 - 2026-09-30
22
+
23
+ ### 中文
24
+ - **Antigravity 控制器自升级完成通知闭环**:
25
+ - 修复通过 Antigravity 机器人(瓦小安)触发 `/update` 后,服务自升级成功重启却未能轮询并投递升级完成卡片及更新日志的问题。
26
+ - 补齐 `UnifiedBridgeCore` 的自升级状态轮询(`pollSelfUpdateStatus`)与多 Bot 归属检测。
27
+ - **只读观察器(Watcher)会话切换自动解绑**:
28
+ - 修复当前聊天会话切换至新任务后,后台旧任务 Watcher 持续抢占消息卡片导致跨任务串屏与日志污染的问题。
29
+ - `pollWatcher` 检测到当前绑定会话与被观察会话不一致时,自动静默卸载 Watcher。
30
+
31
+ ### English
32
+ - **Antigravity Post-Update Notification Delivery**:
33
+ - Fixed an issue where running `/update` on Antigravity bots (e.g. `@WallianBot`) restarted the service upon successful update but never delivered the completion card and release notes.
34
+ - Implemented `pollSelfUpdateStatus` in `UnifiedBridgeCore` with scope ownership validation.
35
+ - **Watcher Auto-Detachment on Thread Switch**:
36
+ - Fixed an issue where background watchers from other conversations continued hijacking Telegram previews after the chat scope switched to a different task.
37
+ - `pollWatcher` now silently detaches when the active thread binding no longer matches the observed conversation.
38
+
5
39
  ## 0.11.0 - 2026-09-30
6
40
 
7
41
  ### 中文
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **狸爪:一个更适配编程需求的移动端 Codex 控制器。**
6
6
 
7
- FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex、OpenCode 或 Antigravity (AGY),把移动端变成一个真正能用的 web coding 入口。Telegram 或微信负责交互,`codex app-server` / `opencode serve` / `agy` 负责本地执行;你发任务、看进度、批审批、切线程,Agent 在电脑上继续写代码。
7
+ FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex、OpenCode、Antigravity (AGY) 或 DeepSeek Harness (DSH),把移动端变成一个真正能用的 web coding 入口。Telegram 或微信负责交互,各后端负责本地执行;你发任务、看进度、批审批、切线程,Agent 在电脑上继续写代码。DSH 通过 Telegram 接入,支持独立 Bot 或在现有 Bot 中切换。
8
8
 
9
9
  这适合你离开办公桌去吃饭、在旅途中、在跑步机上,或者陪小孩逛公园的时候继续工作。人可以离开电脑,Codex 不必停;它会把关键进展、错误、审批请求和最终结果同步回手机。
10
10
 
@@ -38,12 +38,13 @@ FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex、O
38
38
  - 对 Node、Telegram 机器人、Codex CLI 不太熟?看 [新手安装指南](./docs/zh/install-for-beginners.md)。
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
+ - 想接入 DeepSeek Harness,切换模型、推理档位、权限或查看插件配置?看 [DSH 后端配置](./docs/zh/dsh.md)。
41
42
  - 想了解每个版本改了什么?看 [更新日志](./CHANGELOG.md)。
42
43
  - 维护者准备发版?看 [发布 runbook](./docs/zh/release.md)。
43
44
  - Git、Node、`.env` 都玩得转?直接往下看快速设置。
44
45
  - 卡住了?看 [故障排查](./docs/zh/troubleshooting.md)。
45
46
 
46
- 最低要求:一个或多个 Telegram bot token、你的 Telegram 数字用户 ID、Node.js 24+、一份已登录的 `codex` CLI。首次安装大约 10–20 分钟。新安装请使用 `TG_BOT_TOKENS`;`TG_BOT_TOKEN` 兼容旧的单 runtime 配置,也可在多 bot 模式中标记一个“默认/终端共享”bot。
47
+ 最低要求:一个或多个 Telegram bot token、你的 Telegram 数字用户 ID、Node.js 24+,以及已配置的执行后端。Codex 使用 `CODEX_BOT_TOKENS`;DSH 独立运行使用 `DSH_BOT_TOKEN`,无需安装或登录 Codex。首次安装大约 10–20 分钟。
47
48
 
48
49
  **30 秒体验**:启动 FoxClaw 后,给你的 Telegram 机器人发一句 `List files in DEFAULT_CWD`。Codex 会在本地检查那个目录,然后把结果发回 Telegram。
49
50
 
@@ -79,7 +80,7 @@ foxclaw start
79
80
  跑 `doctor` 或 `start` 之前先把 `.env` 填好。私聊模式最小配置:
80
81
 
81
82
  ```dotenv
82
- TG_BOT_TOKENS=123456:telegram-token
83
+ CODEX_BOT_TOKENS=123456:telegram-token
83
84
  TG_ALLOWED_USER_ID=123456789
84
85
  DEFAULT_CWD=/absolute/path/to/workspace
85
86
  DEFAULT_APPROVAL_POLICY=on-request
package/README_EN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **A mobile Codex controller built for real programming workflows.**
6
6
 
7
- FoxClaw turns your phone into a practical web coding cockpit for local Codex, OpenCode, or Antigravity (AGY). Telegram or Weixin handles the chat interface, `codex app-server` / `opencode serve` / `agy` handles local execution, and you can send tasks, inspect progress, approve actions, switch sessions, and keep working without opening a laptop.
7
+ FoxClaw turns your phone into a practical web coding cockpit for local Codex, OpenCode, Antigravity (AGY), or DeepSeek Harness (DSH). Telegram or Weixin handles the chat interface, each backend handles local execution, and you can send tasks, inspect progress, approve actions, switch sessions, and keep working without opening a laptop. DSH connects through Telegram, using a dedicated bot or backend switching in an existing bot.
8
8
 
9
9
  It is built for the moments when you leave your desk for lunch, commute, travel, use a treadmill, or take the kids to the park. You can step away from the keyboard while Codex keeps coding and sends progress, errors, approval requests, and final results back to your phone.
10
10
 
@@ -38,12 +38,13 @@ FoxClaw is more than message forwarding. It provides Telegram panels for Codex w
38
38
  - New to Node, Telegram bots, or Codex CLI? Use the [Beginner Install Guide](./docs/install-for-beginners.md).
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
+ - Want DeepSeek Harness with native models, reasoning, permissions, and plugin configuration? Read [DSH Backend Setup](./docs/dsh.md).
41
42
  - Want to see what changed in each release? Read the [Changelog](./CHANGELOG.md).
42
43
  - Maintaining a release? Use the [Release Runbook](./docs/release.md).
43
44
  - Already comfortable with Git, Node, and `.env` files? Use the quick setup below.
44
45
  - Something failed? Check [Troubleshooting](./docs/troubleshooting.md).
45
46
 
46
- The minimum install needs one or more Telegram bot tokens, your numeric Telegram user id, Node.js 24+, and a logged-in `codex` CLI. A first install usually takes 10–20 minutes. New installs should use `TG_BOT_TOKENS`; `TG_BOT_TOKEN` remains compatible with legacy single-runtime setups and can mark one multi-bot runtime as default/shared-terminal.
47
+ The minimum install needs one or more Telegram bot tokens, your numeric Telegram user id, Node.js 24+, and a configured execution backend. Codex uses `CODEX_BOT_TOKENS`; standalone DSH uses `DSH_BOT_TOKEN` and does not require Codex installation or login. A first install usually takes 10–20 minutes.
47
48
 
48
49
  **30-second demo**: after FoxClaw is running, send `List files in DEFAULT_CWD` to your Telegram bot. FoxClaw asks local Codex to inspect that folder on your computer and sends the answer back to Telegram.
49
50
 
@@ -79,7 +80,7 @@ foxclaw start
79
80
  Fill `.env` before running `doctor` or `start`. Minimum private-chat config:
80
81
 
81
82
  ```dotenv
82
- TG_BOT_TOKENS=123456:telegram-token
83
+ CODEX_BOT_TOKENS=123456:telegram-token
83
84
  TG_ALLOWED_USER_ID=123456789
84
85
  DEFAULT_CWD=/absolute/path/to/workspace
85
86
  DEFAULT_APPROVAL_POLICY=on-request
@@ -17,6 +17,7 @@ export interface UnifiedBridgeRuntimeStatus {
17
17
  running: boolean;
18
18
  connected: boolean;
19
19
  activeTurns: number;
20
+ dshPendingApprovals: number;
20
21
  botUsername: string | null;
21
22
  codexHome: string;
22
23
  codexAppServer?: CodexAppServerRuntimeStatus | undefined;
@@ -53,6 +54,7 @@ export declare class UnifiedBridgeCore {
53
54
  private readonly defaultBackendId;
54
55
  private readonly selfUpdater?;
55
56
  private readonly orchestrator;
57
+ private readonly dshUi?;
56
58
  private readonly watchers;
57
59
  private readonly pendingAgyRenames;
58
60
  private readonly stalePanelDeleteTimers;
@@ -65,6 +67,11 @@ export declare class UnifiedBridgeCore {
65
67
  startTelegramPolling(): Promise<void>;
66
68
  start(): Promise<void>;
67
69
  stop(): Promise<void>;
70
+ private selfUpdatePollTimer;
71
+ private scheduleSelfUpdateStatusPoll;
72
+ private clearSelfUpdateStatusPoll;
73
+ private ownsScope;
74
+ private pollSelfUpdateStatus;
68
75
  getRuntimeStatus(): UnifiedBridgeRuntimeStatus;
69
76
  isIdleForServiceUpdate(): boolean;
70
77
  getCurrentAuthLabel(): Promise<string | null>;
@@ -15,8 +15,9 @@ import { CodexEngineAdapter } from '../codex_app/adapter.js';
15
15
  import { OpencodeEngineAdapter } from '../opencode/adapter.js';
16
16
  import { BridgeSessionCore } from '../controller/controller.js';
17
17
  import { BridgeMessagingRouter } from '../channels/bridge_messaging_router.js';
18
- import { BRIDGE_SCOPE_WEIXIN_PREFIX } from '../core/bridge_scope.js';
18
+ import { BRIDGE_SCOPE_WEIXIN_PREFIX, BRIDGE_SCOPE_TELEGRAM_PREFIX, parseTelegramTargetFromBridgeScope } from '../core/bridge_scope.js';
19
19
  import { syncCodexLocalUsageToStore } from '../store/token_usage.js';
20
+ import { DshUi } from '../dsh/ui.js';
20
21
  function formatCandidateButtonPrefix(c) {
21
22
  const p5h = typeof c.quota?.fiveHourPercent === 'number' ? `${c.quota.fiveHourPercent}%` : '—';
22
23
  const pw = typeof c.quota?.weeklyPercent === 'number' ? `${c.quota.weeklyPercent}%` : '—';
@@ -67,6 +68,7 @@ export class UnifiedBridgeCore {
67
68
  defaultBackendId;
68
69
  selfUpdater;
69
70
  orchestrator;
71
+ dshUi;
70
72
  watchers = new Map();
71
73
  pendingAgyRenames = new Map();
72
74
  stalePanelDeleteTimers = new Map();
@@ -158,7 +160,13 @@ export class UnifiedBridgeCore {
158
160
  isDefault: isOpencodeDefault,
159
161
  });
160
162
  }
163
+ if (config.dsh) {
164
+ this.dshUi = new DshUi(config, store, logger, this.messaging);
165
+ initialBackends.push({ id: 'dsh', name: 'DeepSeek Harness (DSH)', engineType: 'dsh', adapter: this.dshUi.adapter });
166
+ }
161
167
  const allAdapters = [this.adapter];
168
+ if (this.dshUi)
169
+ allAdapters.push(this.dshUi.adapter);
162
170
  if (this.codexAdapter)
163
171
  allAdapters.push(this.codexAdapter);
164
172
  if (this.opencodeAdapter)
@@ -172,9 +180,27 @@ export class UnifiedBridgeCore {
172
180
  adapters: allAdapters,
173
181
  backends: initialBackends,
174
182
  defaultBackendId: this.defaultBackendId,
183
+ ownsScope: scopeId => {
184
+ if (!scopeId.startsWith(BRIDGE_SCOPE_TELEGRAM_PREFIX))
185
+ return !config.tgMultiBotMode && !scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX);
186
+ const target = parseTelegramTargetFromBridgeScope(scopeId);
187
+ return target.botId ? target.botId === this.bot.identity : !config.tgMultiBotMode;
188
+ },
175
189
  backendProvider: () => this.getBackendDescriptors(),
176
190
  messaging: this.messaging,
177
191
  customUi: {
192
+ renderSetupMenu: async (scopeId, locale, messageId) => {
193
+ if (this.orchestrator.getBackendDescriptorForScope(scopeId).engineType !== 'dsh' || !this.dshUi)
194
+ return false;
195
+ await this.dshUi.setup(scopeId, locale, this.orchestrator, messageId);
196
+ return true;
197
+ },
198
+ renderModelsMenu: async (scopeId, locale, messageId) => {
199
+ if (this.orchestrator.getBackendDescriptorForScope(scopeId).engineType !== 'dsh' || !this.dshUi)
200
+ return false;
201
+ await this.dshUi.models(scopeId, locale, this.orchestrator, messageId);
202
+ return true;
203
+ },
178
204
  renderCustomStatus: (scopeId, locale) => this.renderCustomStatus(scopeId, locale),
179
205
  renderCustomSetupRows: (scopeId, locale) => this.renderCustomSetupRows(scopeId, locale),
180
206
  handleCustomCallback: (scopeId, data, locale, messageId, event) => this.handleCustomCallback(scopeId, data, locale, messageId, event),
@@ -185,6 +211,8 @@ export class UnifiedBridgeCore {
185
211
  }
186
212
  async getBackendDescriptors() {
187
213
  const list = [];
214
+ if (this.dshUi)
215
+ list.push({ id: 'dsh', name: 'DeepSeek Harness (DSH)', engineType: 'dsh', adapter: this.dshUi.adapter });
188
216
  const isCodexDefault = this.defaultBackendId === 'codex';
189
217
  const active = await this.auth.getActiveAccount();
190
218
  const activeQuota = active?.quota
@@ -291,9 +319,14 @@ export class UnifiedBridgeCore {
291
319
  catch (err) {
292
320
  this.logger.warn('antigravity.restore_watchers_error', { error: String(err) });
293
321
  }
322
+ if (this.selfUpdater) {
323
+ this.scheduleSelfUpdateStatusPoll(500);
324
+ }
294
325
  }
295
326
  async stop() {
327
+ this.clearSelfUpdateStatusPoll();
296
328
  await this.orchestrator.stop();
329
+ await this.dshUi?.stop();
297
330
  this.auth.stopKeepAlive();
298
331
  for (const [scopeId, watcher] of this.watchers.entries()) {
299
332
  watcher.stopped = true;
@@ -302,6 +335,73 @@ export class UnifiedBridgeCore {
302
335
  this.watchers.delete(scopeId);
303
336
  }
304
337
  }
338
+ selfUpdatePollTimer = null;
339
+ scheduleSelfUpdateStatusPoll(delay = 1000) {
340
+ if (!this.selfUpdater || this.selfUpdatePollTimer) {
341
+ return;
342
+ }
343
+ this.selfUpdatePollTimer = setTimeout(() => {
344
+ this.selfUpdatePollTimer = null;
345
+ void this.pollSelfUpdateStatus().catch((error) => {
346
+ this.logger.error('antigravity.self_update_poll_failed', { error: String(error) });
347
+ this.scheduleSelfUpdateStatusPoll();
348
+ });
349
+ }, delay);
350
+ this.selfUpdatePollTimer.unref?.();
351
+ }
352
+ clearSelfUpdateStatusPoll() {
353
+ if (!this.selfUpdatePollTimer)
354
+ return;
355
+ clearTimeout(this.selfUpdatePollTimer);
356
+ this.selfUpdatePollTimer = null;
357
+ }
358
+ ownsScope(scopeId) {
359
+ if (this.bot.identity) {
360
+ return scopeId.includes(`:${this.bot.identity}:`);
361
+ }
362
+ return !scopeId.includes(':bot');
363
+ }
364
+ async pollSelfUpdateStatus() {
365
+ const status = await this.selfUpdater?.readStatus();
366
+ if (!status)
367
+ return;
368
+ if (status.state === 'pending') {
369
+ this.scheduleSelfUpdateStatusPoll();
370
+ return;
371
+ }
372
+ if (status.scopeId.startsWith('cluster:')) {
373
+ await this.selfUpdater?.clearStatus();
374
+ return;
375
+ }
376
+ if (!this.ownsScope(status.scopeId)) {
377
+ this.scheduleSelfUpdateStatusPoll();
378
+ return;
379
+ }
380
+ const isZh = status.locale === 'zh';
381
+ if (status.state === 'succeeded') {
382
+ const notes = status.releaseNotes?.filter((n) => n.trim()) ?? [];
383
+ const title = isZh ? '🎉 FoxClaw 全链路升级完成' : '🎉 FoxClaw Update Completed';
384
+ const lines = [
385
+ `**${title}**`,
386
+ `• FoxClaw: \`${status.fromVersion}\` ➔ \`${status.toVersion ?? '0.11.0'}\``,
387
+ `• Codex CLI: \`${status.codexFromVersion ?? '0.157.1'}\` ➔ \`${status.codexToVersion ?? '0.159.2'}\``,
388
+ `• Antigravity CLI: \`${status.agyFromVersion ?? '1.2.14'}\` (最新)`,
389
+ ];
390
+ if (notes.length > 0) {
391
+ lines.push('', isZh ? '**更新日志**:' : '**Release Notes**:');
392
+ for (const note of notes) {
393
+ lines.push(`• ${note}`);
394
+ }
395
+ }
396
+ lines.push('', isZh ? '服务已自动重载并恢复就绪。' : 'Service reloaded and ready.');
397
+ await this.sendMessage(status.scopeId, lines.join('\n')).catch(() => { });
398
+ }
399
+ else {
400
+ const errorMsg = status.error ?? (isZh ? '未知错误' : 'Unknown error');
401
+ await this.sendMessage(status.scopeId, `❌ **FoxClaw 升级失败**: ${errorMsg}`).catch(() => { });
402
+ }
403
+ await this.selfUpdater?.clearStatus();
404
+ }
305
405
  getRuntimeStatus() {
306
406
  const isConnected = this.codexApp && typeof this.codexApp.isConnected === 'function'
307
407
  ? this.codexApp.isConnected()
@@ -310,6 +410,7 @@ export class UnifiedBridgeCore {
310
410
  running: true,
311
411
  connected: isConnected,
312
412
  activeTurns: this.orchestrator.getActiveTurnsCount(),
413
+ dshPendingApprovals: this.dshUi?.pendingApprovals ?? 0,
313
414
  botUsername: this.bot.username,
314
415
  codexHome: this.config.codexHome ?? this.config.codexAuthDir ?? path.join(os.homedir(), '.codex'),
315
416
  ...(this.codexApp && typeof this.codexApp.getServerStatus === 'function' && this.codexApp.getServerStatus()
@@ -389,6 +490,8 @@ export class UnifiedBridgeCore {
389
490
  }
390
491
  async renderCustomStatus(scopeId, locale) {
391
492
  const activeBackend = this.orchestrator.getBackendDescriptorForScope(scopeId);
493
+ if (activeBackend.engineType === 'dsh' && this.dshUi)
494
+ return this.dshUi.status(scopeId, locale);
392
495
  if (activeBackend.engineType === 'codex') {
393
496
  const codexHome = this.config.codexAuthDir ?? this.config.codexHome ?? path.join(os.homedir(), '.codex');
394
497
  let currentCodexAccount = 'default';
@@ -496,6 +599,8 @@ export class UnifiedBridgeCore {
496
599
  }
497
600
  async handleCustomInbound(event, locale) {
498
601
  const scopeId = event.scopeId;
602
+ if (this.orchestrator.getBackendDescriptorForScope(scopeId).engineType === 'dsh')
603
+ return false;
499
604
  // Check for JSON token upload / document attachment
500
605
  const doc = event.attachments?.find((a) => a.kind === 'document');
501
606
  if (doc && (doc.fileName?.endsWith('.json') || doc.mimeType?.includes('json'))) {
@@ -654,6 +759,8 @@ export class UnifiedBridgeCore {
654
759
  : '⚠️ Self-updater is not available in current runtime. Run `foxclaw update` in terminal.');
655
760
  return true;
656
761
  }
762
+ if (activeBackend.engineType === 'dsh' && this.dshUi)
763
+ return this.dshUi.command(scopeId, cmd, args, locale, this.orchestrator);
657
764
  if (isCodex) {
658
765
  if (this.codexCore) {
659
766
  switch (cmd.toLowerCase()) {
@@ -765,6 +872,16 @@ export class UnifiedBridgeCore {
765
872
  }
766
873
  }
767
874
  async handleCustomCallback(scopeId, data, locale, messageId, event) {
875
+ if (this.dshUi && await this.dshUi.callback(scopeId, data, locale, this.orchestrator, event))
876
+ return true;
877
+ if (this.orchestrator.getBackendDescriptorForScope(scopeId).engineType === 'dsh') {
878
+ if (!['engine:setup:main', 'engine:setup:active_mode', 'engine:setup:new', 'engine:setup:backend'].includes(data) && !data.startsWith('engine:backend:')) {
879
+ if (event)
880
+ await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? '请重新打开 DSH 设置面板' : 'Open the DSH setup panel');
881
+ return true;
882
+ }
883
+ return false;
884
+ }
768
885
  if (this.codexCore && (data.startsWith('thread:') ||
769
886
  data.startsWith('auth:') ||
770
887
  data.startsWith('settings:access:') ||
@@ -1529,6 +1646,11 @@ export class UnifiedBridgeCore {
1529
1646
  async pollWatcher(watcher) {
1530
1647
  if (watcher.stopped)
1531
1648
  return;
1649
+ const currentBinding = this.store.getBinding(watcher.scopeId);
1650
+ if (currentBinding?.threadId && currentBinding.threadId !== watcher.conversationId) {
1651
+ await this.unwatchConversation(watcher.scopeId, this.getScopeLocale(watcher.scopeId), false);
1652
+ return;
1653
+ }
1532
1654
  if (this.orchestrator.hasActiveTurn(watcher.scopeId)) {
1533
1655
  try {
1534
1656
  if (fs.existsSync(watcher.transcriptPath)) {
package/dist/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { LogLevel } from './logger.js';
2
2
  import type { ApprovalPolicyValue, SandboxModeValue } from './types.js';
3
+ import type { DshClientOptions } from './dsh/client.js';
3
4
  export declare const APP_HOME: string;
4
5
  export declare const DEFAULT_STORE_PATH: string;
5
6
  export declare const DEFAULT_STATUS_PATH: string;
@@ -19,6 +20,8 @@ export declare function resolveEnvPath(): string;
19
20
  export declare function getLoadedEnvPath(): string | null;
20
21
  export declare function loadEnv(): void;
21
22
  export interface AppConfig {
23
+ dsh?: DshClientOptions;
24
+ dshBotToken?: string | null;
22
25
  codexBotTokens: string[];
23
26
  antigravityBotTokens: string[];
24
27
  tgBotToken: string;
@@ -112,6 +115,7 @@ export interface CodexApiProviderConfig {
112
115
  export declare function loadConfig(): AppConfig;
113
116
  export declare function buildCodexApiProviderOverrides(providers: readonly CodexApiProviderConfig[], defaultProviderId?: string | null): string[];
114
117
  export declare function validateCodexBotTokens(tokens: readonly string[]): void;
118
+ export declare function parseDshPatches(value?: string): string[];
115
119
  export declare function validateOpencodeBotToken(token: string | null, codexTokens: readonly string[]): void;
116
120
  export declare function validateAntigravityBotTokens(tokens: readonly string[], codexTokens: readonly string[], opencodeToken: string | null): void;
117
121
  export declare function validateAntigravityBotToken(token: string | null, codexTokens: readonly string[], opencodeToken: string | null): void;
package/dist/config.js CHANGED
@@ -50,8 +50,12 @@ export function loadConfig() {
50
50
  const codexBotTokens = parseCommaSeparatedIds(process.env.CODEX_BOT_TOKENS || process.env.CODEX_BOT_TOKEN);
51
51
  const antigravityBotTokens = parseCommaSeparatedIds(process.env.ANTIGRAVITY_BOT_TOKENS || process.env.ANTIGRAVITY_BOT_TOKEN);
52
52
  const opencodeBotToken = optional('OPENCODE_BOT_TOKEN');
53
- if (codexBotTokens.length === 0 && antigravityBotTokens.length === 0) {
54
- throw new Error('At least one Telegram bot token must be configured via CODEX_BOT_TOKENS or ANTIGRAVITY_BOT_TOKENS');
53
+ const dshBotToken = optional('DSH_BOT_TOKEN');
54
+ if (dshBotToken && [...codexBotTokens, ...antigravityBotTokens, opencodeBotToken].includes(dshBotToken)) {
55
+ throw new Error('DSH_BOT_TOKEN must use a different bot from Codex, Antigravity, and OpenCode');
56
+ }
57
+ if (codexBotTokens.length === 0 && antigravityBotTokens.length === 0 && !dshBotToken) {
58
+ throw new Error('At least one Telegram bot token must be configured via CODEX_BOT_TOKENS, ANTIGRAVITY_BOT_TOKENS, or DSH_BOT_TOKEN');
55
59
  }
56
60
  validateCodexBotTokens(codexBotTokens);
57
61
  validateAntigravityBotTokens(antigravityBotTokens, codexBotTokens, opencodeBotToken);
@@ -60,11 +64,23 @@ export function loadConfig() {
60
64
  const antigravityDefaultRuntimeBotToken = selectDefaultRuntimeBotToken(antigravityBotTokens, optional('ANTIGRAVITY_DEFAULT_RUNTIME_BOT_TOKEN')) ||
61
65
  antigravityBotTokens[0] ||
62
66
  null;
63
- const totalBots = codexBotTokens.length + antigravityBotTokens.length;
64
- const tgBotTokens = codexBotTokens.length > 0 ? codexBotTokens : antigravityBotTokens;
65
- const tgBotToken = (codexBotTokens[0] || antigravityBotTokens[0]);
67
+ const totalBots = codexBotTokens.length + antigravityBotTokens.length + (dshBotToken ? 1 : 0);
68
+ const tgBotTokens = codexBotTokens.length > 0 ? codexBotTokens : antigravityBotTokens.length > 0 ? antigravityBotTokens : dshBotToken ? [dshBotToken] : [];
69
+ const tgBotToken = (codexBotTokens[0] || antigravityBotTokens[0] || dshBotToken);
66
70
  const antigravityBotToken = antigravityBotTokens[0] || null;
67
71
  const config = {
72
+ dshBotToken,
73
+ ...(boolEnv('DSH_ENABLED', Boolean(dshBotToken || optional('DSH_SOURCE_DIR') || optional('DSH_CLI_BIN'))) ? {
74
+ dsh: {
75
+ cliBin: optional('DSH_CLI_BIN') || resolveCommand('dsh') || 'dsh',
76
+ sourceDir: optional('DSH_SOURCE_DIR'),
77
+ home: optional('DSH_HOME'),
78
+ profile: optional('DSH_PROFILE') || 'acp',
79
+ patches: parseDshPatches(process.env.DSH_PATCHES),
80
+ runtimeDir: path.join(APP_HOME, 'runtime', 'dsh'),
81
+ startupTimeoutMs: Number(optional('DSH_STARTUP_TIMEOUT_MS') || 60000),
82
+ },
83
+ } : {}),
68
84
  codexBotTokens,
69
85
  antigravityBotTokens,
70
86
  tgBotToken,
@@ -142,6 +158,12 @@ export function loadConfig() {
142
158
  voiceTextLimit: intEnv('VOICE_TEXT_LIMIT', 2800),
143
159
  voiceTtsTimeoutMs: intEnv('VOICE_TTS_TIMEOUT_MS', 300_000),
144
160
  };
161
+ if (dshBotToken && !config.dsh)
162
+ throw new Error('DSH_BOT_TOKEN requires DSH_ENABLED=true');
163
+ if (config.dsh && (!Number.isSafeInteger(config.dsh.startupTimeoutMs) || config.dsh.startupTimeoutMs <= 0))
164
+ throw new Error('DSH_STARTUP_TIMEOUT_MS must be a positive integer');
165
+ if (config.wxEnabled && codexBotTokens.length === 0 && antigravityBotTokens.length === 0)
166
+ throw new Error('WX_ENABLED currently requires a Codex or Antigravity runtime alongside DSH');
145
167
  ensureAppDirs(config);
146
168
  return config;
147
169
  }
@@ -176,6 +198,20 @@ export function validateCodexBotTokens(tokens) {
176
198
  seen.add(token);
177
199
  }
178
200
  }
201
+ export function parseDshPatches(value) {
202
+ if (!value?.trim())
203
+ return [];
204
+ let parsed;
205
+ try {
206
+ parsed = JSON.parse(value);
207
+ }
208
+ catch {
209
+ throw new Error('DSH_PATCHES must be a JSON array of patch paths');
210
+ }
211
+ if (!Array.isArray(parsed) || !parsed.every(item => typeof item === 'string' && item.trim().length > 0))
212
+ throw new Error('DSH_PATCHES must be a JSON array of patch paths');
213
+ return parsed.map(item => path.resolve(item));
214
+ }
179
215
  export function validateOpencodeBotToken(token, codexTokens) {
180
216
  if (!token)
181
217
  return;
@@ -60,7 +60,7 @@ export interface EngineTurnErrorContext {
60
60
  export interface IEngineAdapter {
61
61
  readonly id: string;
62
62
  readonly name: string;
63
- listModels(): Promise<EngineModel[]>;
63
+ listModels(scopeId?: string): Promise<EngineModel[]>;
64
64
  executeTurn(request: EngineTurnRequest): EngineTurnExecution;
65
65
  preflightTurn?(request: EngineTurnRequest): Promise<void>;
66
66
  handleTurnError?(context: EngineTurnErrorContext): Promise<boolean>;
@@ -21,9 +21,12 @@ export interface UnifiedActiveTurn {
21
21
  stepIndex?: number;
22
22
  toolCount: number;
23
23
  currentTool?: string | null;
24
+ suppressQueueDrain?: boolean;
24
25
  startTime: number;
25
26
  }
26
27
  export interface EngineCustomUiHook {
28
+ renderSetupMenu?(scopeId: string, locale: AppLocale, messageId?: number): Promise<boolean>;
29
+ renderModelsMenu?(scopeId: string, locale: AppLocale, messageId?: number): Promise<boolean>;
27
30
  renderCustomStatus?(scopeId: string, locale: AppLocale): Promise<string | null>;
28
31
  renderCustomSetupRows?(scopeId: string, locale: AppLocale): Promise<InlineKeyboard>;
29
32
  handleCustomCallback?(scopeId: string, data: string, locale: AppLocale, messageId?: number, event?: TelegramCallbackEvent): Promise<boolean>;
@@ -42,6 +45,8 @@ export declare class UnifiedChannelOrchestrator {
42
45
  private readonly defaultBackendId;
43
46
  private readonly activeTurns;
44
47
  private readonly stalePanelDeleteTimers;
48
+ private readonly recoveryTimers;
49
+ private readonly ownsScope;
45
50
  private readonly backendProvider?;
46
51
  get adapter(): IEngineAdapter;
47
52
  constructor(options: {
@@ -54,6 +59,7 @@ export declare class UnifiedChannelOrchestrator {
54
59
  backends?: BackendDescriptor[];
55
60
  backendProvider?: (() => Promise<BackendDescriptor[]> | BackendDescriptor[]) | undefined;
56
61
  defaultBackendId?: string;
62
+ ownsScope?: (scopeId: string) => boolean;
57
63
  messaging: TelegramMessagingPort;
58
64
  customUi?: EngineCustomUiHook | undefined;
59
65
  });