@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
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.13.0 - 2026-10-06
6
+
7
+ ### 中文
8
+
9
+ - **Telegram 主任务减少刷屏**:Codex、Antigravity、DSH 统一任务使用单条进度卡;私聊优先使用可停止的临时草稿,群聊编辑同一消息。完成后显示回答并折叠执行摘要,长摘要按完整记录限长。
10
+ - **停止操作绑定具体任务**:原生停止事件与卡片按钮验证会话、主题及任务编号,旧按钮不会误停新任务;等待后端取消完成,保留已生成内容与排队任务。
11
+ - **任务恢复覆盖入站到交付**:增加持久收件箱、原始输入与执行配置快照、事务队列及结果交付游标。结果已知时只补发消息;执行结果未知时暂停并通过 `/recover continue|retry|cancel` 确认,避免自动重复工具操作。
12
+ - **后端与通道边界重构**:后端定义统一注册执行器、默认设置及原生面板;执行器使用通用入站和附件契约。DSH 保留原生模型、推理档位、权限与插件入口;微信增加 `/choose` 文字菜单。
13
+ - **资源退出与竞态修复**:统一登记共享客户端、数据库和进程锁;关闭时等待取消、交付、审批及后台操作,覆盖部分启动失败。修复 Codex 控制面重复渲染、旧任务回调污染与队列抢跑。
14
+ - 仓库依赖安装、构建和 CI 统一使用 pnpm。上述验收范围是 Telegram 的 Codex / AGY / DSH 统一任务路径;微信存量执行路径、独立 OpenCode 和外部观察展示尚未完整迁移。
15
+
16
+ ### English
17
+
18
+ - **Less Telegram message noise**: unified Codex, Antigravity and DSH tasks update one progress card. Private chats prefer stoppable temporary drafts; groups edit the same message. Final answers include a collapsed execution summary with bounded complete records.
19
+ - **Task-specific stopping**: native stop events and card actions validate scope, topic and task identity. Stale actions cannot stop replacement tasks; cancellation waits for the backend and preserves partial output and queued tasks.
20
+ - **Recovery from intake to delivery**: added a durable inbox, original input/configuration snapshots, transactional queue updates and saved delivery cursors. Known results are redelivered without rerunning tools; unknown outcomes pause for `/recover continue|retry|cancel` confirmation.
21
+ - **Backend and channel boundaries**: registered backend definitions own execution defaults and native panels; executors consume shared inbound/attachment contracts. DSH retains native models, reasoning, permissions and plugin inspection. Weixin adds `/choose` text menus.
22
+ - **Owned resource shutdown and race fixes**: registered clients, database and process lock are cleaned up in dependency order, waiting for cancellations, deliveries, approvals and background work. Fixed duplicate Codex rendering, stale task callbacks and queue races.
23
+ - Dependency installation, builds and CI now use pnpm. Acceptance covers the unified Telegram Codex / AGY / DSH path; legacy Weixin execution, standalone OpenCode and external observation presentation remain outside that migration.
24
+
5
25
  ## 0.12.0 - 2026-10-05
6
26
 
7
27
  ### 中文
