agim-cli 1.0.1

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 (453) hide show
  1. package/CHANGELOG.md +1234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/README.zh-CN.md +414 -0
  5. package/dist/cli-ui/cmd-handlers.d.ts +11 -0
  6. package/dist/cli-ui/cmd-handlers.d.ts.map +1 -0
  7. package/dist/cli-ui/cmd-handlers.js +240 -0
  8. package/dist/cli-ui/cmd-handlers.js.map +1 -0
  9. package/dist/cli-ui/config-wizard.d.ts +3 -0
  10. package/dist/cli-ui/config-wizard.d.ts.map +1 -0
  11. package/dist/cli-ui/config-wizard.js +851 -0
  12. package/dist/cli-ui/config-wizard.js.map +1 -0
  13. package/dist/cli-ui/entry-menu.d.ts +28 -0
  14. package/dist/cli-ui/entry-menu.d.ts.map +1 -0
  15. package/dist/cli-ui/entry-menu.js +50 -0
  16. package/dist/cli-ui/entry-menu.js.map +1 -0
  17. package/dist/cli-ui/env-file.d.ts +35 -0
  18. package/dist/cli-ui/env-file.d.ts.map +1 -0
  19. package/dist/cli-ui/env-file.js +163 -0
  20. package/dist/cli-ui/env-file.js.map +1 -0
  21. package/dist/cli-ui/i18n.d.ts +204 -0
  22. package/dist/cli-ui/i18n.d.ts.map +1 -0
  23. package/dist/cli-ui/i18n.js +455 -0
  24. package/dist/cli-ui/i18n.js.map +1 -0
  25. package/dist/cli-ui/lang-picker.d.ts +10 -0
  26. package/dist/cli-ui/lang-picker.d.ts.map +1 -0
  27. package/dist/cli-ui/lang-picker.js +33 -0
  28. package/dist/cli-ui/lang-picker.js.map +1 -0
  29. package/dist/cli-ui/paths.d.ts +4 -0
  30. package/dist/cli-ui/paths.d.ts.map +1 -0
  31. package/dist/cli-ui/paths.js +11 -0
  32. package/dist/cli-ui/paths.js.map +1 -0
  33. package/dist/cli-ui/prompts.d.ts +65 -0
  34. package/dist/cli-ui/prompts.d.ts.map +1 -0
  35. package/dist/cli-ui/prompts.js +125 -0
  36. package/dist/cli-ui/prompts.js.map +1 -0
  37. package/dist/cli-ui/service.d.ts +41 -0
  38. package/dist/cli-ui/service.d.ts.map +1 -0
  39. package/dist/cli-ui/service.js +241 -0
  40. package/dist/cli-ui/service.js.map +1 -0
  41. package/dist/cli.d.ts +3 -0
  42. package/dist/cli.d.ts.map +1 -0
  43. package/dist/cli.js +1143 -0
  44. package/dist/cli.js.map +1 -0
  45. package/dist/core/acp-server.d.ts +8 -0
  46. package/dist/core/acp-server.d.ts.map +1 -0
  47. package/dist/core/acp-server.js +266 -0
  48. package/dist/core/acp-server.js.map +1 -0
  49. package/dist/core/agent-base.d.ts +94 -0
  50. package/dist/core/agent-base.d.ts.map +1 -0
  51. package/dist/core/agent-base.js +373 -0
  52. package/dist/core/agent-base.js.map +1 -0
  53. package/dist/core/agent-cwd.d.ts +48 -0
  54. package/dist/core/agent-cwd.d.ts.map +1 -0
  55. package/dist/core/agent-cwd.js +181 -0
  56. package/dist/core/agent-cwd.js.map +1 -0
  57. package/dist/core/agent-helper.d.ts +65 -0
  58. package/dist/core/agent-helper.d.ts.map +1 -0
  59. package/dist/core/agent-helper.js +150 -0
  60. package/dist/core/agent-helper.js.map +1 -0
  61. package/dist/core/agim-paths.d.ts +10 -0
  62. package/dist/core/agim-paths.d.ts.map +1 -0
  63. package/dist/core/agim-paths.js +64 -0
  64. package/dist/core/agim-paths.js.map +1 -0
  65. package/dist/core/approval-bus.d.ts +300 -0
  66. package/dist/core/approval-bus.d.ts.map +1 -0
  67. package/dist/core/approval-bus.js +990 -0
  68. package/dist/core/approval-bus.js.map +1 -0
  69. package/dist/core/approval-router.d.ts +101 -0
  70. package/dist/core/approval-router.d.ts.map +1 -0
  71. package/dist/core/approval-router.js +540 -0
  72. package/dist/core/approval-router.js.map +1 -0
  73. package/dist/core/audit-log.d.ts +55 -0
  74. package/dist/core/audit-log.d.ts.map +1 -0
  75. package/dist/core/audit-log.js +203 -0
  76. package/dist/core/audit-log.js.map +1 -0
  77. package/dist/core/bgjob-reader.d.ts +65 -0
  78. package/dist/core/bgjob-reader.d.ts.map +1 -0
  79. package/dist/core/bgjob-reader.js +212 -0
  80. package/dist/core/bgjob-reader.js.map +1 -0
  81. package/dist/core/circuit-breaker.d.ts +37 -0
  82. package/dist/core/circuit-breaker.d.ts.map +1 -0
  83. package/dist/core/circuit-breaker.js +115 -0
  84. package/dist/core/circuit-breaker.js.map +1 -0
  85. package/dist/core/commands/agent.d.ts +4 -0
  86. package/dist/core/commands/agent.d.ts.map +1 -0
  87. package/dist/core/commands/agent.js +40 -0
  88. package/dist/core/commands/agent.js.map +1 -0
  89. package/dist/core/commands/approval.d.ts +3 -0
  90. package/dist/core/commands/approval.d.ts.map +1 -0
  91. package/dist/core/commands/approval.js +85 -0
  92. package/dist/core/commands/approval.js.map +1 -0
  93. package/dist/core/commands/audit.d.ts +3 -0
  94. package/dist/core/commands/audit.d.ts.map +1 -0
  95. package/dist/core/commands/audit.js +84 -0
  96. package/dist/core/commands/audit.js.map +1 -0
  97. package/dist/core/commands/builtin.d.ts +3 -0
  98. package/dist/core/commands/builtin.d.ts.map +1 -0
  99. package/dist/core/commands/builtin.js +304 -0
  100. package/dist/core/commands/builtin.js.map +1 -0
  101. package/dist/core/commands/cron.d.ts +3 -0
  102. package/dist/core/commands/cron.d.ts.map +1 -0
  103. package/dist/core/commands/cron.js +128 -0
  104. package/dist/core/commands/cron.js.map +1 -0
  105. package/dist/core/commands/job.d.ts +3 -0
  106. package/dist/core/commands/job.d.ts.map +1 -0
  107. package/dist/core/commands/job.js +195 -0
  108. package/dist/core/commands/job.js.map +1 -0
  109. package/dist/core/commands/memo.d.ts +3 -0
  110. package/dist/core/commands/memo.d.ts.map +1 -0
  111. package/dist/core/commands/memo.js +151 -0
  112. package/dist/core/commands/memo.js.map +1 -0
  113. package/dist/core/commands/model.d.ts +9 -0
  114. package/dist/core/commands/model.d.ts.map +1 -0
  115. package/dist/core/commands/model.js +183 -0
  116. package/dist/core/commands/model.js.map +1 -0
  117. package/dist/core/commands/plan.d.ts +3 -0
  118. package/dist/core/commands/plan.d.ts.map +1 -0
  119. package/dist/core/commands/plan.js +75 -0
  120. package/dist/core/commands/plan.js.map +1 -0
  121. package/dist/core/commands/remind.d.ts +3 -0
  122. package/dist/core/commands/remind.d.ts.map +1 -0
  123. package/dist/core/commands/remind.js +271 -0
  124. package/dist/core/commands/remind.js.map +1 -0
  125. package/dist/core/commands/router.d.ts +3 -0
  126. package/dist/core/commands/router.d.ts.map +1 -0
  127. package/dist/core/commands/router.js +71 -0
  128. package/dist/core/commands/router.js.map +1 -0
  129. package/dist/core/commands/sessions.d.ts +3 -0
  130. package/dist/core/commands/sessions.d.ts.map +1 -0
  131. package/dist/core/commands/sessions.js +88 -0
  132. package/dist/core/commands/sessions.js.map +1 -0
  133. package/dist/core/commands/stats.d.ts +3 -0
  134. package/dist/core/commands/stats.d.ts.map +1 -0
  135. package/dist/core/commands/stats.js +73 -0
  136. package/dist/core/commands/stats.js.map +1 -0
  137. package/dist/core/commands/think.d.ts +3 -0
  138. package/dist/core/commands/think.d.ts.map +1 -0
  139. package/dist/core/commands/think.js +28 -0
  140. package/dist/core/commands/think.js.map +1 -0
  141. package/dist/core/commands/workspaces.d.ts +3 -0
  142. package/dist/core/commands/workspaces.d.ts.map +1 -0
  143. package/dist/core/commands/workspaces.js +47 -0
  144. package/dist/core/commands/workspaces.js.map +1 -0
  145. package/dist/core/config-schema.d.ts +60 -0
  146. package/dist/core/config-schema.d.ts.map +1 -0
  147. package/dist/core/config-schema.js +75 -0
  148. package/dist/core/config-schema.js.map +1 -0
  149. package/dist/core/coord-systems.d.ts +65 -0
  150. package/dist/core/coord-systems.d.ts.map +1 -0
  151. package/dist/core/coord-systems.js +229 -0
  152. package/dist/core/coord-systems.js.map +1 -0
  153. package/dist/core/cron.d.ts +29 -0
  154. package/dist/core/cron.d.ts.map +1 -0
  155. package/dist/core/cron.js +184 -0
  156. package/dist/core/cron.js.map +1 -0
  157. package/dist/core/event-bus.d.ts +80 -0
  158. package/dist/core/event-bus.d.ts.map +1 -0
  159. package/dist/core/event-bus.js +62 -0
  160. package/dist/core/event-bus.js.map +1 -0
  161. package/dist/core/intent-llm.d.ts +27 -0
  162. package/dist/core/intent-llm.d.ts.map +1 -0
  163. package/dist/core/intent-llm.js +170 -0
  164. package/dist/core/intent-llm.js.map +1 -0
  165. package/dist/core/intent.d.ts +12 -0
  166. package/dist/core/intent.d.ts.map +1 -0
  167. package/dist/core/intent.js +187 -0
  168. package/dist/core/intent.js.map +1 -0
  169. package/dist/core/job-board.d.ts +82 -0
  170. package/dist/core/job-board.d.ts.map +1 -0
  171. package/dist/core/job-board.js +379 -0
  172. package/dist/core/job-board.js.map +1 -0
  173. package/dist/core/location-context.d.ts +32 -0
  174. package/dist/core/location-context.d.ts.map +1 -0
  175. package/dist/core/location-context.js +69 -0
  176. package/dist/core/location-context.js.map +1 -0
  177. package/dist/core/location-token.d.ts +57 -0
  178. package/dist/core/location-token.d.ts.map +1 -0
  179. package/dist/core/location-token.js +128 -0
  180. package/dist/core/location-token.js.map +1 -0
  181. package/dist/core/logger.d.ts +6 -0
  182. package/dist/core/logger.d.ts.map +1 -0
  183. package/dist/core/logger.js +54 -0
  184. package/dist/core/logger.js.map +1 -0
  185. package/dist/core/memo-rpc.d.ts +13 -0
  186. package/dist/core/memo-rpc.d.ts.map +1 -0
  187. package/dist/core/memo-rpc.js +288 -0
  188. package/dist/core/memo-rpc.js.map +1 -0
  189. package/dist/core/memos.d.ts +163 -0
  190. package/dist/core/memos.d.ts.map +1 -0
  191. package/dist/core/memos.js +502 -0
  192. package/dist/core/memos.js.map +1 -0
  193. package/dist/core/metrics.d.ts +55 -0
  194. package/dist/core/metrics.d.ts.map +1 -0
  195. package/dist/core/metrics.js +291 -0
  196. package/dist/core/metrics.js.map +1 -0
  197. package/dist/core/onboarding.d.ts +99 -0
  198. package/dist/core/onboarding.d.ts.map +1 -0
  199. package/dist/core/onboarding.js +426 -0
  200. package/dist/core/onboarding.js.map +1 -0
  201. package/dist/core/pending-reminder.d.ts +25 -0
  202. package/dist/core/pending-reminder.d.ts.map +1 -0
  203. package/dist/core/pending-reminder.js +53 -0
  204. package/dist/core/pending-reminder.js.map +1 -0
  205. package/dist/core/rate-limiter.d.ts +44 -0
  206. package/dist/core/rate-limiter.d.ts.map +1 -0
  207. package/dist/core/rate-limiter.js +115 -0
  208. package/dist/core/rate-limiter.js.map +1 -0
  209. package/dist/core/registry.d.ts +32 -0
  210. package/dist/core/registry.d.ts.map +1 -0
  211. package/dist/core/registry.js +126 -0
  212. package/dist/core/registry.js.map +1 -0
  213. package/dist/core/remind-intent.d.ts +25 -0
  214. package/dist/core/remind-intent.d.ts.map +1 -0
  215. package/dist/core/remind-intent.js +196 -0
  216. package/dist/core/remind-intent.js.map +1 -0
  217. package/dist/core/reminder-rpc.d.ts +17 -0
  218. package/dist/core/reminder-rpc.d.ts.map +1 -0
  219. package/dist/core/reminder-rpc.js +169 -0
  220. package/dist/core/reminder-rpc.js.map +1 -0
  221. package/dist/core/reminders.d.ts +159 -0
  222. package/dist/core/reminders.d.ts.map +1 -0
  223. package/dist/core/reminders.js +977 -0
  224. package/dist/core/reminders.js.map +1 -0
  225. package/dist/core/router.d.ts +55 -0
  226. package/dist/core/router.d.ts.map +1 -0
  227. package/dist/core/router.js +497 -0
  228. package/dist/core/router.js.map +1 -0
  229. package/dist/core/schedule.d.ts +65 -0
  230. package/dist/core/schedule.d.ts.map +1 -0
  231. package/dist/core/schedule.js +323 -0
  232. package/dist/core/schedule.js.map +1 -0
  233. package/dist/core/session.d.ts +182 -0
  234. package/dist/core/session.d.ts.map +1 -0
  235. package/dist/core/session.js +807 -0
  236. package/dist/core/session.js.map +1 -0
  237. package/dist/core/sqlite-helper.d.ts +37 -0
  238. package/dist/core/sqlite-helper.d.ts.map +1 -0
  239. package/dist/core/sqlite-helper.js +79 -0
  240. package/dist/core/sqlite-helper.js.map +1 -0
  241. package/dist/core/transcribe.d.ts +25 -0
  242. package/dist/core/transcribe.d.ts.map +1 -0
  243. package/dist/core/transcribe.js +217 -0
  244. package/dist/core/transcribe.js.map +1 -0
  245. package/dist/core/types.d.ts +360 -0
  246. package/dist/core/types.d.ts.map +1 -0
  247. package/dist/core/types.js +3 -0
  248. package/dist/core/types.js.map +1 -0
  249. package/dist/core/workspace.d.ts +67 -0
  250. package/dist/core/workspace.d.ts.map +1 -0
  251. package/dist/core/workspace.js +113 -0
  252. package/dist/core/workspace.js.map +1 -0
  253. package/dist/index.d.ts +5 -0
  254. package/dist/index.d.ts.map +1 -0
  255. package/dist/index.js +6 -0
  256. package/dist/index.js.map +1 -0
  257. package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
  258. package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
  259. package/dist/plugins/agents/acp/acp-adapter.js +49 -0
  260. package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
  261. package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
  262. package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
  263. package/dist/plugins/agents/acp/acp-client.js +177 -0
  264. package/dist/plugins/agents/acp/acp-client.js.map +1 -0
  265. package/dist/plugins/agents/acp/discovery.d.ts +19 -0
  266. package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
  267. package/dist/plugins/agents/acp/discovery.js +111 -0
  268. package/dist/plugins/agents/acp/discovery.js.map +1 -0
  269. package/dist/plugins/agents/acp/index.d.ts +4 -0
  270. package/dist/plugins/agents/acp/index.d.ts.map +1 -0
  271. package/dist/plugins/agents/acp/index.js +4 -0
  272. package/dist/plugins/agents/acp/index.js.map +1 -0
  273. package/dist/plugins/agents/acp/types.d.ts +62 -0
  274. package/dist/plugins/agents/acp/types.d.ts.map +1 -0
  275. package/dist/plugins/agents/acp/types.js +5 -0
  276. package/dist/plugins/agents/acp/types.js.map +1 -0
  277. package/dist/plugins/agents/claude-code/index.d.ts +25 -0
  278. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
  279. package/dist/plugins/agents/claude-code/index.js +184 -0
  280. package/dist/plugins/agents/claude-code/index.js.map +1 -0
  281. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +59 -0
  282. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
  283. package/dist/plugins/agents/claude-code/mcp-approval-server.js +645 -0
  284. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
  285. package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts +28 -0
  286. package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts.map +1 -0
  287. package/dist/plugins/agents/codex/build-mcp-cli-args.js +74 -0
  288. package/dist/plugins/agents/codex/build-mcp-cli-args.js.map +1 -0
  289. package/dist/plugins/agents/codex/index.d.ts +53 -0
  290. package/dist/plugins/agents/codex/index.d.ts.map +1 -0
  291. package/dist/plugins/agents/codex/index.js +341 -0
  292. package/dist/plugins/agents/codex/index.js.map +1 -0
  293. package/dist/plugins/agents/copilot/index.d.ts +35 -0
  294. package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
  295. package/dist/plugins/agents/copilot/index.js +182 -0
  296. package/dist/plugins/agents/copilot/index.js.map +1 -0
  297. package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts +11 -0
  298. package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts.map +1 -0
  299. package/dist/plugins/agents/opencode/ensure-mcp-config.js +100 -0
  300. package/dist/plugins/agents/opencode/ensure-mcp-config.js.map +1 -0
  301. package/dist/plugins/agents/opencode/index.d.ts +5 -0
  302. package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
  303. package/dist/plugins/agents/opencode/index.js +30 -0
  304. package/dist/plugins/agents/opencode/index.js.map +1 -0
  305. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +166 -0
  306. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
  307. package/dist/plugins/agents/opencode/opencode-http-adapter.js +682 -0
  308. package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
  309. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +32 -0
  310. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
  311. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +137 -0
  312. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
  313. package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
  314. package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
  315. package/dist/plugins/agents/opencode/serve-manager.js +194 -0
  316. package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
  317. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts +57 -0
  318. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts.map +1 -0
  319. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js +409 -0
  320. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js.map +1 -0
  321. package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts +48 -0
  322. package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts.map +1 -0
  323. package/dist/plugins/messengers/dingtalk/dingtalk-client.js +236 -0
  324. package/dist/plugins/messengers/dingtalk/dingtalk-client.js.map +1 -0
  325. package/dist/plugins/messengers/dingtalk/index.d.ts +3 -0
  326. package/dist/plugins/messengers/dingtalk/index.d.ts.map +1 -0
  327. package/dist/plugins/messengers/dingtalk/index.js +3 -0
  328. package/dist/plugins/messengers/dingtalk/index.js.map +1 -0
  329. package/dist/plugins/messengers/dingtalk/link-coords.d.ts +23 -0
  330. package/dist/plugins/messengers/dingtalk/link-coords.d.ts.map +1 -0
  331. package/dist/plugins/messengers/dingtalk/link-coords.js +89 -0
  332. package/dist/plugins/messengers/dingtalk/link-coords.js.map +1 -0
  333. package/dist/plugins/messengers/dingtalk/media-store.d.ts +16 -0
  334. package/dist/plugins/messengers/dingtalk/media-store.d.ts.map +1 -0
  335. package/dist/plugins/messengers/dingtalk/media-store.js +77 -0
  336. package/dist/plugins/messengers/dingtalk/media-store.js.map +1 -0
  337. package/dist/plugins/messengers/dingtalk/types.d.ts +82 -0
  338. package/dist/plugins/messengers/dingtalk/types.d.ts.map +1 -0
  339. package/dist/plugins/messengers/dingtalk/types.js +14 -0
  340. package/dist/plugins/messengers/dingtalk/types.js.map +1 -0
  341. package/dist/plugins/messengers/discord/discord-adapter.d.ts +21 -0
  342. package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
  343. package/dist/plugins/messengers/discord/discord-adapter.js +238 -0
  344. package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
  345. package/dist/plugins/messengers/discord/index.d.ts +4 -0
  346. package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
  347. package/dist/plugins/messengers/discord/index.js +4 -0
  348. package/dist/plugins/messengers/discord/index.js.map +1 -0
  349. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
  350. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
  351. package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
  352. package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
  353. package/dist/plugins/messengers/discord/types.d.ts +9 -0
  354. package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
  355. package/dist/plugins/messengers/discord/types.js +3 -0
  356. package/dist/plugins/messengers/discord/types.js.map +1 -0
  357. package/dist/plugins/messengers/email/email-adapter.d.ts +33 -0
  358. package/dist/plugins/messengers/email/email-adapter.d.ts.map +1 -0
  359. package/dist/plugins/messengers/email/email-adapter.js +137 -0
  360. package/dist/plugins/messengers/email/email-adapter.js.map +1 -0
  361. package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
  362. package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
  363. package/dist/plugins/messengers/feishu/card-builder.js +89 -0
  364. package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
  365. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +23 -0
  366. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
  367. package/dist/plugins/messengers/feishu/feishu-adapter.js +250 -0
  368. package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
  369. package/dist/plugins/messengers/feishu/feishu-client.d.ts +43 -0
  370. package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
  371. package/dist/plugins/messengers/feishu/feishu-client.js +118 -0
  372. package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
  373. package/dist/plugins/messengers/feishu/index.d.ts +4 -0
  374. package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
  375. package/dist/plugins/messengers/feishu/index.js +4 -0
  376. package/dist/plugins/messengers/feishu/index.js.map +1 -0
  377. package/dist/plugins/messengers/feishu/types.d.ts +113 -0
  378. package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
  379. package/dist/plugins/messengers/feishu/types.js +4 -0
  380. package/dist/plugins/messengers/feishu/types.js.map +1 -0
  381. package/dist/plugins/messengers/telegram/index.d.ts +4 -0
  382. package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
  383. package/dist/plugins/messengers/telegram/index.js +4 -0
  384. package/dist/plugins/messengers/telegram/index.js.map +1 -0
  385. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
  386. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
  387. package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
  388. package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
  389. package/dist/plugins/messengers/telegram/media-download.d.ts +59 -0
  390. package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
  391. package/dist/plugins/messengers/telegram/media-download.js +228 -0
  392. package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
  393. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +77 -0
  394. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
  395. package/dist/plugins/messengers/telegram/telegram-adapter.js +880 -0
  396. package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
  397. package/dist/plugins/messengers/telegram/types.d.ts +47 -0
  398. package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
  399. package/dist/plugins/messengers/telegram/types.js +3 -0
  400. package/dist/plugins/messengers/telegram/types.js.map +1 -0
  401. package/dist/plugins/messengers/wechat/context-store.d.ts +18 -0
  402. package/dist/plugins/messengers/wechat/context-store.d.ts.map +1 -0
  403. package/dist/plugins/messengers/wechat/context-store.js +105 -0
  404. package/dist/plugins/messengers/wechat/context-store.js.map +1 -0
  405. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +71 -0
  406. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
  407. package/dist/plugins/messengers/wechat/ilink-adapter.js +664 -0
  408. package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
  409. package/dist/plugins/messengers/wechat/ilink-client.d.ts +75 -0
  410. package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
  411. package/dist/plugins/messengers/wechat/ilink-client.js +331 -0
  412. package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
  413. package/dist/plugins/messengers/wechat/ilink-types.d.ts +181 -0
  414. package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
  415. package/dist/plugins/messengers/wechat/ilink-types.js +22 -0
  416. package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
  417. package/dist/plugins/messengers/wechat/media-download.d.ts +32 -0
  418. package/dist/plugins/messengers/wechat/media-download.d.ts.map +1 -0
  419. package/dist/plugins/messengers/wechat/media-download.js +78 -0
  420. package/dist/plugins/messengers/wechat/media-download.js.map +1 -0
  421. package/dist/scripts/migrate-gcj02-to-wgs84.d.ts +3 -0
  422. package/dist/scripts/migrate-gcj02-to-wgs84.d.ts.map +1 -0
  423. package/dist/scripts/migrate-gcj02-to-wgs84.js +52 -0
  424. package/dist/scripts/migrate-gcj02-to-wgs84.js.map +1 -0
  425. package/dist/utils/backoff.d.ts +35 -0
  426. package/dist/utils/backoff.d.ts.map +1 -0
  427. package/dist/utils/backoff.js +59 -0
  428. package/dist/utils/backoff.js.map +1 -0
  429. package/dist/utils/cross-platform.d.ts +26 -0
  430. package/dist/utils/cross-platform.d.ts.map +1 -0
  431. package/dist/utils/cross-platform.js +58 -0
  432. package/dist/utils/cross-platform.js.map +1 -0
  433. package/dist/utils/message-split.d.ts +14 -0
  434. package/dist/utils/message-split.d.ts.map +1 -0
  435. package/dist/utils/message-split.js +65 -0
  436. package/dist/utils/message-split.js.map +1 -0
  437. package/dist/utils/safe-equal.d.ts +2 -0
  438. package/dist/utils/safe-equal.d.ts.map +1 -0
  439. package/dist/utils/safe-equal.js +11 -0
  440. package/dist/utils/safe-equal.js.map +1 -0
  441. package/dist/web/public/_app.js +196 -0
  442. package/dist/web/public/index.html +936 -0
  443. package/dist/web/public/loc.html +305 -0
  444. package/dist/web/public/login.html +106 -0
  445. package/dist/web/public/memos.html +271 -0
  446. package/dist/web/public/reminders.html +234 -0
  447. package/dist/web/public/settings.html +1355 -0
  448. package/dist/web/public/tasks.html +1835 -0
  449. package/dist/web/server.d.ts +12 -0
  450. package/dist/web/server.d.ts.map +1 -0
  451. package/dist/web/server.js +2399 -0
  452. package/dist/web/server.js.map +1 -0
  453. package/package.json +92 -0
