@foxden-app/foxclaw 0.12.0 → 0.13.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/antigravity/account_ui.d.ts +34 -0
  3. package/dist/antigravity/account_ui.js +354 -0
  4. package/dist/antigravity/adapter.js +1 -1
  5. package/dist/antigravity/auth.d.ts +4 -1
  6. package/dist/antigravity/auth.js +17 -5
  7. package/dist/antigravity/controller.d.ts +3 -108
  8. package/dist/antigravity/controller.js +2 -2129
  9. package/dist/antigravity/conversation_ui.d.ts +56 -0
  10. package/dist/antigravity/conversation_ui.js +509 -0
  11. package/dist/antigravity/ui_callbacks.d.ts +6 -0
  12. package/dist/antigravity/ui_callbacks.js +6 -0
  13. package/dist/bridge/unified_bridge.d.ts +114 -0
  14. package/dist/bridge/unified_bridge.js +1343 -0
  15. package/dist/channels/bridge_messaging_router.d.ts +8 -4
  16. package/dist/channels/bridge_messaging_router.js +15 -4
  17. package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
  18. package/dist/channels/telegram/telegram_messaging_port.js +22 -2
  19. package/dist/channels/text_actions.d.ts +14 -0
  20. package/dist/channels/text_actions.js +48 -0
  21. package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
  22. package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
  23. package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
  24. package/dist/channels/weixin/weixin_messaging_port.js +34 -7
  25. package/dist/codex_app/activity.d.ts +62 -0
  26. package/dist/codex_app/activity.js +418 -0
  27. package/dist/codex_app/adapter.d.ts +2 -2
  28. package/dist/codex_app/adapter.js +55 -25
  29. package/dist/codex_app/backend_ui.d.ts +4 -0
  30. package/dist/codex_app/backend_ui.js +48 -0
  31. package/dist/codex_app/client.js +0 -12
  32. package/dist/controller/access.d.ts +1 -11
  33. package/dist/controller/access.js +1 -33
  34. package/dist/controller/activity.d.ts +1 -62
  35. package/dist/controller/activity.js +2 -418
  36. package/dist/controller/auth_errors.d.ts +11 -0
  37. package/dist/controller/auth_errors.js +124 -0
  38. package/dist/controller/auth_files.d.ts +13 -0
  39. package/dist/controller/auth_files.js +156 -0
  40. package/dist/controller/auth_presentation.d.ts +77 -0
  41. package/dist/controller/auth_presentation.js +697 -0
  42. package/dist/controller/auth_quota_service.d.ts +27 -0
  43. package/dist/controller/auth_quota_service.js +151 -0
  44. package/dist/controller/controller.d.ts +30 -56
  45. package/dist/controller/controller.js +181 -3497
  46. package/dist/controller/interactions.d.ts +74 -0
  47. package/dist/controller/interactions.js +560 -0
  48. package/dist/controller/local_usage_service.d.ts +19 -0
  49. package/dist/controller/local_usage_service.js +62 -0
  50. package/dist/controller/native_panel_service.d.ts +55 -0
  51. package/dist/controller/native_panel_service.js +214 -0
  52. package/dist/controller/native_panels.d.ts +61 -0
  53. package/dist/controller/native_panels.js +784 -0
  54. package/dist/controller/shared_helpers.d.ts +53 -0
  55. package/dist/controller/shared_helpers.js +399 -0
  56. package/dist/controller/state_types.d.ts +390 -0
  57. package/dist/controller/state_types.js +82 -0
  58. package/dist/controller/turn_rendering.d.ts +22 -0
  59. package/dist/controller/turn_rendering.js +288 -0
  60. package/dist/core/access.d.ts +11 -0
  61. package/dist/core/access.js +33 -0
  62. package/dist/core/attachment_files.d.ts +13 -0
  63. package/dist/core/attachment_files.js +180 -0
  64. package/dist/core/attachment_types.d.ts +22 -0
  65. package/dist/core/attachment_types.js +1 -0
  66. package/dist/core/attachments.d.ts +5 -3
  67. package/dist/core/attachments.js +2 -2
  68. package/dist/core/backend_registry.d.ts +22 -0
  69. package/dist/core/backend_registry.js +50 -0
  70. package/dist/core/backend_ui.d.ts +28 -0
  71. package/dist/core/backend_ui.js +1 -0
  72. package/dist/core/channel_events.d.ts +48 -0
  73. package/dist/core/channel_events.js +1 -0
  74. package/dist/core/channel_port.d.ts +52 -8
  75. package/dist/core/engine_spi.d.ts +19 -4
  76. package/dist/core/listener_scope.d.ts +11 -0
  77. package/dist/core/listener_scope.js +10 -0
  78. package/dist/core/message_text.d.ts +2 -0
  79. package/dist/core/message_text.js +28 -0
  80. package/dist/core/network_errors.d.ts +1 -0
  81. package/dist/core/network_errors.js +23 -0
  82. package/dist/core/orchestrator.d.ts +53 -24
  83. package/dist/core/orchestrator.js +855 -507
  84. package/dist/core/queued_engine_input.d.ts +12 -0
  85. package/dist/core/queued_engine_input.js +16 -0
  86. package/dist/core/runtime_supervisor.d.ts +19 -0
  87. package/dist/core/runtime_supervisor.js +45 -0
  88. package/dist/core/scope_operations.d.ts +7 -0
  89. package/dist/core/scope_operations.js +19 -0
  90. package/dist/core/stream_preview.js +21 -9
  91. package/dist/dsh/adapter.js +18 -5
  92. package/dist/dsh/backend.d.ts +7 -0
  93. package/dist/dsh/backend.js +21 -0
  94. package/dist/dsh/runtime.d.ts +0 -1
  95. package/dist/dsh/runtime.js +4 -13
  96. package/dist/dsh/ui.d.ts +8 -8
  97. package/dist/dsh/ui.js +1 -1
  98. package/dist/main.js +55 -43
  99. package/dist/opencode/adapter.js +28 -9
  100. package/dist/opencode/controller.js +1 -1
  101. package/dist/store/channel_inbox.d.ts +14 -0
  102. package/dist/store/channel_inbox.js +21 -0
  103. package/dist/store/database.d.ts +7 -3
  104. package/dist/store/database.js +7 -2
  105. package/dist/store/task_journal.d.ts +32 -0
  106. package/dist/store/task_journal.js +70 -0
  107. package/dist/telegram/api.d.ts +2 -2
  108. package/dist/telegram/api.js +10 -28
  109. package/dist/telegram/gateway.d.ts +13 -27
  110. package/dist/telegram/gateway.js +86 -43
  111. package/dist/telegram/media.d.ts +2 -34
  112. package/dist/telegram/media.js +1 -180
  113. package/dist/telegram/task_preview.d.ts +13 -0
  114. package/dist/telegram/task_preview.js +92 -0
  115. package/dist/telegram/task_result.d.ts +2 -0
  116. package/dist/telegram/task_result.js +12 -0
  117. package/dist/telegram/text.d.ts +1 -1
  118. package/dist/telegram/text.js +2 -27
  119. package/dist/types.d.ts +1 -1
  120. package/docs/release.md +13 -11
  121. package/docs/zh/architecture-review.md +106 -0
  122. package/docs/zh/release.md +13 -11
  123. package/docs/zh/telegram-experience.md +40 -0
  124. package/package.json +5 -4