@@ -0,0 +1,34 @@
1
+ import type { AppLocale } from '../types.js';
2
+ import { type AntigravityAccount } from './auth.js';
3
+ import type { AppConfig } from '../config.js';
4
+ import type { BridgeStore } from '../store/database.js';
5
+ import type { Logger } from '../logger.js';
6
+ import type { BackendUiHost } from '../core/backend_ui.js';
7
+ import type { IEngineAdapter } from '../core/engine_spi.js';
8
+ import type { AntigravityAuthManager } from './auth.js';
9
+ interface PanelUi {
10
+ sendMessage: BackendUiHost['sendMessage'];
11
+ editMessage: BackendUiHost['editMessage'];
12
+ scheduleStalePanelDeletion: BackendUiHost['scheduleStalePanelDeletion'];
13
+ }
14
+ interface ExecutionView {
15
+ syncCurrentBackendSettings(scopeId: string): void;
16
+ hasActiveTurn(scopeId: string): boolean;
17
+ getAdapterForScope(scopeId: string): IEngineAdapter;
18
+ }
19
+ export declare function formatCandidateButtonPrefix(c: AntigravityAccount): string;
20
+ export declare function formatCandidateDisplayName(c: AntigravityAccount): string;
21
+ export declare function formatAccountExpiry(expiryMs: number | null, locale: AppLocale, isActive?: boolean): string;
22
+ export declare class AntigravityAccountUi {
23
+ private readonly config;
24
+ private readonly store;
25
+ private readonly logger;
26
+ private readonly auth;
27
+ private readonly ui;
28
+ private readonly orchestrator;
29
+ constructor(config: AppConfig, store: BridgeStore, logger: Logger, auth: AntigravityAuthManager, ui: PanelUi, orchestrator: ExecutionView);
30
+ sendAuthMenu(scopeId: string, args: string, locale: AppLocale, editMessageId?: number): Promise<void>;
31
+ startLoginFlow(scopeId: string, locale: AppLocale, editMessageId?: number): Promise<void>;
32
+ sendEffortMenu(scopeId: string, arg: string, locale: AppLocale, editMessageId?: number): Promise<void>;
33
+ }
34
+ export {};
@@ -0,0 +1,354 @@
1
+ import { AGY_AUTH_CALLBACK_PREFIX, AGY_EFFORT_CALLBACK_PREFIX } from './ui_callbacks.js';
2
+ import { formatQuotaResetTime } from './auth.js';
3
+ export function formatCandidateButtonPrefix(c) {
4
+ const p5h = typeof c.quota?.fiveHourPercent === 'number' ? `${c.quota.fiveHourPercent}%` : '—';
5
+ const pw = typeof c.quota?.weeklyPercent === 'number' ? `${c.quota.weeklyPercent}%` : '—';
6
+ return `${p5h}|${pw}`;
7
+ }
8
+ export function formatCandidateDisplayName(c) {
9
+ if (c.email) {
10
+ return c.email.replace(/@gmail\.com$/i, '');
11
+ }
12
+ return c.name.replace(/^antigravity-oauth-token_/, '');
13
+ }
14
+ export function formatAccountExpiry(expiryMs, locale, isActive = false) {
15
+ if (!expiryMs)
16
+ return locale === 'zh' ? '未知' : 'unknown';
17
+ const now = Date.now();
18
+ if (expiryMs <= now) {
19
+ if (isActive) {
20
+ return locale === 'zh' ? '待命中 (发任务时即时刷新)' : 'standby (refreshes on task)';
21
+ }
22
+ return locale === 'zh' ? '待命中 (就绪 · 切号时即时激活)' : 'standby (ready · refreshes on switch)';
23
+ }
24
+ const remainingMins = Math.round((expiryMs - now) / 60000);
25
+ const timeStr = new Date(expiryMs).toTimeString().slice(0, 5);
26
+ return locale === 'zh'
27
+ ? `${timeStr} (剩 ${remainingMins} 分钟)`
28
+ : `${timeStr} (${remainingMins}m left)`;
29
+ }
30
+ export class AntigravityAccountUi {
31
+ config;
32
+ store;
33
+ logger;
34
+ auth;
35
+ ui;
36
+ orchestrator;
37
+ constructor(config, store, logger, auth, ui, orchestrator) {
38
+ this.config = config;
39
+ this.store = store;
40
+ this.logger = logger;
41
+ this.auth = auth;
42
+ this.ui = ui;
43
+ this.orchestrator = orchestrator;
44
+ }
45
+ async sendAuthMenu(scopeId, args, locale, editMessageId) {
46
+ const trimmedArgs = (args || '').trim();
47
+ let currentFilter = 'all';
48
+ if (trimmedArgs.startsWith('filter:')) {
49
+ const f = trimmedArgs.slice('filter:'.length).toLowerCase();
50
+ if (f === 'enabled' || f === 'attention' || f === 'all') {
51
+ currentFilter = f;
52
+ }
53
+ }
54
+ else if (trimmedArgs) {
55
+ if (trimmedArgs.startsWith('pause ')) {
56
+ const target = trimmedArgs.slice('pause '.length).trim();
57
+ this.auth.pauseAccount(target);
58
+ await this.ui.sendMessage(scopeId, locale === 'zh' ? `⏸ 账号 \`${target}\` 已暂停使用。` : `⏸ Account \`${target}\` paused.`);
59
+ return;
60
+ }
61
+ if (trimmedArgs.startsWith('resume ')) {
62
+ const target = trimmedArgs.slice('resume '.length).trim();
63
+ this.auth.resumeAccount(target);
64
+ await this.ui.sendMessage(scopeId, locale === 'zh' ? `▶️ 账号 \`${target}\` 已恢复启用。` : `▶️ Account \`${target}\` resumed.`);
65
+ return;
66
+ }
67
+ if (trimmedArgs === 'repair_all' || trimmedArgs === 'repair all') {
68
+ const res = await this.auth.diagnoseAndRepairAll();
69
+ await this.ui.sendMessage(scopeId, locale === 'zh'
70
+ ? `🩺 **账号池体检与修复结果**\n\n• 总账号数: ${res.total}\n• 正常/已修复: ${res.healthy + res.repaired}\n• 需关注/失败: ${res.failed}\n\n${res.summary.join('\n')}`
71
+ : `🩺 **Account Diagnostics & Repair**\n\n• Total: ${res.total}\n• Healthy/Repaired: ${res.healthy + res.repaired}\n• Failed: ${res.failed}\n\n${res.summary.join('\n')}`);
72
+ return;
73
+ }
74
+ if (trimmedArgs.startsWith('repair ') || trimmedArgs === 'repair') {
75
+ const target = trimmedArgs === 'repair' ? 'active' : trimmedArgs.slice('repair '.length).trim();
76
+ const res = await this.auth.diagnoseAndRepairAccount(target);
77
+ await this.ui.sendMessage(scopeId, res.ok
78
+ ? (locale === 'zh' ? `🩺 账号 \`${res.email || res.accountName}\` 检查修复成功:${res.message}` : `🩺 Account \`${res.email || res.accountName}\` repaired: ${res.message}`)
79
+ : (locale === 'zh' ? `❌ 检查修复失败:${res.message}` : `❌ Repair failed: ${res.message}`));
80
+ return;
81
+ }
82
+ if (trimmedArgs === 'rotate') {
83
+ try {
84
+ const res = await this.auth.rotateNextCandidate();
85
+ await this.ui.sendMessage(scopeId, locale === 'zh'
86
+ ? `🔄 已成功轮转到下一个账号: \`${res.account.email ?? res.account.name}\``
87
+ : `🔄 Successfully rotated to next account: \`${res.account.email ?? res.account.name}\``);
88
+ }
89
+ catch (err) {
90
+ await this.ui.sendMessage(scopeId, `❌ ${err instanceof Error ? err.message : String(err)}`);
91
+ }
92
+ return;
93
+ }
94
+ if (trimmedArgs === 'refresh') {
95
+ const active = await this.auth.getActiveAccount();
96
+ if (!active) {
97
+ await this.ui.sendMessage(scopeId, locale === 'zh' ? '❌ 当前无活跃账号。' : '❌ No active account.');
98
+ return;
99
+ }
100
+ const res = await this.auth.refreshTokenForAccount(active.name);
101
+ if (res.success) {
102
+ await this.ui.sendMessage(scopeId, locale === 'zh'
103
+ ? `⚡ 账号 \`${active.email || active.name}\` Token 刷新成功!`
104
+ : `⚡ Account \`${active.email || active.name}\` refreshed successfully!`);
105
+ }
106
+ else {
107
+ await this.ui.sendMessage(scopeId, `❌ 刷新失败: ${res.error}`);
108
+ }
109
+ return;
110
+ }
111
+ if (trimmedArgs === 'refresh all' || trimmedArgs === 'refresh_all') {
112
+ const res = await this.auth.refreshAllTokens();
113
+ await this.auth.populateQuotas(await this.auth.listCandidates(false), true);
114
+ await this.ui.sendMessage(scopeId, locale === 'zh'
115
+ ? `⚡ 全部 Token 与额度已刷新完成: 成功 ${res.refreshed} / 共 ${res.total} 个${res.failed > 0 ? ` (失败 ${res.failed})` : ''}`
116
+ : `⚡ All tokens and quotas refreshed: ${res.refreshed} / ${res.total} succeeded`);
117
+ return;
118
+ }
119
+ if (trimmedArgs.startsWith('add ') || trimmedArgs.startsWith('{')) {
120
+ const jsonContent = trimmedArgs.startsWith('add ') ? trimmedArgs.slice('add '.length).trim() : trimmedArgs;
121
+ const res = await this.auth.importAccountFromJson(jsonContent);
122
+ if (res.success && res.account) {
123
+ const candidates = await this.auth.listCandidates();
124
+ await this.ui.sendMessage(scopeId, locale === 'zh'
125
+ ? `🎉 **Antigravity 账号导入成功!**\n• 账号名称: \`${res.account.name}\`\n• 绑定邮箱: \`${res.account.email || '未知'}\`\n• 候选池总数: 共 ${candidates.length} 个账号`
126
+ : `🎉 **Account Imported!**\n• Name: \`${res.account.name}\`\n• Email: \`${res.account.email || 'unknown'}\`\n• Pool: ${candidates.length} accounts`);
127
+ }
128
+ else {
129
+ await this.ui.sendMessage(scopeId, `❌ 导入失败: ${res.error || '未知错误'}`);
130
+ }
131
+ return;
132
+ }
133
+ // Direct switch
134
+ try {
135
+ const res = await this.auth.switchAccount(trimmedArgs);
136
+ await this.ui.sendMessage(scopeId, locale === 'zh'
137
+ ? `✅ 已成功切换到账号: \`${res.account.email ?? res.account.name}\``
138
+ : `✅ Successfully switched to account: \`${res.account.email ?? res.account.name}\``);
139
+ return;
140
+ }
141
+ catch (err) {
142
+ await this.ui.sendMessage(scopeId, `❌ ${err instanceof Error ? err.message : String(err)}`);
143
+ return;
144
+ }
145
+ }
146
+ const candidates = await this.auth.listCandidates(true);
147
+ const active = candidates.find((c) => c.isActive) || candidates[0];
148
+ const filtered = candidates.filter((c) => {
149
+ if (currentFilter === 'enabled') {
150
+ return !c.isPaused && !c.isCooldown && c.hasRefreshToken;
151
+ }
152
+ if (currentFilter === 'attention') {
153
+ return c.isPaused || c.isCooldown || !c.hasRefreshToken;
154
+ }
155
+ return true;
156
+ });
157
+ const keyboard = [];
158
+ for (const c of filtered) {
159
+ const prefix = formatCandidateButtonPrefix(c);
160
+ let icon = '🔐 ';
161
+ if (c.isPaused) {
162
+ icon = '⏸ ';
163
+ }
164
+ else if (c.isActive) {
165
+ icon = '🟢 ';
166
+ }
167
+ else if (c.isCooldown) {
168
+ icon = '⏳ ';
169
+ }
170
+ const label = `${icon}${prefix}|${formatCandidateDisplayName(c)}${c.isPaused ? ' · off' : ''}`;
171
+ let rightBtnText = '⏸';
172
+ let rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}toggle_pause:${c.name}`;
173
+ if (!c.hasRefreshToken || c.isCooldown) {
174
+ rightBtnText = '🩺';
175
+ rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}repair:${c.name}`;
176
+ }
177
+ else if (c.isPaused) {
178
+ rightBtnText = '▶️';
179
+ rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}toggle_pause:${c.name}`;
180
+ }
181
+ else if (c.isActive) {
182
+ rightBtnText = '✅';
183
+ rightBtnAction = `${AGY_AUTH_CALLBACK_PREFIX}toggle_pause:${c.name}`;
184
+ }
185
+ keyboard.push([
186
+ {
187
+ text: label.length > 28 ? `${label.slice(0, 27)}…` : label,
188
+ callback_data: `${AGY_AUTH_CALLBACK_PREFIX}switch:${c.name}`,
189
+ },
190
+ {
191
+ text: rightBtnText,
192
+ callback_data: rightBtnAction,
193
+ },
194
+ ]);
195
+ }
196
+ // Filter tab row matching Codex
197
+ keyboard.push([
198
+ {
199
+ text: currentFilter === 'all' ? '☑️ 全部' : '全部',
200
+ callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:all`,
201
+ },
202
+ {
203
+ text: currentFilter === 'enabled' ? '☑️ 已启用' : '已启用',
204
+ callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:enabled`,
205
+ },
206
+ {
207
+ text: currentFilter === 'attention' ? '☑️ 需关注' : '需关注',
208
+ callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:attention`,
209
+ },
210
+ ]);
211
+ keyboard.push([
212
+ { text: '🔑 设备登录', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}login` },
213
+ { text: '🩺 账号体检与修复', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}repair_all` },
214
+ ]);
215
+ keyboard.push([
216
+ { text: '🔄 轮转切号', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}rotate` },
217
+ { text: '⚡ 刷新额度与Token', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}refresh_all` },
218
+ ]);
219
+ keyboard.push([
220
+ { text: '📥 账号导入指南', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}import_help` },
221
+ { text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' },
222
+ ]);
223
+ const activeExpiry = formatAccountExpiry(active?.expiry ?? null, locale, true);
224
+ const activeLabel = active ? `${active.email || active.name}` : locale === 'zh' ? '无' : 'None';
225
+ let quotaDetails = '';
226
+ if (active?.quota) {
227
+ const q = active.quota;
228
+ const p5h = typeof q.fiveHourPercent === 'number' ? `${q.fiveHourPercent}%` : '未知';
229
+ const pw = typeof q.weeklyPercent === 'number' ? `${q.weeklyPercent}%` : '未知';
230
+ const r5h = q.resetTime5h ? formatQuotaResetTime(q.resetTime5h) : '—';
231
+ const rw = q.resetTimeWeekly ? formatQuotaResetTime(q.resetTimeWeekly) : '—';
232
+ quotaDetails =
233
+ locale === 'zh'
234
+ ? `\n• **Gemini 5h 额度**: \`${p5h}\` (重置于 ${r5h})` +
235
+ `\n• **Gemini 7d 额度**: \`${pw}\` (重置于 ${rw})`
236
+ : `\n• **Gemini 5h Quota**: \`${p5h}\` (resets ${r5h})` +
237
+ `\n• **Gemini 7d Quota**: \`${pw}\` (resets ${rw})`;
238
+ if (typeof q.thirdPartyWeeklyPercent === 'number') {
239
+ const p3pw = `${q.thirdPartyWeeklyPercent}%`;
240
+ const r3pw = q.resetTimeWeekly ? formatQuotaResetTime(q.resetTimeWeekly) : '—';
241
+ quotaDetails +=
242
+ locale === 'zh'
243
+ ? `\n• **3P (Claude/GPT)**: \`${p3pw}\` (重置于 ${r3pw})`
244
+ : `\n• **3P (Claude/GPT)**: \`${p3pw}\` (resets ${r3pw})`;
245
+ }
246
+ }
247
+ else {
248
+ quotaDetails =
249
+ locale === 'zh'
250
+ ? `\n• **5h/7d 额度**: \`未获取到\` (点击【⚡ 刷新额度与Token】实时获取)`
251
+ : `\n• **5h/7d Quota**: \`Not available\` (tap [⚡ Refresh Quota & Token])`;
252
+ }
253
+ const text = locale === 'zh'
254
+ ? `👤 **Antigravity 账号管理池**\n\n` +
255
+ `• **当前活跃账号**: \`${activeLabel}\`\n` +
256
+ `• **活跃 Token 状态**: ${activeExpiry}` +
257
+ quotaDetails +
258
+ `\n• **账号池总数**: 共 ${candidates.length} 个账号\n` +
259
+ `• **按键前缀说明**: \`5h剩余% | 7d剩余% | 账号别名\` (数值为剩余额度百分比,非时间)\n\n` +
260
+ `点击下方账号名称可即时无缝热切换;点击【🔑 设备登录】可在 Telegram 内直接授权绑定新账号。`
261
+ : `👤 **Antigravity Account Pool**\n\n` +
262
+ `• **Active**: \`${activeLabel}\`\n` +
263
+ `• **Status**: ${activeExpiry}` +
264
+ quotaDetails +
265
+ `\n• **Pool Size**: ${candidates.length} accounts\n` +
266
+ `• **Prefix Meaning**: \`5h% | 7d% | account\` (values represent remaining quota %, not time)\n\n` +
267
+ `Tap candidate below to switch seamlessly. Tap [🔑 Device Login] to sign in.`;
268
+ if (editMessageId) {
269
+ await this.ui.editMessage(scopeId, editMessageId, text, keyboard);
270
+ this.ui.scheduleStalePanelDeletion(scopeId, editMessageId);
271
+ }
272
+ else {
273
+ const msgId = await this.ui.sendMessage(scopeId, text, keyboard);
274
+ this.ui.scheduleStalePanelDeletion(scopeId, msgId);
275
+ }
276
+ }
277
+ async startLoginFlow(scopeId, locale, editMessageId) {
278
+ const session = this.auth.startBrowserLogin(scopeId);
279
+ const keyboard = [
280
+ [{ text: '❌ 取消登录会话', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}cancel_login` }],
281
+ [{ text: '◀️ 返回账号列表', callback_data: `${AGY_AUTH_CALLBACK_PREFIX}filter:all` }],
282
+ ];
283
+ const text = locale === 'zh'
284
+ ? `🔑 **Google / Antigravity 设备授权登录**\n\n` +
285
+ `1️⃣ **点击授权链接**(在浏览器中打开):\n` +
286
+ `[👉 点击打开 Google 授权登录页面](${session.authUrl})\n\n` +
287
+ `2️⃣ 在浏览器中完成账号选择与权限授予;\n` +
288
+ `3️⃣ 授权完成后,浏览器页面可能提示复制授权码(Authorization Code),或重定向至空白页;\n` +
289
+ `4️⃣ 直接将**授权码**或**重定向完整 URL**复制并作为消息发送给本机器人即可!\n\n` +
290
+ `*临时会话有效期 10 分钟。随时发送 \`/auth cancel\` 可取消。*`
291
+ : `🔑 **Google / Antigravity OAuth Login**\n\n` +
292
+ `1️⃣ **Click authorization link**:\n` +
293
+ `[👉 Open Google Authorization](${session.authUrl})\n\n` +
294
+ `2️⃣ Approve requested permissions;\n` +
295
+ `3️⃣ Paste the authorization code or redirect URL back into this chat!\n\n` +
296
+ `*Valid for 10 minutes. Send /auth cancel to abort.*`;
297
+ if (editMessageId) {
298
+ await this.ui.editMessage(scopeId, editMessageId, text, keyboard);
299
+ }
300
+ else {
301
+ await this.ui.sendMessage(scopeId, text, keyboard);
302
+ }
303
+ }
304
+ async sendEffortMenu(scopeId, arg, locale, editMessageId) {
305
+ const settings = this.store.getChatSettings(scopeId);
306
+ const isBoost = settings?.serviceTier === 'boost';
307
+ if (arg) {
308
+ const target = arg.toLowerCase();
309
+ if (target === 'low' || target === 'medium' || target === 'high') {
310
+ if (target !== 'high' && isBoost) {
311
+ this.store.setChatServiceTier(scopeId, null);
312
+ }
313
+ this.store.setChatEffort(scopeId, target);
314
+ this.orchestrator.syncCurrentBackendSettings(scopeId);
315
+ await this.ui.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已切换为: \`${target}\`` : `✅ Effort switched to: \`${target}\``);
316
+ return;
317
+ }
318
+ }
319
+ const current = isBoost ? 'high' : (settings?.reasoningEffort ?? 'high');
320
+ const keyboard = [
321
+ [
322
+ {
323
+ text: `${current === 'low' ? '✅ ' : ''}Low (快速)`,
324
+ callback_data: `${AGY_EFFORT_CALLBACK_PREFIX}low`,
325
+ },
326
+ {
327
+ text: `${current === 'medium' ? '✅ ' : ''}Medium (平衡)`,
328
+ callback_data: `${AGY_EFFORT_CALLBACK_PREFIX}medium`,
329
+ },
330
+ {
331
+ text: `${current === 'high' ? '✅ ' : ''}High (深度思考)`,
332
+ callback_data: `${AGY_EFFORT_CALLBACK_PREFIX}high`,
333
+ },
334
+ ],
335
+ [{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' }],
336
+ ];
337
+ const boostHint = isBoost
338
+ ? locale === 'zh'
339
+ ? '\n\n*(当前处于 Boost 模式,深度锁定为 High)*'
340
+ : '\n\n*(Locked to High in Boost Mode)*'
341
+ : '';
342
+ const text = locale === 'zh'
343
+ ? `⚡ **选择思考深度 (Reasoning Effort)**\n当前深度: \`${current}\`${boostHint}`
344
+ : `⚡ **Select Reasoning Effort**\nCurrent: \`${current}\`${boostHint}`;
345
+ if (editMessageId) {
346
+ await this.ui.editMessage(scopeId, editMessageId, text, keyboard);
347
+ this.ui.scheduleStalePanelDeletion(scopeId, editMessageId);
348
+ }
349
+ else {
350
+ const msgId = await this.ui.sendMessage(scopeId, text, keyboard);
351
+ this.ui.scheduleStalePanelDeletion(scopeId, msgId);
352
+ }
353
+ }
354
+ }
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
2
2
  import path from 'node:path';
3
3
  import { normalizeAgyModelId } from './client.js';
4
4
  import { isCapacityOrUnavailableError, isVerificationError, isQuotaOrAuthError, isTransientNetworkError, } from './events.js';
5
- import { buildAttachmentPrompt } from '../telegram/media.js';
5
+ import { buildAttachmentPrompt } from '../core/attachment_files.js';
6
6
  import { AntigravitySubagentTracker } from './subagents.js';
7
7
  function extractToolSummary(name, parameters) {
8
8
  if (!parameters)
@@ -62,6 +62,9 @@ export declare class AntigravityAuthManager {
62
62
  private readonly quotaCache;
63
63
  private readonly pausedAccounts;
64
64
  private keepAliveTimer;
65
+ private keepAliveWork;
66
+ private keepAliveCheck;
67
+ private keepAliveInitialTimer;
65
68
  constructor(authDir?: string, logger?: Logger, fetchFn?: typeof fetch);
66
69
  startBrowserLogin(scopeId: string): {
67
70
  authUrl: string;
@@ -112,7 +115,7 @@ export declare class AntigravityAuthManager {
112
115
  ensureActiveTokenFresh(thresholdSec?: number): Promise<boolean>;
113
116
  importAccountFromJson(rawJson: string, preferredName?: string): Promise<ImportAccountResult>;
114
117
  startKeepAlive(intervalMs?: number): void;
115
- stopKeepAlive(): void;
118
+ stopKeepAlive(): Promise<void>;
116
119
  runKeepAliveCheck(): Promise<void>;
117
120
  private parseAccountFile;
118
121
  private readActiveTokenContent;
@@ -80,6 +80,11 @@ export class AntigravityAuthManager {
80
80
  quotaCache = new Map();
81
81
  pausedAccounts = new Set();
82
82
  keepAliveTimer;
83
+ keepAliveWork = null;
84
+ keepAliveCheck() {
85
+ return this.keepAliveWork ??= this.runKeepAliveCheck().finally(() => { this.keepAliveWork = null; });
86
+ }
87
+ keepAliveInitialTimer;
83
88
  constructor(authDir, logger, fetchFn = fetch) {
84
89
  this.authDir = authDir || path.join(os.homedir(), '.gemini', 'antigravity-cli');
85
90
  this.activeTokenPath = path.join(this.authDir, 'antigravity-oauth-token');
@@ -816,21 +821,28 @@ export class AntigravityAuthManager {
816
821
  if (this.keepAliveTimer)
817
822
  clearInterval(this.keepAliveTimer);
818
823
  this.keepAliveTimer = setInterval(() => {
819
- this.runKeepAliveCheck().catch((err) => {
824
+ this.keepAliveCheck().catch((err) => {
820
825
  this.logger?.warn('antigravity.auth.keepalive_error', { error: String(err) });
821
826
  });
822
827
  }, intervalMs);
823
828
  this.keepAliveTimer?.unref?.();
824
- const initialTimer = setTimeout(() => {
825
- this.runKeepAliveCheck().catch(() => { });
829
+ if (this.keepAliveInitialTimer)
830
+ clearTimeout(this.keepAliveInitialTimer);
831
+ this.keepAliveInitialTimer = setTimeout(() => {
832
+ this.keepAliveInitialTimer = undefined;
833
+ this.keepAliveCheck().catch(() => { });
826
834
  }, 3000);
827
- initialTimer?.unref?.();
835
+ this.keepAliveInitialTimer?.unref?.();
828
836
  }
829
- stopKeepAlive() {
837
+ async stopKeepAlive() {
838
+ if (this.keepAliveInitialTimer)
839
+ clearTimeout(this.keepAliveInitialTimer);
840
+ this.keepAliveInitialTimer = undefined;
830
841
  if (this.keepAliveTimer) {
831
842
  clearInterval(this.keepAliveTimer);
832
843
  this.keepAliveTimer = undefined;
833
844
  }
845
+ await this.keepAliveWork?.catch(() => { });
834
846
  }
835
847
  async runKeepAliveCheck() {
836
848
  // Only refresh the active account to keep the active session warm without spamming Google OAuth
@@ -1,108 +1,3 @@
1
- import type { AppConfig } from '../config.js';
2
- import { TelegramMessagingPort } from '../channels/telegram/telegram_messaging_port.js';
3
- import type { Logger } from '../logger.js';
4
- import type { BridgeStore } from '../store/database.js';
5
- import type { RuntimeStatus } from '../types.js';
6
- import type { TelegramGateway, TelegramTextEvent } from '../telegram/gateway.js';
7
- import { AntigravityAppClient } from './client.js';
8
- import { AntigravityAuthManager } from './auth.js';
9
- import { AntigravityEngineAdapter } from './adapter.js';
10
- import { CodexEngineAdapter } from '../codex_app/adapter.js';
11
- import type { CodexAppClient, CodexAppServerRuntimeStatus } from '../codex_app/client.js';
12
- import { OpencodeEngineAdapter } from '../opencode/adapter.js';
13
- import type { OpencodeAppClient } from '../opencode/client.js';
14
- import { BridgeSessionCore } from '../controller/controller.js';
15
- import type { SelfUpdateRuntime } from '../update.js';
16
- export interface UnifiedBridgeRuntimeStatus {
17
- running: boolean;
18
- connected: boolean;
19
- activeTurns: number;
20
- dshPendingApprovals: number;
21
- botUsername: string | null;
22
- codexHome: string;
23
- codexAppServer?: CodexAppServerRuntimeStatus | undefined;
24
- userAgent: string | null;
25
- authProactiveRefresh?: RuntimeStatus['authProactiveRefresh'];
26
- lastError?: string | null | undefined;
27
- }
28
- export interface UnifiedBridgeCoreOptions {
29
- codexApp?: CodexAppClient | undefined;
30
- codexAdapter?: CodexEngineAdapter | undefined;
31
- codexCore?: BridgeSessionCore | undefined;
32
- antigravityApp?: AntigravityAppClient | undefined;
33
- antigravityAuth?: AntigravityAuthManager | undefined;
34
- antigravityAdapter?: AntigravityEngineAdapter | undefined;
35
- opencodeApp?: OpencodeAppClient | undefined;
36
- opencodeAdapter?: OpencodeEngineAdapter | undefined;
37
- defaultBackendId?: 'antigravity' | 'codex' | 'opencode' | string | undefined;
38
- selfUpdater?: SelfUpdateRuntime | undefined;
39
- }
40
- export declare class UnifiedBridgeCore {
41
- private readonly config;
42
- private readonly store;
43
- private readonly logger;
44
- private readonly bot;
45
- private readonly app;
46
- private readonly auth;
47
- private readonly conversations;
48
- private readonly messaging;
49
- private readonly adapter;
50
- private readonly codexAdapter?;
51
- private readonly codexApp?;
52
- readonly codexCore?: BridgeSessionCore | undefined;
53
- private readonly opencodeAdapter?;
54
- private readonly defaultBackendId;
55
- private readonly selfUpdater?;
56
- private readonly orchestrator;
57
- private readonly dshUi?;
58
- private readonly watchers;
59
- private readonly pendingAgyRenames;
60
- private readonly stalePanelDeleteTimers;
61
- constructor(config: AppConfig, store: BridgeStore, logger: Logger, bot: TelegramGateway, app?: AntigravityAppClient | undefined, auth?: AntigravityAuthManager | undefined, messaging?: TelegramMessagingPort | undefined, options?: UnifiedBridgeCoreOptions);
62
- private getBackendDescriptors;
63
- registerInboundHandlers(): void;
64
- registerTelegramInboundHandlers(): void;
65
- dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
66
- startCodexApp(): Promise<void>;
67
- startTelegramPolling(): Promise<void>;
68
- start(): Promise<void>;
69
- stop(): Promise<void>;
70
- private selfUpdatePollTimer;
71
- private scheduleSelfUpdateStatusPoll;
72
- private clearSelfUpdateStatusPoll;
73
- private ownsScope;
74
- private pollSelfUpdateStatus;
75
- getRuntimeStatus(): UnifiedBridgeRuntimeStatus;
76
- isIdleForServiceUpdate(): boolean;
77
- getCurrentAuthLabel(): Promise<string | null>;
78
- handleExternalCodexAuthCandidateDeleted(candidateName: string, _reason?: string | null): Promise<void>;
79
- validateExternalCodexAuthCandidate(_candidateName: string, _rawAuth: string, _expectedAccountId: string): Promise<{
80
- ok: boolean;
81
- reason?: string | null;
82
- }>;
83
- private sendMessage;
84
- private editMessage;
85
- scheduleStalePanelDeletion(scopeId: string, messageId: number): void;
86
- private renderCustomStatus;
87
- private renderCustomSetupRows;
88
- private handleCustomInbound;
89
- private handleCustomCommand;
90
- private handleCustomCallback;
91
- private sendThreadsPanel;
92
- private openThread;
93
- private sendCodexAuthMenu;
94
- private switchCodexAccount;
95
- private sendCodexThreadsMenu;
96
- private openCodexThread;
97
- private watchConversation;
98
- private unwatchConversation;
99
- private scheduleWatcherPoll;
100
- private getScopeLocale;
101
- private renderWatcherProgressUpdate;
102
- private pollWatcher;
103
- private handleWatcherTranscriptEntry;
104
- private sendAuthMenu;
105
- private startLoginFlow;
106
- private sendEffortMenu;
107
- }
108
- export { UnifiedBridgeCore as AntigravityBridgeCore };
1
+ /** Compatibility export. Runtime composition belongs to bridge/, not to an engine. */
2
+ export { UnifiedBridgeCore, UnifiedBridgeCore as AntigravityBridgeCore } from '../bridge/unified_bridge.js';
3
+ export type { UnifiedBridgeCoreOptions, UnifiedBridgeRuntimeStatus } from '../bridge/unified_bridge.js';