im-hub-pro 0.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +497 -0
- package/README.zh-CN.md +496 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +921 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/acp-server.d.ts +8 -0
- package/dist/core/acp-server.d.ts.map +1 -0
- package/dist/core/acp-server.js +266 -0
- package/dist/core/acp-server.js.map +1 -0
- package/dist/core/agent-base.d.ts +94 -0
- package/dist/core/agent-base.d.ts.map +1 -0
- package/dist/core/agent-base.js +374 -0
- package/dist/core/agent-base.js.map +1 -0
- package/dist/core/agent-cwd.d.ts +45 -0
- package/dist/core/agent-cwd.d.ts.map +1 -0
- package/dist/core/agent-cwd.js +178 -0
- package/dist/core/agent-cwd.js.map +1 -0
- package/dist/core/agent-cwd.test.d.ts +2 -0
- package/dist/core/agent-cwd.test.d.ts.map +1 -0
- package/dist/core/agent-cwd.test.js +149 -0
- package/dist/core/agent-cwd.test.js.map +1 -0
- package/dist/core/approval-bus.d.ts +232 -0
- package/dist/core/approval-bus.d.ts.map +1 -0
- package/dist/core/approval-bus.js +703 -0
- package/dist/core/approval-bus.js.map +1 -0
- package/dist/core/approval-bus.synthetic.test.d.ts +2 -0
- package/dist/core/approval-bus.synthetic.test.d.ts.map +1 -0
- package/dist/core/approval-bus.synthetic.test.js +182 -0
- package/dist/core/approval-bus.synthetic.test.js.map +1 -0
- package/dist/core/approval-bus.test.d.ts +2 -0
- package/dist/core/approval-bus.test.d.ts.map +1 -0
- package/dist/core/approval-bus.test.js +537 -0
- package/dist/core/approval-bus.test.js.map +1 -0
- package/dist/core/approval-router.d.ts +95 -0
- package/dist/core/approval-router.d.ts.map +1 -0
- package/dist/core/approval-router.js +450 -0
- package/dist/core/approval-router.js.map +1 -0
- package/dist/core/approval-router.test.d.ts +2 -0
- package/dist/core/approval-router.test.d.ts.map +1 -0
- package/dist/core/approval-router.test.js +413 -0
- package/dist/core/approval-router.test.js.map +1 -0
- package/dist/core/audit-log.d.ts +55 -0
- package/dist/core/audit-log.d.ts.map +1 -0
- package/dist/core/audit-log.js +203 -0
- package/dist/core/audit-log.js.map +1 -0
- package/dist/core/bgjob-reader.d.ts +65 -0
- package/dist/core/bgjob-reader.d.ts.map +1 -0
- package/dist/core/bgjob-reader.js +212 -0
- package/dist/core/bgjob-reader.js.map +1 -0
- package/dist/core/bgjob-reader.test.d.ts +2 -0
- package/dist/core/bgjob-reader.test.d.ts.map +1 -0
- package/dist/core/bgjob-reader.test.js +178 -0
- package/dist/core/bgjob-reader.test.js.map +1 -0
- package/dist/core/circuit-breaker.d.ts +37 -0
- package/dist/core/circuit-breaker.d.ts.map +1 -0
- package/dist/core/circuit-breaker.js +115 -0
- package/dist/core/circuit-breaker.js.map +1 -0
- package/dist/core/commands/agent.d.ts +4 -0
- package/dist/core/commands/agent.d.ts.map +1 -0
- package/dist/core/commands/agent.js +21 -0
- package/dist/core/commands/agent.js.map +1 -0
- package/dist/core/commands/approval.d.ts +3 -0
- package/dist/core/commands/approval.d.ts.map +1 -0
- package/dist/core/commands/approval.js +44 -0
- package/dist/core/commands/approval.js.map +1 -0
- package/dist/core/commands/approval.test.d.ts +2 -0
- package/dist/core/commands/approval.test.d.ts.map +1 -0
- package/dist/core/commands/approval.test.js +85 -0
- package/dist/core/commands/approval.test.js.map +1 -0
- package/dist/core/commands/audit.d.ts +3 -0
- package/dist/core/commands/audit.d.ts.map +1 -0
- package/dist/core/commands/audit.js +84 -0
- package/dist/core/commands/audit.js.map +1 -0
- package/dist/core/commands/builtin.d.ts +3 -0
- package/dist/core/commands/builtin.d.ts.map +1 -0
- package/dist/core/commands/builtin.js +26 -0
- package/dist/core/commands/builtin.js.map +1 -0
- package/dist/core/commands/job.d.ts +3 -0
- package/dist/core/commands/job.d.ts.map +1 -0
- package/dist/core/commands/job.js +195 -0
- package/dist/core/commands/job.js.map +1 -0
- package/dist/core/commands/model.d.ts +9 -0
- package/dist/core/commands/model.d.ts.map +1 -0
- package/dist/core/commands/model.js +183 -0
- package/dist/core/commands/model.js.map +1 -0
- package/dist/core/commands/plan.d.ts +3 -0
- package/dist/core/commands/plan.d.ts.map +1 -0
- package/dist/core/commands/plan.js +75 -0
- package/dist/core/commands/plan.js.map +1 -0
- package/dist/core/commands/plan.test.d.ts +2 -0
- package/dist/core/commands/plan.test.d.ts.map +1 -0
- package/dist/core/commands/plan.test.js +122 -0
- package/dist/core/commands/plan.test.js.map +1 -0
- package/dist/core/commands/router.d.ts +3 -0
- package/dist/core/commands/router.d.ts.map +1 -0
- package/dist/core/commands/router.js +71 -0
- package/dist/core/commands/router.js.map +1 -0
- package/dist/core/commands/schedule.d.ts +3 -0
- package/dist/core/commands/schedule.d.ts.map +1 -0
- package/dist/core/commands/schedule.js +123 -0
- package/dist/core/commands/schedule.js.map +1 -0
- package/dist/core/commands/sessions.d.ts +3 -0
- package/dist/core/commands/sessions.d.ts.map +1 -0
- package/dist/core/commands/sessions.js +88 -0
- package/dist/core/commands/sessions.js.map +1 -0
- package/dist/core/commands/stats.d.ts +3 -0
- package/dist/core/commands/stats.d.ts.map +1 -0
- package/dist/core/commands/stats.js +73 -0
- package/dist/core/commands/stats.js.map +1 -0
- package/dist/core/commands/think.d.ts +3 -0
- package/dist/core/commands/think.d.ts.map +1 -0
- package/dist/core/commands/think.js +28 -0
- package/dist/core/commands/think.js.map +1 -0
- package/dist/core/commands/workspaces.d.ts +3 -0
- package/dist/core/commands/workspaces.d.ts.map +1 -0
- package/dist/core/commands/workspaces.js +47 -0
- package/dist/core/commands/workspaces.js.map +1 -0
- package/dist/core/config-schema.d.ts +58 -0
- package/dist/core/config-schema.d.ts.map +1 -0
- package/dist/core/config-schema.js +63 -0
- package/dist/core/config-schema.js.map +1 -0
- package/dist/core/cron.d.ts +29 -0
- package/dist/core/cron.d.ts.map +1 -0
- package/dist/core/cron.js +184 -0
- package/dist/core/cron.js.map +1 -0
- package/dist/core/event-bus.d.ts +80 -0
- package/dist/core/event-bus.d.ts.map +1 -0
- package/dist/core/event-bus.js +62 -0
- package/dist/core/event-bus.js.map +1 -0
- package/dist/core/intent-llm.d.ts +27 -0
- package/dist/core/intent-llm.d.ts.map +1 -0
- package/dist/core/intent-llm.js +170 -0
- package/dist/core/intent-llm.js.map +1 -0
- package/dist/core/intent.d.ts +12 -0
- package/dist/core/intent.d.ts.map +1 -0
- package/dist/core/intent.js +187 -0
- package/dist/core/intent.js.map +1 -0
- package/dist/core/job-board.d.ts +84 -0
- package/dist/core/job-board.d.ts.map +1 -0
- package/dist/core/job-board.js +379 -0
- package/dist/core/job-board.js.map +1 -0
- package/dist/core/logger.d.ts +6 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/metrics.d.ts +55 -0
- package/dist/core/metrics.d.ts.map +1 -0
- package/dist/core/metrics.js +291 -0
- package/dist/core/metrics.js.map +1 -0
- package/dist/core/onboarding.d.ts +94 -0
- package/dist/core/onboarding.d.ts.map +1 -0
- package/dist/core/onboarding.js +426 -0
- package/dist/core/onboarding.js.map +1 -0
- package/dist/core/onboarding.test.d.ts +2 -0
- package/dist/core/onboarding.test.d.ts.map +1 -0
- package/dist/core/onboarding.test.js +112 -0
- package/dist/core/onboarding.test.js.map +1 -0
- package/dist/core/rate-limiter.d.ts +44 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/rate-limiter.js +115 -0
- package/dist/core/rate-limiter.js.map +1 -0
- package/dist/core/registry.d.ts +32 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +122 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/router.d.ts +41 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +431 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schedule.d.ts +65 -0
- package/dist/core/schedule.d.ts.map +1 -0
- package/dist/core/schedule.js +316 -0
- package/dist/core/schedule.js.map +1 -0
- package/dist/core/session-subtasks.test.d.ts +2 -0
- package/dist/core/session-subtasks.test.d.ts.map +1 -0
- package/dist/core/session-subtasks.test.js +88 -0
- package/dist/core/session-subtasks.test.js.map +1 -0
- package/dist/core/session.d.ts +182 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +774 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/sqlite-helper.d.ts +37 -0
- package/dist/core/sqlite-helper.d.ts.map +1 -0
- package/dist/core/sqlite-helper.js +79 -0
- package/dist/core/sqlite-helper.js.map +1 -0
- package/dist/core/transcribe.d.ts +25 -0
- package/dist/core/transcribe.d.ts.map +1 -0
- package/dist/core/transcribe.js +217 -0
- package/dist/core/transcribe.js.map +1 -0
- package/dist/core/transcribe.test.d.ts +2 -0
- package/dist/core/transcribe.test.d.ts.map +1 -0
- package/dist/core/transcribe.test.js +163 -0
- package/dist/core/transcribe.test.js.map +1 -0
- package/dist/core/types.d.ts +352 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/workspace.d.ts +67 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +113 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
- package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/acp/acp-adapter.js +49 -0
- package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
- package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
- package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
- package/dist/plugins/agents/acp/acp-client.js +175 -0
- package/dist/plugins/agents/acp/acp-client.js.map +1 -0
- package/dist/plugins/agents/acp/discovery.d.ts +19 -0
- package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
- package/dist/plugins/agents/acp/discovery.js +109 -0
- package/dist/plugins/agents/acp/discovery.js.map +1 -0
- package/dist/plugins/agents/acp/index.d.ts +4 -0
- package/dist/plugins/agents/acp/index.d.ts.map +1 -0
- package/dist/plugins/agents/acp/index.js +4 -0
- package/dist/plugins/agents/acp/index.js.map +1 -0
- package/dist/plugins/agents/acp/types.d.ts +62 -0
- package/dist/plugins/agents/acp/types.d.ts.map +1 -0
- package/dist/plugins/agents/acp/types.js +5 -0
- package/dist/plugins/agents/acp/types.js.map +1 -0
- package/dist/plugins/agents/claude-code/adapter.test.d.ts +2 -0
- package/dist/plugins/agents/claude-code/adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/adapter.test.js +195 -0
- package/dist/plugins/agents/claude-code/adapter.test.js.map +1 -0
- package/dist/plugins/agents/claude-code/index.d.ts +25 -0
- package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/index.js +184 -0
- package/dist/plugins/agents/claude-code/index.js.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +42 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +235 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts +2 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts.map +1 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.js +188 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.test.js.map +1 -0
- package/dist/plugins/agents/codex/adapter.test.d.ts +2 -0
- package/dist/plugins/agents/codex/adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/codex/adapter.test.js +192 -0
- package/dist/plugins/agents/codex/adapter.test.js.map +1 -0
- package/dist/plugins/agents/codex/index.d.ts +37 -0
- package/dist/plugins/agents/codex/index.d.ts.map +1 -0
- package/dist/plugins/agents/codex/index.js +254 -0
- package/dist/plugins/agents/codex/index.js.map +1 -0
- package/dist/plugins/agents/copilot/index.d.ts +35 -0
- package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
- package/dist/plugins/agents/copilot/index.js +182 -0
- package/dist/plugins/agents/copilot/index.js.map +1 -0
- package/dist/plugins/agents/opencode/adapter.test.d.ts +2 -0
- package/dist/plugins/agents/opencode/adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/adapter.test.js +139 -0
- package/dist/plugins/agents/opencode/adapter.test.js.map +1 -0
- package/dist/plugins/agents/opencode/http-adapter.test.d.ts +2 -0
- package/dist/plugins/agents/opencode/http-adapter.test.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/http-adapter.test.js +492 -0
- package/dist/plugins/agents/opencode/http-adapter.test.js.map +1 -0
- package/dist/plugins/agents/opencode/index.d.ts +5 -0
- package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/index.js +30 -0
- package/dist/plugins/agents/opencode/index.js.map +1 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +138 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.js +549 -0
- package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +24 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +103 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
- package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
- package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
- package/dist/plugins/agents/opencode/serve-manager.js +190 -0
- package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.d.ts +22 -0
- package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.js +241 -0
- package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.d.ts +2 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.js +332 -0
- package/dist/plugins/messengers/discord/discord-adapter.test.js.map +1 -0
- package/dist/plugins/messengers/discord/index.d.ts +4 -0
- package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/index.js +4 -0
- package/dist/plugins/messengers/discord/index.js.map +1 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
- package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
- package/dist/plugins/messengers/discord/types.d.ts +9 -0
- package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
- package/dist/plugins/messengers/discord/types.js +3 -0
- package/dist/plugins/messengers/discord/types.js.map +1 -0
- package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
- package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/card-builder.js +89 -0
- package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +33 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.js +195 -0
- package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-client.d.ts +44 -0
- package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-client.js +120 -0
- package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts +2 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.js +70 -0
- package/dist/plugins/messengers/feishu/feishu-dedup.test.js.map +1 -0
- package/dist/plugins/messengers/feishu/index.d.ts +4 -0
- package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/index.js +4 -0
- package/dist/plugins/messengers/feishu/index.js.map +1 -0
- package/dist/plugins/messengers/feishu/types.d.ts +113 -0
- package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
- package/dist/plugins/messengers/feishu/types.js +4 -0
- package/dist/plugins/messengers/feishu/types.js.map +1 -0
- package/dist/plugins/messengers/telegram/index.d.ts +4 -0
- package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/index.js +4 -0
- package/dist/plugins/messengers/telegram/index.js.map +1 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
- package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.d.ts +51 -0
- package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.js +224 -0
- package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.test.d.ts +2 -0
- package/dist/plugins/messengers/telegram/media-download.test.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/media-download.test.js +125 -0
- package/dist/plugins/messengers/telegram/media-download.test.js.map +1 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +62 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.js +653 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
- package/dist/plugins/messengers/telegram/types.d.ts +47 -0
- package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
- package/dist/plugins/messengers/telegram/types.js +3 -0
- package/dist/plugins/messengers/telegram/types.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +68 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.js +483 -0
- package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-client.d.ts +66 -0
- package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-client.js +288 -0
- package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-types.d.ts +173 -0
- package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
- package/dist/plugins/messengers/wechat/ilink-types.js +12 -0
- package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
- package/dist/utils/backoff.d.ts +35 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +59 -0
- package/dist/utils/backoff.js.map +1 -0
- package/dist/utils/cross-platform.d.ts +26 -0
- package/dist/utils/cross-platform.d.ts.map +1 -0
- package/dist/utils/cross-platform.js +58 -0
- package/dist/utils/cross-platform.js.map +1 -0
- package/dist/utils/message-split.d.ts +14 -0
- package/dist/utils/message-split.d.ts.map +1 -0
- package/dist/utils/message-split.js +65 -0
- package/dist/utils/message-split.js.map +1 -0
- package/dist/utils/safe-equal.d.ts +2 -0
- package/dist/utils/safe-equal.d.ts.map +1 -0
- package/dist/utils/safe-equal.js +11 -0
- package/dist/utils/safe-equal.js.map +1 -0
- package/dist/web/public/_app.js +196 -0
- package/dist/web/public/index.html +935 -0
- package/dist/web/public/settings.html +1181 -0
- package/dist/web/public/tasks.html +1827 -0
- package/dist/web/server.d.ts +11 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +1820 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
// Telegram Bot API Adapter using grammy
|
|
2
|
+
// Implements MessengerAdapter interface with native typing indicator support
|
|
3
|
+
import { Bot } from 'grammy';
|
|
4
|
+
import { markdownToTelegramHtml } from './markdown-to-html.js';
|
|
5
|
+
import { cleanupOldMedia, downloadToMediaRoot, pickExtension, } from './media-download.js';
|
|
6
|
+
import { splitMessage } from '../../../utils/message-split.js';
|
|
7
|
+
import { Backoff } from '../../../utils/backoff.js';
|
|
8
|
+
import { logger as rootLogger } from '../../../core/logger.js';
|
|
9
|
+
import { transcribe, detectProvider, TranscribeError } from '../../../core/transcribe.js';
|
|
10
|
+
const log = rootLogger.child({ component: 'telegram' });
|
|
11
|
+
export class TelegramAdapter {
|
|
12
|
+
name = 'telegram';
|
|
13
|
+
bot = null;
|
|
14
|
+
config = null;
|
|
15
|
+
messageHandler;
|
|
16
|
+
buttonHandler;
|
|
17
|
+
isRunning = false;
|
|
18
|
+
typingIntervals = new Map();
|
|
19
|
+
// grammy 的 bot.start() 长轮询偶尔会被一次网络抖动 wedge —— 既不报错也不
|
|
20
|
+
// resolve,看起来还在跑但不再 fetch updates,TG 那边 pending_update_count
|
|
21
|
+
// 一路涨。watchdog 周期性 ping getMe;连续多次失败就强制 stop+start,让
|
|
22
|
+
// 卡死的 polling loop 重置。
|
|
23
|
+
watchdogTimer;
|
|
24
|
+
consecutivePingFailures = 0;
|
|
25
|
+
static WATCHDOG_INTERVAL_MS = 60_000;
|
|
26
|
+
static WATCHDOG_FAILURE_THRESHOLD = 3; // ~3min 无响应就重启 polling
|
|
27
|
+
mediaCleanupTimer;
|
|
28
|
+
static MEDIA_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // hourly
|
|
29
|
+
async start() {
|
|
30
|
+
// Load config
|
|
31
|
+
const { readFile } = await import('fs/promises');
|
|
32
|
+
const { homedir } = await import('os');
|
|
33
|
+
const { join } = await import('path');
|
|
34
|
+
const configPath = join(homedir(), '.im-hub', 'config.json');
|
|
35
|
+
try {
|
|
36
|
+
const data = await readFile(configPath, 'utf-8');
|
|
37
|
+
const config = JSON.parse(data);
|
|
38
|
+
this.config = config.telegram;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error('Telegram config not found. Run "im-hub config telegram" first.');
|
|
42
|
+
}
|
|
43
|
+
if (!this.config?.botToken) {
|
|
44
|
+
throw new Error('Telegram bot token not configured. Run "im-hub config telegram" first.');
|
|
45
|
+
}
|
|
46
|
+
// Initialize bot
|
|
47
|
+
this.bot = new Bot(this.config.botToken);
|
|
48
|
+
// Set up message handler.
|
|
49
|
+
//
|
|
50
|
+
// CRITICAL: do NOT await messageHandler here. grammy processes updates
|
|
51
|
+
// sequentially per chat — if the handler awaits an agent run that's
|
|
52
|
+
// waiting on a separate IM-side approval reply, polling stalls and the
|
|
53
|
+
// user's approval reply piles up in TG's pending_update_count, never
|
|
54
|
+
// reaching us. Fire-and-forget mirrors how the WeChat ilink adapter
|
|
55
|
+
// dispatches (ilink-adapter.ts:258).
|
|
56
|
+
this.bot.on('message:text', (ctx) => {
|
|
57
|
+
// Ignore messages from bots
|
|
58
|
+
if (ctx.message.from.is_bot)
|
|
59
|
+
return;
|
|
60
|
+
if (!this.messageHandler)
|
|
61
|
+
return;
|
|
62
|
+
const message = {
|
|
63
|
+
id: ctx.message.message_id.toString(),
|
|
64
|
+
threadId: ctx.chat.id.toString(),
|
|
65
|
+
userId: ctx.message.from?.id?.toString() || 'unknown',
|
|
66
|
+
text: ctx.message.text || '',
|
|
67
|
+
timestamp: new Date(ctx.message.date * 1000),
|
|
68
|
+
channelId: this.config?.channelId || 'default',
|
|
69
|
+
};
|
|
70
|
+
const msgCtx = {
|
|
71
|
+
message,
|
|
72
|
+
platform: 'telegram',
|
|
73
|
+
channelId: this.config?.channelId || 'default',
|
|
74
|
+
};
|
|
75
|
+
this.messageHandler(msgCtx).catch((err) => {
|
|
76
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
77
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
78
|
+
log.error({ err: errMsg, stack, threadId: message.threadId }, 'Error in message handler');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// Media handlers — TG photos and image documents. We await the download
|
|
82
|
+
// inside the handler (it's bounded; typical TG photo is < 1 s, hard cap
|
|
83
|
+
// 20 MB) so the resulting Message reflects the image being on disk before
|
|
84
|
+
// we kick off the agent. messageHandler itself is fire-and-forget for the
|
|
85
|
+
// same reasons as message:text above. Order across photo / text within a
|
|
86
|
+
// chat is preserved because grammy serializes updates per chat.
|
|
87
|
+
this.bot.on('message:photo', (ctx) => {
|
|
88
|
+
if (ctx.message.from?.is_bot)
|
|
89
|
+
return;
|
|
90
|
+
if (!this.messageHandler)
|
|
91
|
+
return;
|
|
92
|
+
// Largest size is the last entry — TG ships scaled-down siblings for
|
|
93
|
+
// bandwidth-conscious clients which we ignore.
|
|
94
|
+
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
|
95
|
+
void this.handleMediaUpload(ctx, photo.file_id, undefined, ctx.message.caption ?? '');
|
|
96
|
+
});
|
|
97
|
+
this.bot.on('message:document', (ctx) => {
|
|
98
|
+
if (ctx.message.from?.is_bot)
|
|
99
|
+
return;
|
|
100
|
+
if (!this.messageHandler)
|
|
101
|
+
return;
|
|
102
|
+
const doc = ctx.message.document;
|
|
103
|
+
// Image documents → media upload path. Audio documents → voice path.
|
|
104
|
+
// Anything else gets dropped (videos / archives / misc).
|
|
105
|
+
if (doc.mime_type?.startsWith('image/')) {
|
|
106
|
+
void this.handleMediaUpload(ctx, doc.file_id, doc.mime_type, ctx.message.caption ?? '');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (doc.mime_type?.startsWith('audio/')) {
|
|
110
|
+
// Document type has no `duration` field even when MIME is audio/*;
|
|
111
|
+
// only message:voice / message:audio carry it. Pass undefined.
|
|
112
|
+
void this.handleVoiceUpload(ctx, doc.file_id, doc.mime_type, ctx.message.caption ?? '', undefined);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
log.debug({ mime: doc.mime_type, chatId: ctx.chat.id }, 'ignoring non-image/audio document');
|
|
116
|
+
});
|
|
117
|
+
// Voice messages (the mic-button "press and hold" recording, OGG OPUS).
|
|
118
|
+
// Caption is rare on voice but TG allows it.
|
|
119
|
+
this.bot.on('message:voice', (ctx) => {
|
|
120
|
+
if (ctx.message.from?.is_bot)
|
|
121
|
+
return;
|
|
122
|
+
if (!this.messageHandler)
|
|
123
|
+
return;
|
|
124
|
+
const v = ctx.message.voice;
|
|
125
|
+
void this.handleVoiceUpload(ctx, v.file_id, v.mime_type, ctx.message.caption ?? '', v.duration);
|
|
126
|
+
});
|
|
127
|
+
// Audio messages (a music file or the "Audio" attachment button).
|
|
128
|
+
this.bot.on('message:audio', (ctx) => {
|
|
129
|
+
if (ctx.message.from?.is_bot)
|
|
130
|
+
return;
|
|
131
|
+
if (!this.messageHandler)
|
|
132
|
+
return;
|
|
133
|
+
const a = ctx.message.audio;
|
|
134
|
+
void this.handleVoiceUpload(ctx, a.file_id, a.mime_type, ctx.message.caption ?? '', a.duration);
|
|
135
|
+
});
|
|
136
|
+
// Inline-button taps (approval cards). Same fire-and-forget discipline
|
|
137
|
+
// as message:text — buttonHandler may resolve a pending approval which
|
|
138
|
+
// calls back into editApprovalCard; we don't want that to block grammy's
|
|
139
|
+
// sequential update queue.
|
|
140
|
+
//
|
|
141
|
+
// Telegram requires answerCallbackQuery within ~1s or the client shows
|
|
142
|
+
// a spinner. We wrap ack() so the handler can call it explicitly when
|
|
143
|
+
// it has a meaningful toast; if it doesn't, we send an empty ack at the
|
|
144
|
+
// end as a safety net (idempotent — TG ignores the second call).
|
|
145
|
+
this.bot.on('callback_query:data', (ctx) => {
|
|
146
|
+
if (!this.buttonHandler) {
|
|
147
|
+
void ctx.answerCallbackQuery({ text: '系统未就绪' }).catch(() => { });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const data = ctx.callbackQuery.data;
|
|
151
|
+
const from = ctx.callbackQuery.from;
|
|
152
|
+
const msg = ctx.callbackQuery.message;
|
|
153
|
+
if (!msg) {
|
|
154
|
+
void ctx.answerCallbackQuery({ text: '消息已不可用' }).catch(() => { });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Optional allowlist gate. Empty / missing list = allow anyone (matches
|
|
158
|
+
// the text-reply path). When configured, refuse with a toast — the
|
|
159
|
+
// refusal does NOT resolve the pending, so an authorized user can
|
|
160
|
+
// still click later.
|
|
161
|
+
const allowlist = this.config?.approvalAllowlist;
|
|
162
|
+
if (allowlist && allowlist.length > 0) {
|
|
163
|
+
const fromIdStr = from.id.toString();
|
|
164
|
+
if (!allowlist.includes(fromIdStr)) {
|
|
165
|
+
log.warn({
|
|
166
|
+
event: 'telegram.approval.unauthorized_click',
|
|
167
|
+
userId: fromIdStr,
|
|
168
|
+
username: from.username,
|
|
169
|
+
chatType: msg.chat.type,
|
|
170
|
+
chatId: msg.chat.id,
|
|
171
|
+
}, 'Unauthorized button click rejected by allowlist');
|
|
172
|
+
void ctx.answerCallbackQuery({ text: '无权审批此请求' }).catch(() => { });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let acked = false;
|
|
177
|
+
const cb = {
|
|
178
|
+
data,
|
|
179
|
+
threadId: msg.chat.id.toString(),
|
|
180
|
+
userId: from.id.toString(),
|
|
181
|
+
userDisplay: from.username ? `@${from.username}` : (from.first_name || from.id.toString()),
|
|
182
|
+
messageId: msg.message_id.toString(),
|
|
183
|
+
ack: async (text) => {
|
|
184
|
+
if (acked)
|
|
185
|
+
return;
|
|
186
|
+
acked = true;
|
|
187
|
+
try {
|
|
188
|
+
await ctx.answerCallbackQuery(text ? { text } : undefined);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
log.warn({ err: String(err) }, 'answerCallbackQuery failed');
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
this.buttonHandler(cb)
|
|
196
|
+
.catch((err) => {
|
|
197
|
+
log.error({ err: String(err), data }, 'Error in button handler');
|
|
198
|
+
})
|
|
199
|
+
.finally(() => {
|
|
200
|
+
if (!acked) {
|
|
201
|
+
// Safety net so the user's TG client doesn't keep spinning when
|
|
202
|
+
// the handler forgot to ack.
|
|
203
|
+
void ctx.answerCallbackQuery().catch(() => { });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
// Start bot in background. grammy's start() uses long polling and resolves
|
|
208
|
+
// only when polling stops. We wrap it in a self-healing loop so that:
|
|
209
|
+
// 1. an unexpected resolve while still isRunning → restart polling
|
|
210
|
+
// 2. a thrown error → log it and retry after a short backoff
|
|
211
|
+
// Combined with the watchdog (below), this defends against the silent
|
|
212
|
+
// wedge mode where bot.start() neither resolves nor rejects but stops
|
|
213
|
+
// fetching updates.
|
|
214
|
+
log.info('Starting bot with long polling');
|
|
215
|
+
this.isRunning = true;
|
|
216
|
+
void this.runPollingLoop();
|
|
217
|
+
this.startWatchdog();
|
|
218
|
+
this.startMediaCleanup();
|
|
219
|
+
log.info('Telegram adapter started');
|
|
220
|
+
}
|
|
221
|
+
/** Run media cleanup once now (so a long-running im-hub doesn't accumulate
|
|
222
|
+
* files indefinitely when restarts are infrequent) and then hourly. The
|
|
223
|
+
* hourly cadence matches the typical 7-day TTL with plenty of slack. */
|
|
224
|
+
startMediaCleanup() {
|
|
225
|
+
if (this.mediaCleanupTimer)
|
|
226
|
+
clearInterval(this.mediaCleanupTimer);
|
|
227
|
+
void cleanupOldMedia().catch((err) => {
|
|
228
|
+
log.warn({ err: String(err), event: 'telegram.media.cleanup_failed' }, 'startup media cleanup failed');
|
|
229
|
+
});
|
|
230
|
+
this.mediaCleanupTimer = setInterval(() => {
|
|
231
|
+
void cleanupOldMedia().catch((err) => {
|
|
232
|
+
log.warn({ err: String(err), event: 'telegram.media.cleanup_failed' }, 'periodic media cleanup failed');
|
|
233
|
+
});
|
|
234
|
+
}, TelegramAdapter.MEDIA_CLEANUP_INTERVAL_MS);
|
|
235
|
+
}
|
|
236
|
+
async runPollingLoop() {
|
|
237
|
+
// M9: exponential backoff with jitter replaces the previous fixed 2s
|
|
238
|
+
// (unexpected resolve) / 5s (error) delays. Auth-revoked states no
|
|
239
|
+
// longer hammer the API at a fixed cadence, and fleet-wide network
|
|
240
|
+
// recovery doesn't lock-step its reconnects.
|
|
241
|
+
//
|
|
242
|
+
// baseMs = 2_000 preserves the previous "first error → ~2s" feel so
|
|
243
|
+
// operators eyeballing logs during normal restarts see the same
|
|
244
|
+
// ballpark; capMs = 60_000 prevents indefinite drift past 1 minute.
|
|
245
|
+
const backoff = new Backoff({ baseMs: 2_000, capMs: 60_000, jitter: 0.5 });
|
|
246
|
+
/** Reset the backoff if bot.start() ran healthy for at least this long
|
|
247
|
+
* before erroring. Without this, a single transient error at hour 12
|
|
248
|
+
* of operation would still queue the same exponential tail as a
|
|
249
|
+
* startup-time crash loop. 30 s is comfortably longer than any
|
|
250
|
+
* realistic handshake / first-fetch round-trip. */
|
|
251
|
+
const HEALTHY_RUN_THRESHOLD_MS = 30_000;
|
|
252
|
+
while (this.isRunning && this.bot) {
|
|
253
|
+
const startedAt = Date.now();
|
|
254
|
+
try {
|
|
255
|
+
await this.bot.start();
|
|
256
|
+
if (!this.isRunning) {
|
|
257
|
+
log.info('Bot stopped gracefully');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// bot.start() resolved while we still want to be running — that's
|
|
261
|
+
// grammy's silent-wedge mode (or an internal early return). Treat
|
|
262
|
+
// it as a fault and back off before restarting.
|
|
263
|
+
if (Date.now() - startedAt >= HEALTHY_RUN_THRESHOLD_MS)
|
|
264
|
+
backoff.reset();
|
|
265
|
+
const delayMs = backoff.nextDelayMs();
|
|
266
|
+
log.warn({
|
|
267
|
+
event: 'telegram.polling.unexpected_stop',
|
|
268
|
+
attempt: backoff.currentAttempt(),
|
|
269
|
+
delayMs,
|
|
270
|
+
}, 'bot.start() resolved while still running; restarting after backoff');
|
|
271
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (!this.isRunning)
|
|
275
|
+
return;
|
|
276
|
+
if (Date.now() - startedAt >= HEALTHY_RUN_THRESHOLD_MS)
|
|
277
|
+
backoff.reset();
|
|
278
|
+
const delayMs = backoff.nextDelayMs();
|
|
279
|
+
log.error({
|
|
280
|
+
err: err instanceof Error ? err.message : String(err),
|
|
281
|
+
event: 'telegram.polling.error',
|
|
282
|
+
attempt: backoff.currentAttempt(),
|
|
283
|
+
delayMs,
|
|
284
|
+
}, 'Bot polling error; restarting after backoff');
|
|
285
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
startWatchdog() {
|
|
290
|
+
if (this.watchdogTimer)
|
|
291
|
+
clearInterval(this.watchdogTimer);
|
|
292
|
+
this.consecutivePingFailures = 0;
|
|
293
|
+
this.watchdogTimer = setInterval(async () => {
|
|
294
|
+
if (!this.isRunning || !this.bot)
|
|
295
|
+
return;
|
|
296
|
+
try {
|
|
297
|
+
await this.bot.api.getMe();
|
|
298
|
+
if (this.consecutivePingFailures > 0) {
|
|
299
|
+
log.info({ event: 'telegram.watchdog.recovered' }, 'getMe ping recovered');
|
|
300
|
+
}
|
|
301
|
+
this.consecutivePingFailures = 0;
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
this.consecutivePingFailures += 1;
|
|
305
|
+
log.warn({
|
|
306
|
+
event: 'telegram.watchdog.ping_failed',
|
|
307
|
+
consecutive: this.consecutivePingFailures,
|
|
308
|
+
err: err instanceof Error ? err.message : String(err),
|
|
309
|
+
}, 'Watchdog getMe ping failed');
|
|
310
|
+
if (this.consecutivePingFailures >= TelegramAdapter.WATCHDOG_FAILURE_THRESHOLD) {
|
|
311
|
+
log.error({ event: 'telegram.watchdog.restarting_polling' }, 'Polling appears wedged; forcing bot.stop() to trigger restart');
|
|
312
|
+
this.consecutivePingFailures = 0;
|
|
313
|
+
try {
|
|
314
|
+
await this.bot.stop();
|
|
315
|
+
}
|
|
316
|
+
catch { /* ignore */ }
|
|
317
|
+
// runPollingLoop will see bot.start() resolve and restart it.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}, TelegramAdapter.WATCHDOG_INTERVAL_MS);
|
|
321
|
+
}
|
|
322
|
+
async stop() {
|
|
323
|
+
this.isRunning = false;
|
|
324
|
+
if (this.watchdogTimer) {
|
|
325
|
+
clearInterval(this.watchdogTimer);
|
|
326
|
+
this.watchdogTimer = undefined;
|
|
327
|
+
}
|
|
328
|
+
if (this.mediaCleanupTimer) {
|
|
329
|
+
clearInterval(this.mediaCleanupTimer);
|
|
330
|
+
this.mediaCleanupTimer = undefined;
|
|
331
|
+
}
|
|
332
|
+
// Clean up all typing intervals
|
|
333
|
+
for (const interval of this.typingIntervals.values()) {
|
|
334
|
+
clearInterval(interval);
|
|
335
|
+
}
|
|
336
|
+
this.typingIntervals.clear();
|
|
337
|
+
if (this.bot) {
|
|
338
|
+
await this.bot.stop();
|
|
339
|
+
this.bot = null;
|
|
340
|
+
}
|
|
341
|
+
log.info('Telegram adapter stopped');
|
|
342
|
+
}
|
|
343
|
+
onMessage(handler) {
|
|
344
|
+
this.messageHandler = handler;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Download a TG photo / image-document, save it under MEDIA_ROOT, and surface
|
|
348
|
+
* the result to messageHandler as a Message whose `text` includes the
|
|
349
|
+
* caption (if any) plus a "[图片附件:/path/x.jpg]" marker — claude-code
|
|
350
|
+
* picks that up and uses Read to view it.
|
|
351
|
+
*
|
|
352
|
+
* On download failure we still call messageHandler with a "[图片下载失败]"
|
|
353
|
+
* marker so the user's interaction isn't silently dropped — they can resend
|
|
354
|
+
* or be told what went wrong.
|
|
355
|
+
*/
|
|
356
|
+
async handleMediaUpload(ctx, fileId, mime, caption) {
|
|
357
|
+
if (!this.bot || !ctx.chat || !ctx.message)
|
|
358
|
+
return;
|
|
359
|
+
const chatId = ctx.chat.id;
|
|
360
|
+
const msgId = ctx.message.message_id;
|
|
361
|
+
let attachmentLine;
|
|
362
|
+
const botToken = this.config.botToken;
|
|
363
|
+
// Scrub the bot token from any string before it reaches a log line or a
|
|
364
|
+
// user-facing chat reply. The token rides inside the file URL we hand to
|
|
365
|
+
// curl, and curl's stderr can echo the URL on TLS / DNS failures.
|
|
366
|
+
const scrub = (s) => botToken ? s.split(botToken).join('[REDACTED]') : s;
|
|
367
|
+
try {
|
|
368
|
+
const file = await this.bot.api.getFile(fileId);
|
|
369
|
+
if (!file.file_path)
|
|
370
|
+
throw new Error('TG returned no file_path');
|
|
371
|
+
const ext = pickExtension(file.file_path, mime);
|
|
372
|
+
const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
|
|
373
|
+
const { path } = await downloadToMediaRoot({
|
|
374
|
+
url,
|
|
375
|
+
subdir: `telegram/${chatId}`,
|
|
376
|
+
filename: `${msgId}.${ext}`,
|
|
377
|
+
});
|
|
378
|
+
attachmentLine = `[图片附件:${path}]`;
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
const msg = scrub(err instanceof Error ? err.message : String(err));
|
|
382
|
+
log.warn({ event: 'telegram.media.download_failed', err: msg, chatId, msgId }, 'media download failed');
|
|
383
|
+
attachmentLine = `[图片附件下载失败:${msg}]`;
|
|
384
|
+
}
|
|
385
|
+
const text = caption ? `${caption}\n\n${attachmentLine}` : attachmentLine;
|
|
386
|
+
const message = {
|
|
387
|
+
id: msgId.toString(),
|
|
388
|
+
threadId: chatId.toString(),
|
|
389
|
+
userId: ctx.message.from?.id?.toString() || 'unknown',
|
|
390
|
+
text,
|
|
391
|
+
timestamp: new Date(ctx.message.date * 1000),
|
|
392
|
+
channelId: this.config?.channelId || 'default',
|
|
393
|
+
};
|
|
394
|
+
const msgCtx = {
|
|
395
|
+
message,
|
|
396
|
+
platform: 'telegram',
|
|
397
|
+
channelId: this.config?.channelId || 'default',
|
|
398
|
+
};
|
|
399
|
+
if (!this.messageHandler)
|
|
400
|
+
return;
|
|
401
|
+
this.messageHandler(msgCtx).catch((err) => {
|
|
402
|
+
log.error({
|
|
403
|
+
err: err instanceof Error ? err.message : String(err),
|
|
404
|
+
threadId: message.threadId,
|
|
405
|
+
}, 'Error in media message handler');
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Download a TG voice / audio message, transcribe it via whichever provider
|
|
410
|
+
* is configured (OpenAI Whisper or whisper.cpp), and surface the transcript
|
|
411
|
+
* to messageHandler as Message.text. The downloaded audio file path is
|
|
412
|
+
* also included so the agent can reference it (e.g. send it back, replay).
|
|
413
|
+
*
|
|
414
|
+
* Failures are surfaced as text markers, not silent drops:
|
|
415
|
+
* - download failure → "[语音附件下载失败:…]"
|
|
416
|
+
* - no provider configured → "[语音附件未转写:未配置 OPENAI_API_KEY 或 IMHUB_WHISPERCPP_BIN]"
|
|
417
|
+
* - transcribe error → "[语音转写失败(${provider}):…]"
|
|
418
|
+
*
|
|
419
|
+
* Since transcription can take 5-30s on a slow CPU + whisper.cpp medium,
|
|
420
|
+
* we fire-and-forget the entire operation so grammy's update queue keeps
|
|
421
|
+
* draining for other chats. Within this chat, ordering is still serialized
|
|
422
|
+
* by grammy.
|
|
423
|
+
*/
|
|
424
|
+
async handleVoiceUpload(ctx, fileId, mime, caption, durationSec) {
|
|
425
|
+
if (!this.bot || !ctx.chat || !ctx.message)
|
|
426
|
+
return;
|
|
427
|
+
const chatId = ctx.chat.id;
|
|
428
|
+
const msgId = ctx.message.message_id;
|
|
429
|
+
let savedPath = null;
|
|
430
|
+
let downloadErr = null;
|
|
431
|
+
const botToken = this.config.botToken;
|
|
432
|
+
const scrub = (s) => botToken ? s.split(botToken).join('[REDACTED]') : s;
|
|
433
|
+
try {
|
|
434
|
+
const file = await this.bot.api.getFile(fileId);
|
|
435
|
+
if (!file.file_path)
|
|
436
|
+
throw new Error('TG returned no file_path');
|
|
437
|
+
const ext = pickExtension(file.file_path, mime);
|
|
438
|
+
const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
|
|
439
|
+
const { path } = await downloadToMediaRoot({
|
|
440
|
+
url,
|
|
441
|
+
subdir: `telegram/${chatId}`,
|
|
442
|
+
filename: `${msgId}.${ext}`,
|
|
443
|
+
});
|
|
444
|
+
savedPath = path;
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
downloadErr = scrub(err instanceof Error ? err.message : String(err));
|
|
448
|
+
log.warn({ event: 'telegram.voice.download_failed', err: downloadErr, chatId, msgId }, 'voice download failed');
|
|
449
|
+
}
|
|
450
|
+
let voiceLine;
|
|
451
|
+
if (!savedPath) {
|
|
452
|
+
voiceLine = `[语音附件下载失败:${downloadErr}]`;
|
|
453
|
+
}
|
|
454
|
+
else if (detectProvider() === 'none') {
|
|
455
|
+
voiceLine = `[语音附件未转写(未配置 OPENAI_API_KEY 或 IMHUB_WHISPERCPP_BIN):${savedPath}]`;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
try {
|
|
459
|
+
const result = await transcribe(savedPath, { language: 'zh' });
|
|
460
|
+
const dur = durationSec != null ? `${durationSec}s, ` : '';
|
|
461
|
+
voiceLine = [
|
|
462
|
+
`[语音转写(${dur}provider=${result.provider}, ${result.elapsedMs}ms):`,
|
|
463
|
+
result.text || '(空)',
|
|
464
|
+
`源文件:${savedPath}]`,
|
|
465
|
+
].join('\n');
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
const reason = err instanceof TranscribeError
|
|
469
|
+
? `${err.provider}: ${err.reason}`
|
|
470
|
+
: err instanceof Error ? err.message : String(err);
|
|
471
|
+
voiceLine = `[语音转写失败(${reason})\n源文件:${savedPath}]`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const text = caption ? `${caption}\n\n${voiceLine}` : voiceLine;
|
|
475
|
+
const message = {
|
|
476
|
+
id: msgId.toString(),
|
|
477
|
+
threadId: chatId.toString(),
|
|
478
|
+
userId: ctx.message.from?.id?.toString() || 'unknown',
|
|
479
|
+
text,
|
|
480
|
+
timestamp: new Date(ctx.message.date * 1000),
|
|
481
|
+
channelId: this.config?.channelId || 'default',
|
|
482
|
+
};
|
|
483
|
+
const msgCtx = {
|
|
484
|
+
message,
|
|
485
|
+
platform: 'telegram',
|
|
486
|
+
channelId: this.config?.channelId || 'default',
|
|
487
|
+
};
|
|
488
|
+
if (!this.messageHandler)
|
|
489
|
+
return;
|
|
490
|
+
this.messageHandler(msgCtx).catch((err) => {
|
|
491
|
+
log.error({
|
|
492
|
+
err: err instanceof Error ? err.message : String(err),
|
|
493
|
+
threadId: message.threadId,
|
|
494
|
+
}, 'Error in voice message handler');
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
async sendMessage(threadId, text) {
|
|
498
|
+
if (!this.bot) {
|
|
499
|
+
throw new Error('Telegram adapter not started');
|
|
500
|
+
}
|
|
501
|
+
const htmlText = markdownToTelegramHtml(text);
|
|
502
|
+
const chunks = splitMessage(htmlText, { maxLength: 4000, addContinuationMarker: false });
|
|
503
|
+
for (const chunk of chunks) {
|
|
504
|
+
await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'HTML' });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
onButtonCallback(handler) {
|
|
508
|
+
this.buttonHandler = handler;
|
|
509
|
+
}
|
|
510
|
+
async sendApprovalCard(threadId, prompt) {
|
|
511
|
+
if (!this.bot)
|
|
512
|
+
throw new Error('Telegram adapter not started');
|
|
513
|
+
const text = renderApprovalCardHtml(prompt);
|
|
514
|
+
const reply_markup = renderApprovalKeyboard(prompt);
|
|
515
|
+
const sent = await this.bot.api.sendMessage(threadId, text, {
|
|
516
|
+
parse_mode: 'HTML',
|
|
517
|
+
reply_markup,
|
|
518
|
+
});
|
|
519
|
+
return { messageId: sent.message_id.toString() };
|
|
520
|
+
}
|
|
521
|
+
async editApprovalCard(threadId, messageId, outcome) {
|
|
522
|
+
if (!this.bot)
|
|
523
|
+
return;
|
|
524
|
+
const numericId = Number.parseInt(messageId, 10);
|
|
525
|
+
if (!Number.isFinite(numericId)) {
|
|
526
|
+
log.warn({ messageId }, 'editApprovalCard: non-numeric messageId');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const text = renderApprovalOutcomeHtml(outcome);
|
|
530
|
+
try {
|
|
531
|
+
await this.bot.api.editMessageText(threadId, numericId, text, {
|
|
532
|
+
parse_mode: 'HTML',
|
|
533
|
+
// Omit reply_markup → buttons stay. We want to drop them, so pass
|
|
534
|
+
// an empty inline_keyboard to clear.
|
|
535
|
+
reply_markup: { inline_keyboard: [] },
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
// Common: "message is not modified", "message can't be edited" (>48h),
|
|
540
|
+
// "message to edit not found". All non-fatal — bus already resolved.
|
|
541
|
+
log.warn({ err: String(err), messageId }, 'editApprovalCard failed (non-fatal)');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async sendTyping(threadId, isTyping) {
|
|
545
|
+
if (!this.bot) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (isTyping) {
|
|
549
|
+
// Send initial typing action
|
|
550
|
+
try {
|
|
551
|
+
await this.bot.api.sendChatAction(threadId, 'typing');
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Ignore errors during typing
|
|
555
|
+
}
|
|
556
|
+
// Clear any existing interval
|
|
557
|
+
const existing = this.typingIntervals.get(threadId);
|
|
558
|
+
if (existing) {
|
|
559
|
+
clearInterval(existing);
|
|
560
|
+
}
|
|
561
|
+
// Set up periodic refresh every 4 seconds (Telegram expires after ~5s)
|
|
562
|
+
const interval = setInterval(async () => {
|
|
563
|
+
try {
|
|
564
|
+
await this.bot.api.sendChatAction(threadId, 'typing');
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
// Ignore errors during typing refresh
|
|
568
|
+
}
|
|
569
|
+
}, 4000);
|
|
570
|
+
this.typingIntervals.set(threadId, interval);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
// Clear the refresh interval
|
|
574
|
+
const interval = this.typingIntervals.get(threadId);
|
|
575
|
+
if (interval) {
|
|
576
|
+
clearInterval(interval);
|
|
577
|
+
this.typingIntervals.delete(threadId);
|
|
578
|
+
}
|
|
579
|
+
// Note: Telegram has no "cancel" action - typing just expires
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function escapeHtml(s) {
|
|
584
|
+
// M12: cover quote characters too. Most usages place the value in a text
|
|
585
|
+
// node where `'` and `"` are harmless, but the approval-card template
|
|
586
|
+
// builds attributes like <a href="..."> with user-derived values, where
|
|
587
|
+
// an unescaped quote could close the attribute and inject markup.
|
|
588
|
+
return s.replace(/&/g, '&')
|
|
589
|
+
.replace(/</g, '<')
|
|
590
|
+
.replace(/>/g, '>')
|
|
591
|
+
.replace(/"/g, '"')
|
|
592
|
+
.replace(/'/g, ''');
|
|
593
|
+
}
|
|
594
|
+
function formatHm(d) {
|
|
595
|
+
const hh = d.getHours().toString().padStart(2, '0');
|
|
596
|
+
const mm = d.getMinutes().toString().padStart(2, '0');
|
|
597
|
+
return `${hh}:${mm}`;
|
|
598
|
+
}
|
|
599
|
+
function renderApprovalCardHtml(p) {
|
|
600
|
+
const tool = escapeHtml(p.toolName);
|
|
601
|
+
const input = escapeHtml(p.inputJson);
|
|
602
|
+
const reqShort = escapeHtml(p.reqId.slice(0, 8));
|
|
603
|
+
if (p.mode === 'auto-allow') {
|
|
604
|
+
const sec = p.graceSeconds ?? 5;
|
|
605
|
+
return [
|
|
606
|
+
`⏱ <b>自动放行中</b>(${sec}s 后执行)`,
|
|
607
|
+
`工具:<b>${tool}</b>`,
|
|
608
|
+
`入参:<pre>${input}</pre>`,
|
|
609
|
+
`点 ❌ 拒绝可同时撤销该工具的自动放行规则`,
|
|
610
|
+
`<i>req: ${reqShort}</i>`,
|
|
611
|
+
].join('\n');
|
|
612
|
+
}
|
|
613
|
+
return [
|
|
614
|
+
`🔐 <b>工具调用审批</b>`,
|
|
615
|
+
`工具:<b>${tool}</b>`,
|
|
616
|
+
`入参:<pre>${input}</pre>`,
|
|
617
|
+
`<i>req: ${reqShort}</i>`,
|
|
618
|
+
].join('\n');
|
|
619
|
+
}
|
|
620
|
+
function renderApprovalKeyboard(p) {
|
|
621
|
+
const r = p.reqId;
|
|
622
|
+
if (p.mode === 'auto-allow') {
|
|
623
|
+
return {
|
|
624
|
+
inline_keyboard: [[
|
|
625
|
+
{ text: '❌ 拒绝(撤销规则)', callback_data: `apv:${r}:n` },
|
|
626
|
+
]],
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
inline_keyboard: [
|
|
631
|
+
[
|
|
632
|
+
{ text: '✅ 同意', callback_data: `apv:${r}:y` },
|
|
633
|
+
{ text: '❌ 拒绝', callback_data: `apv:${r}:n` },
|
|
634
|
+
],
|
|
635
|
+
[
|
|
636
|
+
{ text: '🛡 本会话自动放行同类', callback_data: `apv:${r}:a` },
|
|
637
|
+
],
|
|
638
|
+
],
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function renderApprovalOutcomeHtml(o) {
|
|
642
|
+
const t = formatHm(o.atDate);
|
|
643
|
+
const by = o.byUserDisplay ? ` · by ${escapeHtml(o.byUserDisplay)}` : '';
|
|
644
|
+
switch (o.decision) {
|
|
645
|
+
case 'allowed': return `✅ <b>已批准</b> · ${t}${by}`;
|
|
646
|
+
case 'allowed-pinned': return `🛡 <b>已批准并加入自动放行</b> · ${t}${by}`;
|
|
647
|
+
case 'denied': return `❌ <b>已拒绝</b> · ${t}${by}`;
|
|
648
|
+
case 'denied-revoked': return `❌ <b>已拒绝并撤销自动放行</b> · ${t}${by}`;
|
|
649
|
+
case 'expired': return `⏱ <b>已过期</b> · ${t}`;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
export const telegramAdapter = new TelegramAdapter();
|
|
653
|
+
//# sourceMappingURL=telegram-adapter.js.map
|