@@ -0,0 +1,12 @@
1
+ import type { StagedAttachment } from './attachment_types.js';
2
+ export interface QueuedEngineInput {
3
+ version: 1;
4
+ prompt: string;
5
+ backendId: string;
6
+ cwd: string;
7
+ stagedAttachments: StagedAttachment[];
8
+ }
9
+ export declare function decodeQueuedEngineInput(inputJson: string, fallbackPrompt: string): QueuedEngineInput | {
10
+ prompt: string;
11
+ stagedAttachments: StagedAttachment[];
12
+ };
@@ -0,0 +1,16 @@
1
+ export function decodeQueuedEngineInput(inputJson, fallbackPrompt) {
2
+ const input = JSON.parse(inputJson);
3
+ // Legacy queues contained only a Codex-shaped text input array.
4
+ if (Array.isArray(input)) {
5
+ const text = input[0]?.text;
6
+ return { prompt: typeof text === 'string' ? text : fallbackPrompt, stagedAttachments: [] };
7
+ }
8
+ if (typeof input !== 'object' || input === null || !('version' in input) || input.version !== 1 ||
9
+ !('prompt' in input) || typeof input.prompt !== 'string' ||
10
+ !('backendId' in input) || typeof input.backendId !== 'string' ||
11
+ !('cwd' in input) || typeof input.cwd !== 'string' ||
12
+ !('stagedAttachments' in input) || !Array.isArray(input.stagedAttachments)) {
13
+ throw new Error('Invalid queued engine input');
14
+ }
15
+ return input;
16
+ }
@@ -0,0 +1,19 @@
1
+ export interface ManagedRuntime {
2
+ start?(): void | Promise<void>;
3
+ stop(): void | Promise<void>;
4
+ }
5
+ export interface ShutdownFailure {
6
+ name: string;
7
+ error: unknown;
8
+ }
9
+ /** Owns resources from allocation onward, including partially started runtimes. */
10
+ export declare class RuntimeSupervisor {
11
+ private readonly entries;
12
+ private readonly identities;
13
+ private closing;
14
+ private shutdown;
15
+ register(name: string, resource: ManagedRuntime): void;
16
+ start(name: string): Promise<void>;
17
+ stop(): Promise<ShutdownFailure[]>;
18
+ private stopAll;
19
+ }
@@ -0,0 +1,45 @@
1
+ /** Owns resources from allocation onward, including partially started runtimes. */
2
+ export class RuntimeSupervisor {
3
+ entries = [];
4
+ identities = new Set();
5
+ closing = false;
6
+ shutdown = null;
7
+ register(name, resource) {
8
+ if (this.closing)
9
+ throw new Error('Runtime supervisor is stopping');
10
+ if (this.identities.has(resource))
11
+ return;
12
+ if (this.entries.some(entry => entry.name === name))
13
+ throw new Error(`Runtime already registered: ${name}`);
14
+ this.identities.add(resource);
15
+ this.entries.push({ name, resource, start: null });
16
+ }
17
+ start(name) {
18
+ if (this.closing)
19
+ return Promise.reject(new Error('Runtime supervisor is stopping'));
20
+ const entry = this.entries.find(entry => entry.name === name);
21
+ if (!entry)
22
+ return Promise.reject(new Error(`Unknown runtime: ${name}`));
23
+ entry.start ??= Promise.resolve().then(() => entry.resource.start?.());
24
+ return entry.start;
25
+ }
26
+ stop() {
27
+ this.closing = true;
28
+ this.shutdown ??= this.stopAll();
29
+ return this.shutdown;
30
+ }
31
+ async stopAll() {
32
+ const failures = [];
33
+ // Consumers are registered after their dependencies and stopped before them.
34
+ for (const entry of [...this.entries].reverse()) {
35
+ await entry.start?.catch(() => { });
36
+ try {
37
+ await entry.resource.stop();
38
+ }
39
+ catch (error) {
40
+ failures.push({ name: entry.name, error });
41
+ }
42
+ }
43
+ return failures;
44
+ }
45
+ }
@@ -0,0 +1,7 @@
1
+ /** Serialize state changes for one conversation; unrelated scopes proceed independently. */
2
+ export declare class ScopeOperations {
3
+ private readonly tails;
4
+ run<T>(scopeId: string, operation: () => Promise<T>): Promise<T>;
5
+ isIdle(): boolean;
6
+ idle(): Promise<void>;
7
+ }
@@ -0,0 +1,19 @@
1
+ /** Serialize state changes for one conversation; unrelated scopes proceed independently. */
2
+ export class ScopeOperations {
3
+ tails = new Map();
4
+ run(scopeId, operation) {
5
+ const previous = this.tails.get(scopeId) ?? Promise.resolve();
6
+ const next = previous.catch(() => { }).then(operation);
7
+ this.tails.set(scopeId, next);
8
+ void next.finally(() => {
9
+ if (this.tails.get(scopeId) === next)
10
+ this.tails.delete(scopeId);
11
+ }).catch(() => { });
12
+ return next;
13
+ }
14
+ isIdle() { return this.tails.size === 0; }
15
+ async idle() {
16
+ while (this.tails.size)
17
+ await Promise.allSettled([...this.tails.values()]);
18
+ }
19
+ }
@@ -1,5 +1,5 @@
1
1
  import { formatMetricTokenCount } from '../store/token_usage.js';