@@ -0,0 +1,880 @@
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
+ import { setLocationContext } from '../../../core/location-context.js';
11
+ import { normalizeIncomingCoords } from '../../../core/coord-systems.js';
12
+ const log = rootLogger.child({ component: 'telegram' });
13
+ export class TelegramAdapter {
14
+ name = 'telegram';
15
+ bot = null;
16
+ config = null;
17
+ messageHandler;
18
+ buttonHandler;
19
+ isRunning = false;
20
+ typingIntervals = new Map();
21
+ // grammy 的 bot.start() 长轮询偶尔会被一次网络抖动 wedge —— 既不报错也不
22
+ // resolve,看起来还在跑但不再 fetch updates,TG 那边 pending_update_count
23
+ // 一路涨。watchdog 周期性 ping getMe;连续多次失败就强制 stop+start,让
24
+ // 卡死的 polling loop 重置。
25
+ watchdogTimer;
26
+ consecutivePingFailures = 0;
27
+ static WATCHDOG_INTERVAL_MS = 60_000;
28
+ static WATCHDOG_FAILURE_THRESHOLD = 3; // ~3min 无响应就重启 polling
29
+ mediaCleanupTimer;
30
+ static MEDIA_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // hourly
31
+ async start() {
32
+ // Load config
33
+ const { readFile } = await import('node:fs/promises');
34
+ const { join } = await import('node:path');
35
+ const { AGIM_HOME } = await import('../../../core/agim-paths.js');
36
+ const configPath = join(AGIM_HOME, 'config.json');
37
+ try {
38
+ const data = await readFile(configPath, 'utf-8');
39
+ const config = JSON.parse(data);
40
+ this.config = config.telegram;
41
+ }
42
+ catch {
43
+ throw new Error('Telegram config not found. Run "im-hub config telegram" first.');
44
+ }
45
+ if (!this.config?.botToken) {
46
+ throw new Error('Telegram bot token not configured. Run "im-hub config telegram" first.');
47
+ }
48
+ // Initialize bot
49
+ this.bot = new Bot(this.config.botToken);
50
+ // Set up message handler.
51
+ //
52
+ // CRITICAL: do NOT await messageHandler here. grammy processes updates
53
+ // sequentially per chat — if the handler awaits an agent run that's
54
+ // waiting on a separate IM-side approval reply, polling stalls and the
55
+ // user's approval reply piles up in TG's pending_update_count, never
56
+ // reaching us. Fire-and-forget mirrors how the WeChat ilink adapter
57
+ // dispatches (ilink-adapter.ts:258).
58
+ this.bot.on('message:text', (ctx) => {
59
+ // Ignore messages from bots
60
+ if (ctx.message.from.is_bot)
61
+ return;
62
+ if (!this.messageHandler)
63
+ return;
64
+ const message = {
65
+ id: ctx.message.message_id.toString(),
66
+ threadId: ctx.chat.id.toString(),
67
+ userId: ctx.message.from?.id?.toString() || 'unknown',
68
+ text: ctx.message.text || '',
69
+ timestamp: new Date(ctx.message.date * 1000),
70
+ channelId: this.config?.channelId || 'default',
71
+ };
72
+ const msgCtx = {
73
+ message,
74
+ platform: 'telegram',
75
+ channelId: this.config?.channelId || 'default',
76
+ };
77
+ this.messageHandler(msgCtx).catch((err) => {
78
+ const errMsg = err instanceof Error ? err.message : String(err);
79
+ const stack = err instanceof Error ? err.stack : undefined;
80
+ log.error({ err: errMsg, stack, threadId: message.threadId }, 'Error in message handler');
81
+ });
82
+ });
83
+ // Media handlers — TG photos and image documents. We await the download
84
+ // inside the handler (it's bounded; typical TG photo is < 1 s, hard cap
85
+ // 20 MB) so the resulting Message reflects the image being on disk before
86
+ // we kick off the agent. messageHandler itself is fire-and-forget for the
87
+ // same reasons as message:text above. Order across photo / text within a
88
+ // chat is preserved because grammy serializes updates per chat.
89
+ this.bot.on('message:photo', (ctx) => {
90
+ if (ctx.message.from?.is_bot)
91
+ return;
92
+ if (!this.messageHandler)
93
+ return;
94
+ // Largest size is the last entry — TG ships scaled-down siblings for
95
+ // bandwidth-conscious clients which we ignore.
96
+ const photo = ctx.message.photo[ctx.message.photo.length - 1];
97
+ void this.handleMediaUpload(ctx, photo.file_id, undefined, ctx.message.caption ?? '');
98
+ });
99
+ this.bot.on('message:document', (ctx) => {
100
+ if (ctx.message.from?.is_bot)
101
+ return;
102
+ if (!this.messageHandler)
103
+ return;
104
+ const doc = ctx.message.document;
105
+ // Image documents → media upload path. Audio documents → voice path.
106
+ // Everything else (PDF / Word / Excel / zip / txt / arbitrary binaries)
107
+ // → generic file path so the agent at least sees the attachment.
108
+ if (doc.mime_type?.startsWith('image/')) {
109
+ void this.handleMediaUpload(ctx, doc.file_id, doc.mime_type, ctx.message.caption ?? '');
110
+ return;
111
+ }
112
+ if (doc.mime_type?.startsWith('audio/')) {
113
+ // Document type has no `duration` field even when MIME is audio/*;
114
+ // only message:voice / message:audio carry it. Pass undefined.
115
+ void this.handleVoiceUpload(ctx, doc.file_id, doc.mime_type, ctx.message.caption ?? '', undefined);
116
+ return;
117
+ }
118
+ void this.handleFileUpload(ctx, doc.file_id, doc.mime_type, ctx.message.caption ?? '', doc.file_name);
119
+ });
120
+ // Voice messages (the mic-button "press and hold" recording, OGG OPUS).
121
+ // Caption is rare on voice but TG allows it.
122
+ this.bot.on('message:voice', (ctx) => {
123
+ if (ctx.message.from?.is_bot)
124
+ return;
125
+ if (!this.messageHandler)
126
+ return;
127
+ const v = ctx.message.voice;
128
+ void this.handleVoiceUpload(ctx, v.file_id, v.mime_type, ctx.message.caption ?? '', v.duration);
129
+ });
130
+ // Audio messages (a music file or the "Audio" attachment button).
131
+ this.bot.on('message:audio', (ctx) => {
132
+ if (ctx.message.from?.is_bot)
133
+ return;
134
+ if (!this.messageHandler)
135
+ return;
136
+ const a = ctx.message.audio;
137
+ void this.handleVoiceUpload(ctx, a.file_id, a.mime_type, ctx.message.caption ?? '', a.duration);
138
+ });
139
+ // Video sent via "Video" attachment (auto-compressed). Sending "as file"
140
+ // arrives as message:document and goes through handleFileUpload instead.
141
+ this.bot.on('message:video', (ctx) => {
142
+ if (ctx.message.from?.is_bot)
143
+ return;
144
+ if (!this.messageHandler)
145
+ return;
146
+ const v = ctx.message.video;
147
+ void this.handleVideoUpload(ctx, v.file_id, v.mime_type, ctx.message.caption ?? '', v.duration);
148
+ });
149
+ // Round video notes (40s max, recorded via the round-camera button).
150
+ this.bot.on('message:video_note', (ctx) => {
151
+ if (ctx.message.from?.is_bot)
152
+ return;
153
+ if (!this.messageHandler)
154
+ return;
155
+ const v = ctx.message.video_note;
156
+ void this.handleVideoUpload(ctx, v.file_id, undefined, '', v.duration);
157
+ });
158
+ // Animations (silent looping MP4 / GIF). Same surface as video.
159
+ this.bot.on('message:animation', (ctx) => {
160
+ if (ctx.message.from?.is_bot)
161
+ return;
162
+ if (!this.messageHandler)
163
+ return;
164
+ const a = ctx.message.animation;
165
+ void this.handleVideoUpload(ctx, a.file_id, a.mime_type, ctx.message.caption ?? '', a.duration);
166
+ });
167
+ // Location pins (📎 → Location → Send My Current Location / Choose on Map).
168
+ //
169
+ // Architecture (agent-driven, matches WeChat's H5 flow):
170
+ // The adapter does NOT make any decisions about the user's intent.
171
+ // It just stashes the coords in core/location-context (TTL 5 min)
172
+ // and sends a quiet ack. The user's NEXT text message in this thread
173
+ // gets prefixed with `[user just shared location: lat=…, lng=…, …]`
174
+ // by cli.ts before dispatching to the agent. The agent then decides
175
+ // whether to save (with what label) or just acknowledge.
176
+ //
177
+ // Why no "记下来 / 跳过 / 回名字" prompt anymore: those forced the
178
+ // adapter into a deterministic state machine and ran a brittle
179
+ // heuristic noun extractor on the reply text. Several edge cases
180
+ // produced wrong `what` values ("记下", "一个好吃的日", etc.). The
181
+ // agent has LLM judgment + full conversation context — let it decide.
182
+ //
183
+ // Coordinate-system handling lives in core/coord-systems.ts.
184
+ // Telegram default: pass-through (WGS-84). Most senders are Telegram
185
+ // Desktop / Android-without-China-GMS / users outside China — all of
186
+ // which deliver raw WGS-84. iOS Core Location in China applies the
187
+ // GCJ offset; users hitting that case set IMHUB_TELEGRAM_COORDS_GCJ02=1.
188
+ this.bot.on('message:location', (botCtx) => {
189
+ if (botCtx.message.from?.is_bot)
190
+ return;
191
+ const loc = botCtx.message.location;
192
+ const threadId = botCtx.chat.id.toString();
193
+ const threadKey = `telegram:default:${threadId}`;
194
+ const norm = normalizeIncomingCoords('telegram-native', loc.latitude, loc.longitude);
195
+ setLocationContext(threadKey, {
196
+ lat: norm.lat,
197
+ lng: norm.lng,
198
+ accuracy: typeof loc.horizontal_accuracy === 'number' ? loc.horizontal_accuracy : undefined,
199
+ });
200
+ const accuracyHint = typeof loc.horizontal_accuracy === 'number'
201
+ ? `(精度 ±${Math.round(loc.horizontal_accuracy)}m)`
202
+ : '';
203
+ void botCtx.reply(`📍 位置已收到 (${norm.lat.toFixed(5)}, ${norm.lng.toFixed(5)})${accuracyHint}。告诉我要做什么。`).catch((err) => {
204
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, 'failed to ack location share');
205
+ });
206
+ log.info({
207
+ event: 'telegram.location.received',
208
+ threadId,
209
+ raw_lat: loc.latitude, raw_lng: loc.longitude,
210
+ wgs_lat: norm.lat, wgs_lng: norm.lng,
211
+ applied: norm.applied,
212
+ });
213
+ });
214
+ // Venues — same coord handling, plus the venue title + address ride
215
+ // through as structured fields so the agent sees them in the annotation.
216
+ this.bot.on('message:venue', (botCtx) => {
217
+ if (botCtx.message.from?.is_bot)
218
+ return;
219
+ const v = botCtx.message.venue;
220
+ const threadId = botCtx.chat.id.toString();
221
+ const threadKey = `telegram:default:${threadId}`;
222
+ const norm = normalizeIncomingCoords('telegram-native', v.location.latitude, v.location.longitude);
223
+ setLocationContext(threadKey, {
224
+ lat: norm.lat,
225
+ lng: norm.lng,
226
+ venueLabel: v.title,
227
+ venueAddress: v.address,
228
+ });
229
+ const addrLine = v.address ? `\n ${v.address}` : '';
230
+ void botCtx.reply(`📍 「${v.title}」 已收到${addrLine}\n告诉我要做什么。`).catch((err) => {
231
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, 'failed to ack venue share');
232
+ });
233
+ log.info({ event: 'telegram.venue.received', threadId, title: v.title, applied: norm.applied });
234
+ });
235
+ // Inline-button taps (approval cards). Same fire-and-forget discipline
236
+ // as message:text — buttonHandler may resolve a pending approval which
237
+ // calls back into editApprovalCard; we don't want that to block grammy's
238
+ // sequential update queue.
239
+ //
240
+ // Telegram requires answerCallbackQuery within ~1s or the client shows
241
+ // a spinner. We wrap ack() so the handler can call it explicitly when
242
+ // it has a meaningful toast; if it doesn't, we send an empty ack at the
243
+ // end as a safety net (idempotent — TG ignores the second call).
244
+ this.bot.on('callback_query:data', (ctx) => {
245
+ if (!this.buttonHandler) {
246
+ void ctx.answerCallbackQuery({ text: '系统未就绪' }).catch(() => { });
247
+ return;
248
+ }
249
+ const data = ctx.callbackQuery.data;
250
+ const from = ctx.callbackQuery.from;
251
+ const msg = ctx.callbackQuery.message;
252
+ if (!msg) {
253
+ void ctx.answerCallbackQuery({ text: '消息已不可用' }).catch(() => { });
254
+ return;
255
+ }
256
+ // Optional allowlist gate. Empty / missing list = allow anyone (matches
257
+ // the text-reply path). When configured, refuse with a toast — the
258
+ // refusal does NOT resolve the pending, so an authorized user can
259
+ // still click later.
260
+ const allowlist = this.config?.approvalAllowlist;
261
+ if (allowlist && allowlist.length > 0) {
262
+ const fromIdStr = from.id.toString();
263
+ if (!allowlist.includes(fromIdStr)) {
264
+ log.warn({
265
+ event: 'telegram.approval.unauthorized_click',
266
+ userId: fromIdStr,
267
+ username: from.username,
268
+ chatType: msg.chat.type,
269
+ chatId: msg.chat.id,
270
+ }, 'Unauthorized button click rejected by allowlist');
271
+ void ctx.answerCallbackQuery({ text: '无权审批此请求' }).catch(() => { });
272
+ return;
273
+ }
274
+ }
275
+ let acked = false;
276
+ const cb = {
277
+ data,
278
+ threadId: msg.chat.id.toString(),
279
+ userId: from.id.toString(),
280
+ userDisplay: from.username ? `@${from.username}` : (from.first_name || from.id.toString()),
281
+ messageId: msg.message_id.toString(),
282
+ ack: async (text) => {
283
+ if (acked)
284
+ return;
285
+ acked = true;
286
+ try {
287
+ await ctx.answerCallbackQuery(text ? { text } : undefined);
288
+ }
289
+ catch (err) {
290
+ log.warn({ err: String(err) }, 'answerCallbackQuery failed');
291
+ }
292
+ },
293
+ };
294
+ this.buttonHandler(cb)
295
+ .catch((err) => {
296
+ log.error({ err: String(err), data }, 'Error in button handler');
297
+ })
298
+ .finally(() => {
299
+ if (!acked) {
300
+ // Safety net so the user's TG client doesn't keep spinning when
301
+ // the handler forgot to ack.
302
+ void ctx.answerCallbackQuery().catch(() => { });
303
+ }
304
+ });
305
+ });
306
+ // Start bot in background. grammy's start() uses long polling and resolves
307
+ // only when polling stops. We wrap it in a self-healing loop so that:
308
+ // 1. an unexpected resolve while still isRunning → restart polling
309
+ // 2. a thrown error → log it and retry after a short backoff
310
+ // Combined with the watchdog (below), this defends against the silent
311
+ // wedge mode where bot.start() neither resolves nor rejects but stops
312
+ // fetching updates.
313
+ log.info('Starting bot with long polling');
314
+ this.isRunning = true;
315
+ void this.runPollingLoop();
316
+ this.startWatchdog();
317
+ this.startMediaCleanup();
318
+ log.info('Telegram adapter started');
319
+ }
320
+ /** Run media cleanup once now (so a long-running im-hub doesn't accumulate
321
+ * files indefinitely when restarts are infrequent) and then hourly. The
322
+ * hourly cadence matches the typical 7-day TTL with plenty of slack. */
323
+ startMediaCleanup() {
324
+ if (this.mediaCleanupTimer)
325
+ clearInterval(this.mediaCleanupTimer);
326
+ void cleanupOldMedia().catch((err) => {
327
+ log.warn({ err: String(err), event: 'telegram.media.cleanup_failed' }, 'startup media cleanup failed');
328
+ });
329
+ this.mediaCleanupTimer = setInterval(() => {
330
+ void cleanupOldMedia().catch((err) => {
331
+ log.warn({ err: String(err), event: 'telegram.media.cleanup_failed' }, 'periodic media cleanup failed');
332
+ });
333
+ }, TelegramAdapter.MEDIA_CLEANUP_INTERVAL_MS);
334
+ }
335
+ async runPollingLoop() {
336
+ // M9: exponential backoff with jitter replaces the previous fixed 2s
337
+ // (unexpected resolve) / 5s (error) delays. Auth-revoked states no
338
+ // longer hammer the API at a fixed cadence, and fleet-wide network
339
+ // recovery doesn't lock-step its reconnects.
340
+ //
341
+ // baseMs = 2_000 preserves the previous "first error → ~2s" feel so
342
+ // operators eyeballing logs during normal restarts see the same
343
+ // ballpark; capMs = 60_000 prevents indefinite drift past 1 minute.
344
+ const backoff = new Backoff({ baseMs: 2_000, capMs: 60_000, jitter: 0.5 });
345
+ /** Reset the backoff if bot.start() ran healthy for at least this long
346
+ * before erroring. Without this, a single transient error at hour 12
347
+ * of operation would still queue the same exponential tail as a
348
+ * startup-time crash loop. 30 s is comfortably longer than any
349
+ * realistic handshake / first-fetch round-trip. */
350
+ const HEALTHY_RUN_THRESHOLD_MS = 30_000;
351
+ while (this.isRunning && this.bot) {
352
+ const startedAt = Date.now();
353
+ try {
354
+ await this.bot.start();
355
+ if (!this.isRunning) {
356
+ log.info('Bot stopped gracefully');
357
+ return;
358
+ }
359
+ // bot.start() resolved while we still want to be running — that's
360
+ // grammy's silent-wedge mode (or an internal early return). Treat
361
+ // it as a fault and back off before restarting.
362
+ if (Date.now() - startedAt >= HEALTHY_RUN_THRESHOLD_MS)
363
+ backoff.reset();
364
+ const delayMs = backoff.nextDelayMs();
365
+ log.warn({
366
+ event: 'telegram.polling.unexpected_stop',
367
+ attempt: backoff.currentAttempt(),
368
+ delayMs,
369
+ }, 'bot.start() resolved while still running; restarting after backoff');
370
+ await new Promise((r) => setTimeout(r, delayMs));
371
+ }
372
+ catch (err) {
373
+ if (!this.isRunning)
374
+ return;
375
+ if (Date.now() - startedAt >= HEALTHY_RUN_THRESHOLD_MS)
376
+ backoff.reset();
377
+ const delayMs = backoff.nextDelayMs();
378
+ log.error({
379
+ err: err instanceof Error ? err.message : String(err),
380
+ event: 'telegram.polling.error',
381
+ attempt: backoff.currentAttempt(),
382
+ delayMs,
383
+ }, 'Bot polling error; restarting after backoff');
384
+ await new Promise((r) => setTimeout(r, delayMs));
385
+ }
386
+ }
387
+ }
388
+ startWatchdog() {
389
+ if (this.watchdogTimer)
390
+ clearInterval(this.watchdogTimer);
391
+ this.consecutivePingFailures = 0;
392
+ this.watchdogTimer = setInterval(async () => {
393
+ if (!this.isRunning || !this.bot)
394
+ return;
395
+ try {
396
+ await this.bot.api.getMe();
397
+ if (this.consecutivePingFailures > 0) {
398
+ log.info({ event: 'telegram.watchdog.recovered' }, 'getMe ping recovered');
399
+ }
400
+ this.consecutivePingFailures = 0;
401
+ }
402
+ catch (err) {
403
+ this.consecutivePingFailures += 1;
404
+ log.warn({
405
+ event: 'telegram.watchdog.ping_failed',
406
+ consecutive: this.consecutivePingFailures,
407
+ err: err instanceof Error ? err.message : String(err),
408
+ }, 'Watchdog getMe ping failed');
409
+ if (this.consecutivePingFailures >= TelegramAdapter.WATCHDOG_FAILURE_THRESHOLD) {
410
+ log.error({ event: 'telegram.watchdog.restarting_polling' }, 'Polling appears wedged; forcing bot.stop() to trigger restart');
411
+ this.consecutivePingFailures = 0;
412
+ try {
413
+ await this.bot.stop();
414
+ }
415
+ catch { /* ignore */ }
416
+ // runPollingLoop will see bot.start() resolve and restart it.
417
+ }
418
+ }
419
+ }, TelegramAdapter.WATCHDOG_INTERVAL_MS);
420
+ }
421
+ async stop() {
422
+ this.isRunning = false;
423
+ if (this.watchdogTimer) {
424
+ clearInterval(this.watchdogTimer);
425
+ this.watchdogTimer = undefined;
426
+ }
427
+ if (this.mediaCleanupTimer) {
428
+ clearInterval(this.mediaCleanupTimer);
429
+ this.mediaCleanupTimer = undefined;
430
+ }
431
+ // Clean up all typing intervals
432
+ for (const interval of this.typingIntervals.values()) {
433
+ clearInterval(interval);
434
+ }
435
+ this.typingIntervals.clear();
436
+ if (this.bot) {
437
+ await this.bot.stop();
438
+ this.bot = null;
439
+ }
440
+ log.info('Telegram adapter stopped');
441
+ }
442
+ onMessage(handler) {
443
+ this.messageHandler = handler;
444
+ }
445
+ /**
446
+ * Download a TG photo / image-document, save it under MEDIA_ROOT, and surface
447
+ * the result to messageHandler as a Message whose `text` includes the
448
+ * caption (if any) plus a "[图片附件:/path/x.jpg]" marker — claude-code
449
+ * picks that up and uses Read to view it.
450
+ *
451
+ * On download failure we still call messageHandler with a "[图片下载失败]"
452
+ * marker so the user's interaction isn't silently dropped — they can resend
453
+ * or be told what went wrong.
454
+ */
455
+ async handleMediaUpload(ctx, fileId, mime, caption) {
456
+ if (!this.bot || !ctx.chat || !ctx.message)
457
+ return;
458
+ const chatId = ctx.chat.id;
459
+ const msgId = ctx.message.message_id;
460
+ let attachmentLine;
461
+ const botToken = this.config?.botToken;
462
+ // Scrub the bot token from any string before it reaches a log line or a
463
+ // user-facing chat reply. The token rides inside the file URL we hand to
464
+ // curl, and curl's stderr can echo the URL on TLS / DNS failures.
465
+ const scrub = (s) => botToken ? s.split(botToken).join('[REDACTED]') : s;
466
+ try {
467
+ const file = await this.bot.api.getFile(fileId);
468
+ if (!file.file_path)
469
+ throw new Error('TG returned no file_path');
470
+ const ext = pickExtension(file.file_path, mime);
471
+ const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
472
+ const { path } = await downloadToMediaRoot({
473
+ url,
474
+ subdir: `telegram/${chatId}`,
475
+ filename: `${msgId}.${ext}`,
476
+ });
477
+ attachmentLine = `[图片附件:${path}]`;
478
+ }
479
+ catch (err) {
480
+ const msg = scrub(err instanceof Error ? err.message : String(err));
481
+ log.warn({ event: 'telegram.media.download_failed', err: msg, chatId, msgId }, 'media download failed');
482
+ attachmentLine = `[图片附件下载失败:${msg}]`;
483
+ }
484
+ const text = caption ? `${caption}\n\n${attachmentLine}` : attachmentLine;
485
+ const message = {
486
+ id: msgId.toString(),
487
+ threadId: chatId.toString(),
488
+ userId: ctx.message.from?.id?.toString() || 'unknown',
489
+ text,
490
+ timestamp: new Date(ctx.message.date * 1000),
491
+ channelId: this.config?.channelId || 'default',
492
+ };
493
+ const msgCtx = {
494
+ message,
495
+ platform: 'telegram',
496
+ channelId: this.config?.channelId || 'default',
497
+ };
498
+ if (!this.messageHandler)
499
+ return;
500
+ this.messageHandler(msgCtx).catch((err) => {
501
+ log.error({
502
+ err: err instanceof Error ? err.message : String(err),
503
+ threadId: message.threadId,
504
+ }, 'Error in media message handler');
505
+ });
506
+ }
507
+ /**
508
+ * Download a TG voice / audio message, transcribe it via whichever provider
509
+ * is configured (OpenAI Whisper or whisper.cpp), and surface the transcript
510
+ * to messageHandler as Message.text. The downloaded audio file path is
511
+ * also included so the agent can reference it (e.g. send it back, replay).
512
+ *
513
+ * Failures are surfaced as text markers, not silent drops:
514
+ * - download failure → "[语音附件下载失败:…]"
515
+ * - no provider configured → "[语音附件未转写:未配置 OPENAI_API_KEY 或 IMHUB_WHISPERCPP_BIN]"
516
+ * - transcribe error → "[语音转写失败(${provider}):…]"
517
+ *
518
+ * Since transcription can take 5-30s on a slow CPU + whisper.cpp medium,
519
+ * we fire-and-forget the entire operation so grammy's update queue keeps
520
+ * draining for other chats. Within this chat, ordering is still serialized
521
+ * by grammy.
522
+ */
523
+ async handleVoiceUpload(ctx, fileId, mime, caption, durationSec) {
524
+ if (!this.bot || !ctx.chat || !ctx.message)
525
+ return;
526
+ const chatId = ctx.chat.id;
527
+ const msgId = ctx.message.message_id;
528
+ let savedPath = null;
529
+ let downloadErr = null;
530
+ const botToken = this.config?.botToken;
531
+ const scrub = (s) => botToken ? s.split(botToken).join('[REDACTED]') : s;
532
+ try {
533
+ const file = await this.bot.api.getFile(fileId);
534
+ if (!file.file_path)
535
+ throw new Error('TG returned no file_path');
536
+ const ext = pickExtension(file.file_path, mime);
537
+ const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
538
+ const { path } = await downloadToMediaRoot({
539
+ url,
540
+ subdir: `telegram/${chatId}`,
541
+ filename: `${msgId}.${ext}`,
542
+ });
543
+ savedPath = path;
544
+ }
545
+ catch (err) {
546
+ downloadErr = scrub(err instanceof Error ? err.message : String(err));
547
+ log.warn({ event: 'telegram.voice.download_failed', err: downloadErr, chatId, msgId }, 'voice download failed');
548
+ }
549
+ let voiceLine;
550
+ if (!savedPath) {
551
+ voiceLine = `[语音附件下载失败:${downloadErr}]`;
552
+ }
553
+ else if (detectProvider() === 'none') {
554
+ voiceLine = `[语音附件未转写(未配置 OPENAI_API_KEY 或 IMHUB_WHISPERCPP_BIN):${savedPath}]`;
555
+ }
556
+ else {
557
+ try {
558
+ const result = await transcribe(savedPath, { language: 'zh' });
559
+ const dur = durationSec != null ? `${durationSec}s, ` : '';
560
+ voiceLine = [
561
+ `[语音转写(${dur}provider=${result.provider}, ${result.elapsedMs}ms):`,
562
+ result.text || '(空)',
563
+ `源文件:${savedPath}]`,
564
+ ].join('\n');
565
+ }
566
+ catch (err) {
567
+ const reason = err instanceof TranscribeError
568
+ ? `${err.provider}: ${err.reason}`
569
+ : err instanceof Error ? err.message : String(err);
570
+ voiceLine = `[语音转写失败(${reason})\n源文件:${savedPath}]`;
571
+ }
572
+ }
573
+ const text = caption ? `${caption}\n\n${voiceLine}` : voiceLine;
574
+ const message = {
575
+ id: msgId.toString(),
576
+ threadId: chatId.toString(),
577
+ userId: ctx.message.from?.id?.toString() || 'unknown',
578
+ text,
579
+ timestamp: new Date(ctx.message.date * 1000),
580
+ channelId: this.config?.channelId || 'default',
581
+ };
582
+ const msgCtx = {
583
+ message,
584
+ platform: 'telegram',
585
+ channelId: this.config?.channelId || 'default',
586
+ };
587
+ if (!this.messageHandler)
588
+ return;
589
+ this.messageHandler(msgCtx).catch((err) => {
590
+ log.error({
591
+ err: err instanceof Error ? err.message : String(err),
592
+ threadId: message.threadId,
593
+ }, 'Error in voice message handler');
594
+ });
595
+ }
596
+ /**
597
+ * Download an arbitrary file (PDF / Word / archive / binary) and surface
598
+ * `[文件附件:${path} (${displayName}, ${size})]` to the agent. Same
599
+ * download infra as media/voice; differs only in marker format and
600
+ * filename handling — we keep the user's `file_name` (sanitised) so the
601
+ * on-disk file stays recognisable, falling back to `${msgId}.${ext}` when
602
+ * Telegram doesn't supply one.
603
+ */
604
+ async handleFileUpload(ctx, fileId, mime, caption, fileName) {
605
+ if (!this.bot || !ctx.chat || !ctx.message)
606
+ return;
607
+ const chatId = ctx.chat.id;
608
+ const msgId = ctx.message.message_id;
609
+ const displayName = fileName || '未命名文件';
610
+ let attachmentLine;
611
+ const botToken = this.config?.botToken;
612
+ const scrub = (s) => botToken ? s.split(botToken).join('[REDACTED]') : s;
613
+ try {
614
+ const file = await this.bot.api.getFile(fileId);
615
+ if (!file.file_path)
616
+ throw new Error('TG returned no file_path');
617
+ const ext = pickExtension(file.file_path, mime);
618
+ // Sanitise the user-supplied file_name. downloadToMediaRoot also
619
+ // rejects '/', '\\', '..' but we replace defensively so the disk name
620
+ // stays meaningful instead of erroring out on weird names.
621
+ const safeName = fileName
622
+ ? fileName.replace(/[/\\]/g, '_').replace(/\.\.+/g, '.').slice(0, 200)
623
+ : `${msgId}.${ext}`;
624
+ // Prefix with msgId so two uploads of the same name in a chat don't
625
+ // collide. Underscore separator stays human-readable.
626
+ const onDiskName = `${msgId}_${safeName}`;
627
+ const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
628
+ const { path, bytes } = await downloadToMediaRoot({
629
+ url,
630
+ subdir: `telegram/${chatId}`,
631
+ filename: onDiskName,
632
+ });
633
+ const sizeStr = bytes >= 1024 * 1024
634
+ ? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
635
+ : `${(bytes / 1024).toFixed(0)} KB`;
636
+ attachmentLine = `[文件附件:${path} (${displayName}, ${sizeStr})]`;
637
+ }
638
+ catch (err) {
639
+ const msg = scrub(err instanceof Error ? err.message : String(err));
640
+ log.warn({ event: 'telegram.file.download_failed', err: msg, chatId, msgId }, 'file download failed');
641
+ attachmentLine = `[文件附件下载失败:${msg} (${displayName})]`;
642
+ }
643
+ const text = caption ? `${caption}\n\n${attachmentLine}` : attachmentLine;
644
+ const message = {
645
+ id: msgId.toString(),
646
+ threadId: chatId.toString(),
647
+ userId: ctx.message.from?.id?.toString() || 'unknown',
648
+ text,
649
+ timestamp: new Date(ctx.message.date * 1000),
650
+ channelId: this.config?.channelId || 'default',
651
+ };
652
+ const msgCtx = {
653
+ message,
654
+ platform: 'telegram',
655
+ channelId: this.config?.channelId || 'default',
656
+ };
657
+ if (!this.messageHandler)
658
+ return;
659
+ this.messageHandler(msgCtx).catch((err) => {
660
+ log.error({
661
+ err: err instanceof Error ? err.message : String(err),
662
+ threadId: message.threadId,
663
+ }, 'Error in file message handler');
664
+ });
665
+ }
666
+ /**
667
+ * Download a video / video_note / animation and surface
668
+ * `[视频附件:${path} (${dur}s, ${size})]` to the agent. Same caveats as
669
+ * handleFileUpload; we don't attempt frame extraction or transcription.
670
+ */
671
+ async handleVideoUpload(ctx, fileId, mime, caption, durationSec) {
672
+ if (!this.bot || !ctx.chat || !ctx.message)
673
+ return;
674
+ const chatId = ctx.chat.id;
675
+ const msgId = ctx.message.message_id;
676
+ let attachmentLine;
677
+ const botToken = this.config?.botToken;
678
+ const scrub = (s) => botToken ? s.split(botToken).join('[REDACTED]') : s;
679
+ try {
680
+ const file = await this.bot.api.getFile(fileId);
681
+ if (!file.file_path)
682
+ throw new Error('TG returned no file_path');
683
+ const ext = pickExtension(file.file_path, mime) || 'mp4';
684
+ const url = `https://api.telegram.org/file/bot${botToken}/${file.file_path}`;
685
+ const { path, bytes } = await downloadToMediaRoot({
686
+ url,
687
+ subdir: `telegram/${chatId}`,
688
+ filename: `${msgId}.${ext}`,
689
+ });
690
+ const dur = durationSec != null ? `${durationSec}s, ` : '';
691
+ const sizeStr = bytes >= 1024 * 1024
692
+ ? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
693
+ : `${(bytes / 1024).toFixed(0)} KB`;
694
+ attachmentLine = `[视频附件:${path} (${dur}${sizeStr})]`;
695
+ }
696
+ catch (err) {
697
+ const msg = scrub(err instanceof Error ? err.message : String(err));
698
+ log.warn({ event: 'telegram.video.download_failed', err: msg, chatId, msgId }, 'video download failed');
699
+ attachmentLine = `[视频附件下载失败:${msg}]`;
700
+ }
701
+ const text = caption ? `${caption}\n\n${attachmentLine}` : attachmentLine;
702
+ const message = {
703
+ id: msgId.toString(),
704
+ threadId: chatId.toString(),
705
+ userId: ctx.message.from?.id?.toString() || 'unknown',
706
+ text,
707
+ timestamp: new Date(ctx.message.date * 1000),
708
+ channelId: this.config?.channelId || 'default',
709
+ };
710
+ const msgCtx = {
711
+ message,
712
+ platform: 'telegram',
713
+ channelId: this.config?.channelId || 'default',
714
+ };
715
+ if (!this.messageHandler)
716
+ return;
717
+ this.messageHandler(msgCtx).catch((err) => {
718
+ log.error({
719
+ err: err instanceof Error ? err.message : String(err),
720
+ threadId: message.threadId,
721
+ }, 'Error in video message handler');
722
+ });
723
+ }
724
+ async sendMessage(threadId, text) {
725
+ if (!this.bot) {
726
+ throw new Error('Telegram adapter not started');
727
+ }
728
+ const htmlText = markdownToTelegramHtml(text);
729
+ const chunks = splitMessage(htmlText, { maxLength: 4000, addContinuationMarker: false });
730
+ for (const chunk of chunks) {
731
+ await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'HTML' });
732
+ }
733
+ }
734
+ onButtonCallback(handler) {
735
+ this.buttonHandler = handler;
736
+ }
737
+ async sendApprovalCard(threadId, prompt) {
738
+ if (!this.bot)
739
+ throw new Error('Telegram adapter not started');
740
+ const text = renderApprovalCardHtml(prompt);
741
+ const reply_markup = renderApprovalKeyboard(prompt);
742
+ const sent = await this.bot.api.sendMessage(threadId, text, {
743
+ parse_mode: 'HTML',
744
+ reply_markup,
745
+ });
746
+ return { messageId: sent.message_id.toString() };
747
+ }
748
+ async editApprovalCard(threadId, messageId, outcome) {
749
+ if (!this.bot)
750
+ return;
751
+ const numericId = Number.parseInt(messageId, 10);
752
+ if (!Number.isFinite(numericId)) {
753
+ log.warn({ messageId }, 'editApprovalCard: non-numeric messageId');
754
+ return;
755
+ }
756
+ const text = renderApprovalOutcomeHtml(outcome);
757
+ try {
758
+ await this.bot.api.editMessageText(threadId, numericId, text, {
759
+ parse_mode: 'HTML',
760
+ // Omit reply_markup → buttons stay. We want to drop them, so pass
761
+ // an empty inline_keyboard to clear.
762
+ reply_markup: { inline_keyboard: [] },
763
+ });
764
+ }
765
+ catch (err) {
766
+ // Common: "message is not modified", "message can't be edited" (>48h),
767
+ // "message to edit not found". All non-fatal — bus already resolved.
768
+ log.warn({ err: String(err), messageId }, 'editApprovalCard failed (non-fatal)');
769
+ }
770
+ }
771
+ async sendTyping(threadId, isTyping) {
772
+ if (!this.bot) {
773
+ return;
774
+ }
775
+ if (isTyping) {
776
+ // Send initial typing action
777
+ try {
778
+ await this.bot.api.sendChatAction(threadId, 'typing');
779
+ }
780
+ catch {
781
+ // Ignore errors during typing
782
+ }
783
+ // Clear any existing interval
784
+ const existing = this.typingIntervals.get(threadId);
785
+ if (existing) {
786
+ clearInterval(existing);
787
+ }
788
+ // Set up periodic refresh every 4 seconds (Telegram expires after ~5s)
789
+ const interval = setInterval(async () => {
790
+ try {
791
+ await this.bot?.api.sendChatAction(threadId, 'typing');
792
+ }
793
+ catch {
794
+ // Ignore errors during typing refresh
795
+ }
796
+ }, 4000);
797
+ this.typingIntervals.set(threadId, interval);
798
+ }
799
+ else {
800
+ // Clear the refresh interval
801
+ const interval = this.typingIntervals.get(threadId);
802
+ if (interval) {
803
+ clearInterval(interval);
804
+ this.typingIntervals.delete(threadId);
805
+ }
806
+ // Note: Telegram has no "cancel" action - typing just expires
807
+ }
808
+ }
809
+ }
810
+ function escapeHtml(s) {
811
+ // M12: cover quote characters too. Most usages place the value in a text
812
+ // node where `'` and `"` are harmless, but the approval-card template
813
+ // builds attributes like <a href="..."> with user-derived values, where
814
+ // an unescaped quote could close the attribute and inject markup.
815
+ return s.replace(/&/g, '&amp;')
816
+ .replace(/</g, '&lt;')
817
+ .replace(/>/g, '&gt;')
818
+ .replace(/"/g, '&quot;')
819
+ .replace(/'/g, '&#39;');
820
+ }
821
+ function formatHm(d) {
822
+ const hh = d.getHours().toString().padStart(2, '0');
823
+ const mm = d.getMinutes().toString().padStart(2, '0');
824
+ return `${hh}:${mm}`;
825
+ }
826
+ function renderApprovalCardHtml(p) {
827
+ const tool = escapeHtml(p.toolName);
828
+ const input = escapeHtml(p.inputJson);
829
+ const reqShort = escapeHtml(p.reqId.slice(0, 8));
830
+ if (p.mode === 'auto-allow') {
831
+ const sec = p.graceSeconds ?? 5;
832
+ return [
833
+ `⏱ <b>自动放行中</b>(${sec}s 后执行)`,
834
+ `工具:<b>${tool}</b>`,
835
+ `入参:<pre>${input}</pre>`,
836
+ `点 ❌ 拒绝可同时撤销该工具的自动放行规则`,
837
+ `<i>req: ${reqShort}</i>`,
838
+ ].join('\n');
839
+ }
840
+ return [
841
+ `🔐 <b>工具调用审批</b>`,
842
+ `工具:<b>${tool}</b>`,
843
+ `入参:<pre>${input}</pre>`,
844
+ `<i>req: ${reqShort}</i>`,
845
+ ].join('\n');
846
+ }
847
+ function renderApprovalKeyboard(p) {
848
+ const r = p.reqId;
849
+ if (p.mode === 'auto-allow') {
850
+ return {
851
+ inline_keyboard: [[
852
+ { text: '❌ 拒绝(撤销规则)', callback_data: `apv:${r}:n` },
853
+ ]],
854
+ };
855
+ }
856
+ return {
857
+ inline_keyboard: [
858
+ [
859
+ { text: '✅ 同意', callback_data: `apv:${r}:y` },
860
+ { text: '❌ 拒绝', callback_data: `apv:${r}:n` },
861
+ ],
862
+ [
863
+ { text: '🛡 本会话自动放行同类', callback_data: `apv:${r}:a` },
864
+ ],
865
+ ],
866
+ };
867
+ }
868
+ function renderApprovalOutcomeHtml(o) {
869
+ const t = formatHm(o.atDate);
870
+ const by = o.byUserDisplay ? ` · by ${escapeHtml(o.byUserDisplay)}` : '';
871
+ switch (o.decision) {
872
+ case 'allowed': return `✅ <b>已批准</b> · ${t}${by}`;
873
+ case 'allowed-pinned': return `🛡 <b>已批准并加入自动放行</b> · ${t}${by}`;
874
+ case 'denied': return `❌ <b>已拒绝</b> · ${t}${by}`;
875
+ case 'denied-revoked': return `❌ <b>已拒绝并撤销自动放行</b> · ${t}${by}`;
876
+ case 'expired': return `⏱ <b>已过期</b> · ${t}`;
877
+ }
878
+ }
879
+ export const telegramAdapter = new TelegramAdapter();
880
+ //# sourceMappingURL=telegram-adapter.js.map