@foxden-app/foxclaw 0.11.1 → 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 (138) hide show
  1. package/.env.example +18 -1
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +4 -3
  4. package/README_EN.md +4 -3
  5. package/dist/antigravity/account_ui.d.ts +34 -0
  6. package/dist/antigravity/account_ui.js +354 -0
  7. package/dist/antigravity/adapter.js +1 -1
  8. package/dist/antigravity/auth.d.ts +4 -1
  9. package/dist/antigravity/auth.js +17 -5
  10. package/dist/antigravity/controller.d.ts +3 -106
  11. package/dist/antigravity/controller.js +2 -2083
  12. package/dist/antigravity/conversation_ui.d.ts +56 -0
  13. package/dist/antigravity/conversation_ui.js +509 -0
  14. package/dist/antigravity/ui_callbacks.d.ts +6 -0
  15. package/dist/antigravity/ui_callbacks.js +6 -0
  16. package/dist/bridge/unified_bridge.d.ts +114 -0
  17. package/dist/bridge/unified_bridge.js +1343 -0
  18. package/dist/channels/bridge_messaging_router.d.ts +8 -4
  19. package/dist/channels/bridge_messaging_router.js +15 -4
  20. package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
  21. package/dist/channels/telegram/telegram_messaging_port.js +22 -2
  22. package/dist/channels/text_actions.d.ts +14 -0
  23. package/dist/channels/text_actions.js +48 -0
  24. package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
  25. package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
  26. package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
  27. package/dist/channels/weixin/weixin_messaging_port.js +34 -7
  28. package/dist/codex_app/activity.d.ts +62 -0
  29. package/dist/codex_app/activity.js +418 -0
  30. package/dist/codex_app/adapter.d.ts +2 -2
  31. package/dist/codex_app/adapter.js +55 -25
  32. package/dist/codex_app/backend_ui.d.ts +4 -0
  33. package/dist/codex_app/backend_ui.js +48 -0
  34. package/dist/codex_app/client.js +0 -12
  35. package/dist/config.d.ts +4 -0
  36. package/dist/config.js +41 -5
  37. package/dist/controller/access.d.ts +1 -11
  38. package/dist/controller/access.js +1 -33
  39. package/dist/controller/activity.d.ts +1 -62
  40. package/dist/controller/activity.js +2 -418
  41. package/dist/controller/auth_errors.d.ts +11 -0
  42. package/dist/controller/auth_errors.js +124 -0
  43. package/dist/controller/auth_files.d.ts +13 -0
  44. package/dist/controller/auth_files.js +156 -0
  45. package/dist/controller/auth_presentation.d.ts +77 -0
  46. package/dist/controller/auth_presentation.js +697 -0
  47. package/dist/controller/auth_quota_service.d.ts +27 -0
  48. package/dist/controller/auth_quota_service.js +151 -0
  49. package/dist/controller/controller.d.ts +30 -56
  50. package/dist/controller/controller.js +181 -3497
  51. package/dist/controller/interactions.d.ts +74 -0
  52. package/dist/controller/interactions.js +560 -0
  53. package/dist/controller/local_usage_service.d.ts +19 -0
  54. package/dist/controller/local_usage_service.js +62 -0
  55. package/dist/controller/native_panel_service.d.ts +55 -0
  56. package/dist/controller/native_panel_service.js +214 -0
  57. package/dist/controller/native_panels.d.ts +61 -0
  58. package/dist/controller/native_panels.js +784 -0
  59. package/dist/controller/shared_helpers.d.ts +53 -0
  60. package/dist/controller/shared_helpers.js +399 -0
  61. package/dist/controller/state_types.d.ts +390 -0
  62. package/dist/controller/state_types.js +82 -0
  63. package/dist/controller/turn_rendering.d.ts +22 -0
  64. package/dist/controller/turn_rendering.js +288 -0
  65. package/dist/core/access.d.ts +11 -0
  66. package/dist/core/access.js +33 -0
  67. package/dist/core/attachment_files.d.ts +13 -0
  68. package/dist/core/attachment_files.js +180 -0
  69. package/dist/core/attachment_types.d.ts +22 -0
  70. package/dist/core/attachment_types.js +1 -0
  71. package/dist/core/attachments.d.ts +5 -3
  72. package/dist/core/attachments.js +2 -2
  73. package/dist/core/backend_registry.d.ts +22 -0
  74. package/dist/core/backend_registry.js +50 -0
  75. package/dist/core/backend_ui.d.ts +28 -0
  76. package/dist/core/backend_ui.js +1 -0
  77. package/dist/core/channel_events.d.ts +48 -0
  78. package/dist/core/channel_events.js +1 -0
  79. package/dist/core/channel_port.d.ts +52 -8
  80. package/dist/core/engine_spi.d.ts +20 -5
  81. package/dist/core/listener_scope.d.ts +11 -0
  82. package/dist/core/listener_scope.js +10 -0
  83. package/dist/core/message_text.d.ts +2 -0
  84. package/dist/core/message_text.js +28 -0
  85. package/dist/core/network_errors.d.ts +1 -0
  86. package/dist/core/network_errors.js +23 -0
  87. package/dist/core/orchestrator.d.ts +57 -22
  88. package/dist/core/orchestrator.js +877 -477
  89. package/dist/core/queued_engine_input.d.ts +12 -0
  90. package/dist/core/queued_engine_input.js +16 -0
  91. package/dist/core/runtime_supervisor.d.ts +19 -0
  92. package/dist/core/runtime_supervisor.js +45 -0
  93. package/dist/core/scope_operations.d.ts +7 -0
  94. package/dist/core/scope_operations.js +19 -0
  95. package/dist/core/stream_preview.js +21 -9
  96. package/dist/dsh/adapter.d.ts +48 -0
  97. package/dist/dsh/adapter.js +205 -0
  98. package/dist/dsh/backend.d.ts +7 -0
  99. package/dist/dsh/backend.js +21 -0
  100. package/dist/dsh/client.d.ts +46 -0
  101. package/dist/dsh/client.js +184 -0
  102. package/dist/dsh/permissions_plugin.d.ts +30 -0
  103. package/dist/dsh/permissions_plugin.js +21 -0
  104. package/dist/dsh/runtime.d.ts +20 -0
  105. package/dist/dsh/runtime.js +36 -0
  106. package/dist/dsh/ui.d.ts +38 -0
  107. package/dist/dsh/ui.js +342 -0
  108. package/dist/i18n.d.ts +4 -0
  109. package/dist/i18n.js +12 -0
  110. package/dist/main.js +144 -72
  111. package/dist/opencode/adapter.js +28 -9
  112. package/dist/opencode/controller.js +1 -1
  113. package/dist/store/channel_inbox.d.ts +14 -0
  114. package/dist/store/channel_inbox.js +21 -0
  115. package/dist/store/database.d.ts +10 -4
  116. package/dist/store/database.js +13 -3
  117. package/dist/store/task_journal.d.ts +32 -0
  118. package/dist/store/task_journal.js +70 -0
  119. package/dist/telegram/api.d.ts +2 -2
  120. package/dist/telegram/api.js +10 -28
  121. package/dist/telegram/gateway.d.ts +13 -27
  122. package/dist/telegram/gateway.js +86 -43
  123. package/dist/telegram/media.d.ts +2 -34
  124. package/dist/telegram/media.js +1 -180
  125. package/dist/telegram/task_preview.d.ts +13 -0
  126. package/dist/telegram/task_preview.js +92 -0
  127. package/dist/telegram/task_result.d.ts +2 -0
  128. package/dist/telegram/task_result.js +12 -0
  129. package/dist/telegram/text.d.ts +1 -1
  130. package/dist/telegram/text.js +2 -27
  131. package/dist/types.d.ts +2 -2
  132. package/docs/dsh.md +83 -0
  133. package/docs/release.md +13 -11
  134. package/docs/zh/architecture-review.md +106 -0
  135. package/docs/zh/dsh.md +83 -0
  136. package/docs/zh/release.md +13 -11
  137. package/docs/zh/telegram-experience.md +40 -0
  138. package/package.json +7 -5
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,42 @@
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
+
25
+ ## 0.12.0 - 2026-10-05
26
+
27
+ ### 中文
28
+
29
+ - 新增 **DeepSeek Harness (DSH) 后端**:通过 DSH 原生 ACP profile 接入,现有 Codex/Antigravity Bot 可使用 `/backend dsh` 切换,也支持 `DSH_BOT_TOKEN` 独立运行。
30
+ - 新增 DSH 专用 Telegram 设置面板:原生模型与推理档位、只读/工作区写入/完全访问、工具审批、持久会话列表与恢复;隐藏 Codex 多账号、认证同步和 Fast tier 控件。
31
+ - 支持 DSH 工具进度、最终回答、图片与 JSON 文件附件、排队、中断接管和新建会话。修复中断后的队列抢跑,以及服务重启时跨 Bot 恢复会话和队列的问题。
32
+ - 新增 `/plugins` 配置查看入口及中英文接入文档;插件安装/移除沿用 DSH 终端命令。状态页明确标注 ACP 尚未提供的逐轮 token 统计,现有统一 Bot 的 `/update` 在 DSH 模式下继续可用。
33
+
34
+ ### English
35
+
36
+ - 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`.
37
+ - 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.
38
+ - 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.
39
+ - 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.
40
+
5
41
  ## 0.11.1 - 2026-09-30
6
42
 
7
43
  ### 中文
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
@@ -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