2
- import { chunkTelegramMessage } from '../telegram/text.js';
2
+ import { chunkMessage } from './message_text.js';
3
3
  export function formatElapsedDuration(seconds, locale = 'zh') {
4
4
  if (!Number.isFinite(seconds) || seconds <= 0)
5
5
  return '0s';
@@ -75,14 +75,26 @@ export function buildFoldedToolsSummary(options) {
75
75
  : '';
76
76
  previewLines = [...head, omittedNote, ...tail].filter(Boolean);
77
77
  }
78
- toolDetailSection = `\n\n<i>${locale === 'zh' ? '工具调用记录:' : 'Tool calls:'}</i>\n${previewLines.join('\n')}`;
78
+ // Keep whole formatted lines within the summary budget; never slice through tags/entities.
79
+ const bounded = [];
80
+ let budget = 1500;
81
+ for (const line of [...previewLines].reverse()) {
82
+ if (line.length + 1 > budget)
83
+ continue;
84
+ bounded.unshift(line);
85
+ budget -= line.length + 1;
86
+ }
87
+ const omittedBySize = previewLines.length - bounded.length;
88
+ if (omittedBySize)
89
+ bounded.unshift(`<i>${locale === 'zh' ? `另有 ${omittedBySize} 项记录已收起` : `${omittedBySize} additional entries omitted`}</i>`);
90
+ toolDetailSection = `\n\n<i>${locale === 'zh' ? '工具调用记录:' : 'Tool calls:'}</i>\n${bounded.join('\n')}`;
79
91
  }
80
92
  return `<blockquote expandable>${header}${usageLine}${toolDetailSection}</blockquote>\n\n`;
81
93
  }
82
94
  export function combineSummaryAndResponse(foldedSummary, responseText, limit = 4000) {
83
95
  const content = (responseText || '(无输出 / No output)').trim();
84
96
  if (!foldedSummary) {
85
- return chunkTelegramMessage(content, limit);
97
+ return chunkMessage(content, limit);
86
98
  }
87
99
  const combined = `${foldedSummary}${content}`;
88
100
  if (combined.length <= limit) {
@@ -96,18 +108,18 @@ export function combineSummaryAndResponse(foldedSummary, responseText, limit = 4
96
108
  if (splitAt >= Math.floor(remainingInFirstChunk / 3)) {
97
109
  const firstChunkText = content.slice(0, splitAt + 1).trim();
98
110
  const rest = content.slice(splitAt + 1).trim();
99
- const restChunks = chunkTelegramMessage(rest, limit);
111
+ const restChunks = chunkMessage(rest, limit);
100
112
  return [`${foldedSummary}${firstChunkText}`, ...restChunks];
101
113
  }
102
114
  }
103
115
  // If foldedSummary takes up most of the message or no clean paragraph break was found,
104
116
  // foldedSummary gets its own clean message, and responseText is chunked independently.
105
- const responseChunks = chunkTelegramMessage(content, limit);
117
+ const responseChunks = chunkMessage(content, limit);
106
118
  return [foldedSummary.trim(), ...responseChunks];
107
119
  }
108
120
  export function renderStreamPreviewContent(state) {
109
121
  const parts = [];
110
- const name = state.engineName || 'AI';
122
+ const name = escapeTelegramHtml(state.engineName || 'AI');
111
123
  const roundText = state.stepIndex && state.stepIndex > 0 ? `第 ${state.stepIndex} 轮` : '';
112
124
  const toolText = typeof state.toolCount === 'number' && state.toolCount > 0 ? `累计执行 ${state.toolCount} 次工具` : '';
113
125
  const timeText = typeof state.elapsedSeconds === 'number' && state.elapsedSeconds > 0 ? `已耗时 ${state.elapsedSeconds}s` : '';
@@ -125,13 +137,13 @@ export function renderStreamPreviewContent(state) {
125
137
  parts.push('');
126
138
  }
127
139
  if (state.toolLines.length > 0) {
128
- const recent = state.toolLines.slice(-6);
140
+ const recent = state.toolLines.slice(-3);
129
141
  parts.push(`<blockquote expandable>🛠️ <b>正在调用工具 (${state.toolLines.length} 项)</b>\n${recent.join('\n')}</blockquote>`);
130
142
  parts.push('');
131
143
  }
132
144
  if (state.accumulatedText) {
133
- const preview = state.accumulatedText.slice(-3000);
134
- parts.push(preview);
145
+ const preview = state.accumulatedText.slice(-1000);
146
+ parts.push(escapeTelegramHtml(preview));
135
147
  }
136
148
  else {
137
149
  parts.push(state.isBoost ? `🚀 ${name} (Boost 模式) 正在深度思考中…` : `⏳ ${name} 正在思考中…`);
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import fs from 'node:fs/promises';
3
- import { buildAttachmentPrompt, isNativeImageAttachment } from '../telegram/media.js';
3
+ import { buildAttachmentPrompt, isNativeImageAttachment } from '../core/attachment_files.js';
4
4
  export function optionChoices(option) {
5
5
  if (option?.type !== 'select')
6
6
  return [];
@@ -107,7 +107,7 @@ export class DshEngineAdapter {
107
107
  let finalMessageId;
108
108
  const tools = new Map();
109
109
  const onUpdate = (notification) => {
110
- if (notification.sessionId !== state?.sessionId)
110
+ if (cancelled || notification.sessionId !== state?.sessionId)
111
111
  return;
112
112
  const update = notification.update;
113
113
  if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
@@ -170,13 +170,26 @@ export class DshEngineAdapter {
170
170
  };
171
171
  queueMicrotask(() => { void run(); });
172
172
  return {
173
- cancel: () => {
174
- if (finished || cancelled)
173
+ cancel: async () => {
174
+ if (finished || cancelled) {
175
+ await promise;
175
176
  return;
177
+ }
176
178
  cancelled = true;
177
179
  state?.client.cancelPermissions?.();
178
180
  if (state?.client.connected)
179
- void state.client.agent.notify('session/cancel', { sessionId: state.sessionId }).catch(() => { });
181
+ await state.client.agent.notify('session/cancel', { sessionId: state.sessionId }).catch(() => { });
182
+ let timer;
183
+ try {
184
+ await Promise.race([promise, new Promise(resolve => { timer = setTimeout(resolve, 5000); })]);
185
+ if (!finished)
186
+ await this.clients.get(request.scopeId)?.stop();
187
+ await promise;
188
+ }
189
+ finally {
190
+ if (timer)
191
+ clearTimeout(timer);
192
+ }
180
193
  },
181
194
  waitForResult: () => promise,
182
195
  on: (event, listener) => { emitter.on(event, listener); },
@@ -0,0 +1,7 @@
1
+ import type { AppConfig } from '../config.js';
2
+ import type { BridgeStore } from '../store/database.js';
3
+ import type { Logger } from '../logger.js';
4
+ import type { ChannelPort } from '../core/channel_port.js';
5
+ import type { BackendDescriptor } from '../core/engine_spi.js';
6
+ /** Same definition is mounted on a shared bot or on a dedicated DSH bot. */
7
+ export declare function createDshBackend(config: AppConfig, store: BridgeStore, logger: Logger, messaging: ChannelPort): BackendDescriptor;
@@ -0,0 +1,21 @@
1
+ import { getDshTelegramCommands } from '../i18n.js';
2
+ import { DshUi } from './ui.js';
3
+ /** Same definition is mounted on a shared bot or on a dedicated DSH bot. */
4
+ export function createDshBackend(config, store, logger, messaging) {
5
+ const ui = new DshUi(config, store, logger, messaging);
6
+ return {
7
+ id: ui.adapter.id, name: ui.adapter.name, engineType: ui.adapter.id, adapter: ui.adapter,
8
+ defaults: { reasoningEffort: null, supportedReasoningEfforts: [], boost: false, tokenUsage: false },
9
+ commands: getDshTelegramCommands,
10
+ createUi: host => ({
11
+ ownsCallback: data => data.startsWith('dsh:'),
12
+ getPendingApprovals: () => ui.pendingApprovals,
13
+ stop: () => ui.stop(),
14
+ renderCustomStatus: async (scopeId, locale) => ui.status(scopeId, locale),
15
+ renderSetupMenu: async (scopeId, locale, messageId) => { await ui.setup(scopeId, locale, host, messageId); return true; },
16
+ renderModelsMenu: async (scopeId, locale, messageId) => { await ui.models(scopeId, locale, host, messageId); return true; },
17
+ handleCustomCommand: (scopeId, command, args, locale) => ui.command(scopeId, command, args, locale, host),
18
+ handleCustomCallback: (scopeId, data, locale, _messageId, event) => ui.callback(scopeId, data, locale, host, event),
19
+ }),
20
+ };
21
+ }
@@ -4,7 +4,6 @@ import type { BridgeStore } from '../store/database.js';
4
4
  /** Dedicated DSH bot using the same backend and channel orchestration as other bots. */
5
5
  export declare class DshTelegramRuntime {
6
6
  private readonly bot;
7
- private readonly ui;
8
7
  private readonly orchestrator;
9
8
  private running;
10
9
  constructor(config: AppConfig, store: BridgeStore, logger: Logger);
@@ -2,12 +2,11 @@ import { TelegramGateway } from '../telegram/gateway.js';
2
2
  import { TelegramMessagingPort } from '../channels/telegram/telegram_messaging_port.js';
3
3
  import { getDshTelegramCommands } from '../i18n.js';
4
4
  import { UnifiedChannelOrchestrator } from '../core/orchestrator.js';
5
- import { DshUi } from './ui.js';
5
+ import { createDshBackend } from './backend.js';
6
6
  import { BRIDGE_SCOPE_TELEGRAM_PREFIX, parseTelegramTargetFromBridgeScope } from '../core/bridge_scope.js';
7
7
  /** Dedicated DSH bot using the same backend and channel orchestration as other bots. */
8
8
  export class DshTelegramRuntime {
9
9
  bot;
10
- ui;
11
10
  orchestrator;
12
11
  running = false;
13
12
  constructor(config, store, logger) {
@@ -15,17 +14,10 @@ export class DshTelegramRuntime {
15
14
  throw new Error('DSH_BOT_TOKEN is required');
16
15
  this.bot = new TelegramGateway(config.dshBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true, getDshTelegramCommands);
17
16
  const messaging = new TelegramMessagingPort(this.bot);
18
- this.ui = new DshUi(config, store, logger, messaging);
17
+ const backend = createDshBackend(config, store, logger, messaging);
19
18
  this.orchestrator = new UnifiedChannelOrchestrator({
20
- config, store, logger, bot: this.bot, adapter: this.ui.adapter, messaging,
19
+ config, store, logger, bot: this.bot, backends: [backend], defaultBackendId: backend.id, messaging,
21
20
  ownsScope: scopeId => scopeId.startsWith(BRIDGE_SCOPE_TELEGRAM_PREFIX) && parseTelegramTargetFromBridgeScope(scopeId).botId === this.bot.identity,
22
- customUi: {
23
- renderCustomStatus: async (scopeId, locale) => this.ui.status(scopeId, locale),
24
- renderSetupMenu: async (scopeId, locale, messageId) => { await this.ui.setup(scopeId, locale, this.orchestrator, messageId); return true; },
25
- renderModelsMenu: async (scopeId, locale, messageId) => { await this.ui.models(scopeId, locale, this.orchestrator, messageId); return true; },
26
- handleCustomCommand: (scopeId, command, args, locale) => this.ui.command(scopeId, command, args, locale, this.orchestrator),
27
- handleCustomCallback: (scopeId, data, locale, _messageId, event) => this.ui.callback(scopeId, data, locale, this.orchestrator, event),
28
- },
29
21
  });
30
22
  this.orchestrator.registerInboundHandlers();
31
23
  }
@@ -37,9 +29,8 @@ export class DshTelegramRuntime {
37
29
  async stop() {
38
30
  this.running = false;
39
31
  await this.orchestrator.stop();
40
- await this.ui.stop();
41
32
  }
42
33
  getRuntimeStatus() {
43
- return { id: this.bot.identity ?? 'dsh', username: this.bot.username, connected: this.running, activeTurns: this.orchestrator.getActiveTurnsCount(), pendingApprovals: this.ui.pendingApprovals, defaultBackend: 'dsh' };
34
+ return { id: this.bot.identity ?? 'dsh', username: this.bot.username, connected: this.running, activeTurns: this.orchestrator.getActiveTurnsCount(), pendingApprovals: this.orchestrator.getPendingApprovals(), defaultBackend: 'dsh' };
44
35
  }
45
36
  }
package/dist/dsh/ui.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { AppConfig } from '../config.js';
2
2
  import type { BridgeStore } from '../store/database.js';
3
3
  import type { Logger } from '../logger.js';
4
- import type { TelegramMessagingPort } from '../channels/telegram/telegram_messaging_port.js';
4
+ import type { ChannelPort } from '../core/channel_port.js';
5
5
  import type { AppLocale } from '../types.js';
6
- import type { TelegramCallbackEvent } from '../telegram/gateway.js';
7
- import type { UnifiedChannelOrchestrator } from '../core/orchestrator.js';
6
+ import type { ChannelCallbackEvent } from '../core/channel_events.js';
7
+ import type { BackendUiHost } from '../core/backend_ui.js';
8
8
  import { DshEngineAdapter } from './adapter.js';
9
9
  /** DSH controls shared by the unified bot and the optional dedicated bot. */
10
10
  export declare class DshUi {
@@ -16,14 +16,14 @@ export declare class DshUi {
16
16
  private readonly approvals;
17
17
  readonly adapter: DshEngineAdapter;
18
18
  get pendingApprovals(): number;
19
- constructor(config: AppConfig, store: BridgeStore, logger: Logger, messaging: TelegramMessagingPort);
19
+ constructor(config: AppConfig, store: BridgeStore, logger: Logger, messaging: ChannelPort);
20
20
  private defaultAccess;
21
21
  private copy;
22
22
  status(scopeId: string, locale: AppLocale): string;
23
23
  private button;
24
24
  private panel;
25
- setup(scopeId: string, locale: AppLocale, orchestrator: UnifiedChannelOrchestrator, messageId?: number): Promise<void>;
26
- models(scopeId: string, locale: AppLocale, orchestrator: UnifiedChannelOrchestrator, messageId?: number): Promise<void>;
25
+ setup(scopeId: string, locale: AppLocale, orchestrator: BackendUiHost, messageId?: number): Promise<void>;
26
+ models(scopeId: string, locale: AppLocale, orchestrator: BackendUiHost, messageId?: number): Promise<void>;
27
27
  private efforts;
28
28
  private selectModel;
29
29
  private selectEffort;
@@ -31,8 +31,8 @@ export declare class DshUi {
31
31
  private threads;
32
32
  private open;
33
33
  private plugins;
34
- command(scopeId: string, command: string, args: string, locale: AppLocale, orchestrator: UnifiedChannelOrchestrator): Promise<boolean>;
35
- callback(scopeId: string, data: string, locale: AppLocale, orchestrator: UnifiedChannelOrchestrator, event?: TelegramCallbackEvent): Promise<boolean>;
34
+ command(scopeId: string, command: string, args: string, locale: AppLocale, orchestrator: BackendUiHost): Promise<boolean>;
35
+ callback(scopeId: string, data: string, locale: AppLocale, orchestrator: BackendUiHost, event?: ChannelCallbackEvent): Promise<boolean>;
36
36
  private requestPermission;
37
37
  stop(): Promise<void>;
38
38
  }
package/dist/dsh/ui.js CHANGED
@@ -251,7 +251,7 @@ export class DshUi {
251
251
  async callback(scopeId, data, locale, orchestrator, event) {
252
252
  const answer = (text = '') => this.messaging.answerCallback(event?.callbackQueryId ?? '', text);
253
253
  if (!data.startsWith('dsh:')) {
254
- if (orchestrator.getBackendDescriptorForScope(scopeId).engineType === 'dsh' && data.startsWith('engine:') && !['engine:setup:main', 'engine:setup:active_mode', 'engine:setup:new', 'engine:setup:backend'].includes(data) && !data.startsWith('engine:backend:')) {
254
+ if (orchestrator.getBackendDescriptorForScope(scopeId).engineType === 'dsh' && data.startsWith('engine:') && !['engine:setup:main', 'engine:setup:models', 'engine:setup:active_mode', 'engine:setup:new', 'engine:setup:backend'].includes(data) && !data.startsWith('engine:backend:')) {
255
255
  await answer(this.copy(locale, '请重新打开 DSH 设置', 'Open DSH settings again'));
256
256
  return true;
257
257
  }
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { RuntimeSupervisor } from './core/runtime_supervisor.js';
2
3
  import crypto from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import os from 'node:os';
@@ -605,7 +606,7 @@ function truncateStatusLine(value, maxLength) {
605
606
  return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
606
607
  }
607
608
  async function runServeCli() {
608
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { DshTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
609
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { DshTelegramRuntime }, { createDshBackend }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
609
610
  import('./channels/bridge_messaging_router.js'),
610
611
  import('./channels/telegram/telegram_messaging_port.js'),
611
612
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -622,7 +623,8 @@ async function runServeCli() {
622
623
  import('./auth/cross_node_sync.js'),
623
624
  import('./opencode/runtime.js'),
624
625
  import('./dsh/runtime.js'),
625
- import('./antigravity/controller.js'),
626
+ import('./dsh/backend.js'),
627
+ import('./bridge/unified_bridge.js'),
626
628
  import('./antigravity/client.js'),
627
629
  import('./antigravity/auth.js'),
628
630
  ]);
@@ -632,11 +634,13 @@ async function runServeCli() {
632
634
  const authNotificationAggregator = createAuthRefreshNotificationAggregator(logger);
633
635
  attachIlinkRuntimeFromBridgeLogger(logger, config.wxIlinkRouteTag);
634
636
  const processLock = acquireProcessLock(config.lockPath);
637
+ const supervisor = new RuntimeSupervisor();
638
+ supervisor.register('process-lock', { stop: () => processLock.release() });
635
639
  let store = null;
636
640
  let weixinAdapter = null;
637
641
  let activeWeixinCore = null;
638
- let activeTelegramAdapters = [];
639
- let managedApps = [];
642
+ const activeTelegramAdapters = [];
643
+ const managedApps = [];
640
644
  let activeAuthMirror = null;
641
645
  let activeAuthSync = null;
642
646
  let activeOpencodeRuntime = null;
@@ -646,6 +650,30 @@ async function runServeCli() {
646
650
  let sharedAntigravityAuth = null;
647
651
  try {
648
652
  store = new BridgeStore(config.storePath);
653
+ supervisor.register('store', { stop: () => store?.close() });
654
+ supervisor.register('native-apps', { stop: async () => {
655
+ const apps = new Set([...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])]);
656
+ const results = await Promise.allSettled([...apps].map(app => app.stop({ terminateServer: true })));
657
+ for (const result of results)
658
+ if (result.status === 'rejected')
659
+ logger.warn('codex.app-server.stop_failed', { error: serializeError(result.reason) });
660
+ } });
661
+ supervisor.register('channels-and-sync', { stop: async () => {
662
+ const failures = [];
663
+ const cleanups = [() => authNotificationAggregator.flushAll(), () => activeAuthSync?.stop(), () => activeAuthMirror?.stop(),
664
+ () => weixinAdapter?.stop(), () => activeWeixinCore?.stop({ keepTransports: true }),
665
+ ...activeTelegramAdapters.map(adapter => () => adapter.stop()),
666
+ () => activeOpencodeRuntime?.stop(), () => activeDshRuntime?.stop()];
667
+ for (const cleanup of cleanups)
668
+ try {
669
+ await cleanup();
670
+ }
671
+ catch (error) {
672
+ failures.push(error);
673
+ }
674
+ if (failures.length)
675
+ throw new AggregateError(failures, 'Runtime cleanup failed');
676
+ } });
649
677
  if (config.dshBotToken) {
650
678
  activeDshRuntime = new DshTelegramRuntime(config, store, logger);
651
679
  await activeDshRuntime.start();
@@ -668,15 +696,15 @@ async function runServeCli() {
668
696
  };
669
697
  writeDshStatus();
670
698
  const timer = setInterval(writeDshStatus, 5000);
671
- const shutdown = async () => {
699
+ let shutdownPromise = null;
700
+ const shutdown = () => shutdownPromise ??= (async () => {
672
701
  clearInterval(timer);
673
- await activeDshRuntime.stop();
674
- await activeOpencodeRuntime?.stop();
675
702
  writeDshStatus(false);
676
- store.close();
677
- processLock.release();
703
+ const failures = await supervisor.stop();
704
+ for (const failure of failures)
705
+ logger.warn('bridge.shutdown_failed', { resource: failure.name, error: serializeError(failure.error) });
678
706
  process.exit(0);
679
- };
707
+ })();
680
708
  process.on('SIGINT', () => void shutdown());
681
709
  process.on('SIGTERM', () => void shutdown());
682
710
  return;
@@ -748,6 +776,7 @@ async function runServeCli() {
748
776
  ...codexApiProviderOverrides,
749
777
  ...(isCodexDefault ? [] : ['cli_auth_credentials_store="file"']),
750
778
  ]);
779
+ managedApps.push(codexApp);
751
780
  // Antigravity environment
752
781
  const isAntigravityDefault = backend === 'antigravity' && isDefaultRuntimeForBackend;
753
782
  let antigravityHome;
@@ -819,9 +848,8 @@ async function runServeCli() {
819
848
  restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
820
849
  },
821
850
  });
822
- await mirror.initialize();
823
851
  activeAuthMirror = mirror;
824
- managedApps = seeds.map((runtime) => runtime.app);
852
+ await mirror.initialize();
825
853
  const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
826
854
  let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
827
855
  const runtimes = [];
@@ -841,7 +869,7 @@ async function runServeCli() {
841
869
  ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
842
870
  botUsername: first?.botUsername ?? null,
843
871
  currentBindings: store.countBindings(),
844
- pendingApprovals: store.countPendingApprovals() + statuses.reduce((sum, status) => sum + status.dshPendingApprovals, 0) + (activeDshRuntime?.getRuntimeStatus().pendingApprovals ?? 0),
872
+ pendingApprovals: store.countPendingApprovals() + statuses.reduce((sum, status) => sum + status.pendingApprovals, 0) + (activeDshRuntime?.getRuntimeStatus().pendingApprovals ?? 0),
845
873
  pendingUserInputs: store.countPendingUserInputs(),
846
874
  queuedTurns: store.countQueuedTurnInputs(),
847
875
  activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
@@ -973,18 +1001,20 @@ async function runServeCli() {
973
1001
  antigravityApp: seed.antigravityApp,
974
1002
  antigravityAuth: seed.antigravityAuth,
975
1003
  defaultBackendId: seed.defaultBackend,
1004
+ backends: seed.config.dsh ? [createDshBackend(seed.config, store, logger, telegramMessaging)] : [],
976
1005
  selfUpdater,
977
1006
  });
978
- runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
1007
+ const telegram = new TelegramChannelAdapter(core);
1008
+ activeTelegramAdapters.push(telegram);
1009
+ runtimes.push({ ...seed, core, telegram });
979
1010
  }
980
1011
  if (config.wxEnabled) {
981
1012
  const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, null, codexApiProviderOverrides);
1013
+ managedApps.push(weixinApp);
982
1014
  const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
983
1015
  activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
984
- managedApps.push(weixinApp);
985
1016
  weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
986
1017
  }
987
- activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
988
1018
  if (config.authSyncEnabled) {
989
1019
  const authSyncTransportBot = seeds[0];
990
1020
  const authSyncTransportLabel = authSyncTransportBot.bot.username
@@ -1051,40 +1081,22 @@ async function runServeCli() {
1051
1081
  }
1052
1082
  writeAggregateStatus();
1053
1083
  logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
1054
- const shutdown = async (signal) => {
1084
+ let shutdownPromise = null;
1085
+ const shutdown = (signal) => shutdownPromise ??= (async () => {
1055
1086
  logger.info('bridge.shutting_down', { signal });
1056
- await authNotificationAggregator.flushAll();
1057
- authSync?.stop();
1058
- mirror.stop();
1059
- await weixinAdapter?.stop();
1060
- await activeWeixinCore?.stop();
1061
- await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
1062
- await activeOpencodeRuntime?.stop();
1063
- await activeDshRuntime?.stop();
1064
1087
  writeAggregateStatus(false);
1065
- const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
1066
- await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
1067
- logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
1068
- })));
1069
- store?.close();
1070
- processLock.release();
1088
+ const failures = await supervisor.stop();
1089
+ for (const failure of failures)
1090
+ logger.warn('bridge.shutdown_failed', { resource: failure.name, error: serializeError(failure.error) });
1071
1091
  process.exit(0);
1072
- };
1092
+ })();
1073
1093
  process.on('SIGINT', () => void shutdown('SIGINT'));
1074
1094
  process.on('SIGTERM', () => void shutdown('SIGTERM'));
1075
1095
  }
1076
1096
  catch (error) {
1077
- await authNotificationAggregator.flushAll().catch(() => { });
1078
- activeAuthSync?.stop();
1079
- activeAuthMirror?.stop();
1080
- await weixinAdapter?.stop().catch(() => { });
1081
- await activeWeixinCore?.stop().catch(() => { });
1082
- await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
1083
- await activeOpencodeRuntime?.stop().catch(() => { });
1084
- await activeDshRuntime?.stop().catch(() => { });
1085
- await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
1086
- store?.close();
1087
- processLock.release();
1097
+ const failures = await supervisor.stop();
1098
+ for (const failure of failures)
1099
+ logger.warn('bridge.shutdown_failed', { resource: failure.name, error: serializeError(failure.error) });
1088
1100
  throw error;
1089
1101
  }
1090
1102
  }