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,664 @@
1
+ // WeChat iLink Bot API Adapter
2
+ // Implements MessengerAdapter using the iLink HTTP API
3
+ import { ILinkClient } from './ilink-client.js';
4
+ import { ILINK_ERRORS, ITEM_TYPE } from './ilink-types.js';
5
+ import { join } from 'node:path';
6
+ import { AGIM_HOME } from '../../../core/agim-paths.js';
7
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
8
+ import { logger as rootLogger } from '../../../core/logger.js';
9
+ import { Backoff } from '../../../utils/backoff.js';
10
+ import { downloadWechatMedia, extractCdnMedia, pickExtensionForImage, pickExtensionForVoice, pickExtensionForFile, pickExtensionForVideo, } from './media-download.js';
11
+ import { transcribe, detectProvider, TranscribeError } from '../../../core/transcribe.js';
12
+ import { persistContextToken, loadContextToken, loadAllContextTokens, cleanupExpiredTokens as cleanupExpiredTokensInStore, closeContextStore, } from './context-store.js';
13
+ const log = rootLogger.child({ component: 'wechat-ilink' });
14
+ const CREDENTIALS_FILE = join(AGIM_HOME, 'wechat-credentials.json');
15
+ const _POLL_TIMEOUT = 30000; // 30 seconds
16
+ const CONTEXT_TOKEN_TTL = 30 * 60 * 1000; // 30 minutes
17
+ const PROCESSED_MESSAGES_TTL = 60 * 1000; // 1 minute
18
+ export class ILinkWeChatAdapter {
19
+ name = 'wechat-ilink';
20
+ client;
21
+ messageHandler;
22
+ isRunning = false;
23
+ pollState = {
24
+ getUpdatesBuf: '',
25
+ isPolling: false,
26
+ lastPollTime: 0,
27
+ };
28
+ contextTokens = new Map();
29
+ typingTickets = new Map();
30
+ processedMessages = new Map(); // message_id -> timestamp
31
+ constructor() {
32
+ this.client = new ILinkClient();
33
+ }
34
+ // ============================================
35
+ // Lifecycle
36
+ // ============================================
37
+ async start() {
38
+ // Load saved credentials
39
+ const credentials = await this.loadCredentials();
40
+ if (credentials) {
41
+ this.client.setCredentials(credentials);
42
+ log.info('Credentials loaded from cache');
43
+ }
44
+ else {
45
+ throw new Error('No WeChat credentials found. Run "im-hub config wechat" first.');
46
+ }
47
+ // Warm in-memory context-token cache from persistent store. Without this,
48
+ // any user who messaged before the last restart but is silent now can't
49
+ // receive pushes (e.g. recurring reminders) until they speak again.
50
+ const warmed = loadAllContextTokens(CONTEXT_TOKEN_TTL, Date.now());
51
+ for (const t of warmed) {
52
+ this.contextTokens.set(t.userId, {
53
+ userId: t.userId,
54
+ contextToken: t.token,
55
+ timestamp: t.fetchedAt,
56
+ });
57
+ }
58
+ if (warmed.length > 0) {
59
+ log.info({ event: 'wechat.context.warm', count: warmed.length }, 'Warmed context-token cache from persistent store');
60
+ }
61
+ this.isRunning = true;
62
+ log.info('WeChat iLink adapter started');
63
+ // Start polling in background
64
+ this.startPolling();
65
+ }
66
+ async stop() {
67
+ this.isRunning = false;
68
+ this.client.clearCredentials();
69
+ closeContextStore();
70
+ log.info('WeChat iLink adapter stopped');
71
+ }
72
+ // ============================================
73
+ // Message Handling
74
+ // ============================================
75
+ onMessage(handler) {
76
+ this.messageHandler = handler;
77
+ }
78
+ async sendMessage(threadId, text) {
79
+ if (!this.client.hasCredentials()) {
80
+ throw new Error('WeChat adapter not authenticated');
81
+ }
82
+ // Extract user ID from threadId (format: user:xxx or room:xxx)
83
+ const userId = threadId.replace(/^(user|room):/, '');
84
+ // Get context token for this user
85
+ const contextToken = this.getContextToken(userId);
86
+ if (!contextToken) {
87
+ throw new Error('No context token available for this user');
88
+ }
89
+ // Split long messages
90
+ const chunks = this.splitMessage(text);
91
+ for (const chunk of chunks) {
92
+ const response = await this.client.sendMessage(userId, chunk, contextToken);
93
+ if (response.ret !== 0 && response.ret !== undefined) {
94
+ if (response.ret === ILINK_ERRORS.SESSION_EXPIRED) {
95
+ throw new Error('WeChat session expired. Please re-login.');
96
+ }
97
+ throw new Error(`Failed to send message: ${response.errmsg || response.ret}`);
98
+ }
99
+ }
100
+ }
101
+ // ============================================
102
+ // Typing Indicator
103
+ // ============================================
104
+ // Note: WeChat does NOT implement sendThinking on purpose. The previous
105
+ // "🤔 思考中…" placeholder was added in May 2026 as a fallback when the
106
+ // native typing indicator was deemed not prominent enough. After the
107
+ // remind-intent perf fix (avg reply latency 80s → 3s) the case
108
+ // collapsed: the bubble would linger forever (iLink has no recall API)
109
+ // and just polluted chat history. Native sendTyping below covers the
110
+ // immediate-feedback need on its own. Feishu still keeps sendThinking
111
+ // because its sendTyping is a no-op.
112
+ async sendTyping(threadId, isTyping) {
113
+ if (!this.client.hasCredentials()) {
114
+ return;
115
+ }
116
+ // Extract user ID from threadId (format: user:xxx or room:xxx)
117
+ const userId = threadId.replace(/^(user|room):/, '');
118
+ // Get or fetch typing ticket
119
+ let typingTicket = this.typingTickets.get(userId);
120
+ if (!typingTicket) {
121
+ const contextToken = this.getContextToken(userId);
122
+ const ticket = await this.client.getTypingTicket(userId, contextToken ?? undefined);
123
+ if (!ticket) {
124
+ log.warn({ userId }, 'Could not get typing ticket');
125
+ return;
126
+ }
127
+ typingTicket = ticket;
128
+ this.typingTickets.set(userId, ticket);
129
+ }
130
+ // Send typing status (1 = start, 2 = stop)
131
+ const status = isTyping ? 1 : 2;
132
+ const success = await this.client.sendTyping(userId, typingTicket, status);
133
+ if (!success) {
134
+ // Ticket might be expired, clear it for next attempt
135
+ this.typingTickets.delete(userId);
136
+ log.warn({ userId }, 'Failed to send typing indicator, cleared ticket');
137
+ }
138
+ }
139
+ // ============================================
140
+ // QR Code Login
141
+ // ============================================
142
+ /**
143
+ * Start QR code login flow
144
+ * Returns QR code URL and token for polling
145
+ */
146
+ async startQRLogin() {
147
+ const response = await this.client.getQRCode();
148
+ return {
149
+ qrUrl: response.qrcode_img_content,
150
+ qrToken: response.qrcode,
151
+ };
152
+ }
153
+ /**
154
+ * Poll QR code status until confirmed or expired
155
+ */
156
+ async waitForQRLogin(qrToken, onStatus) {
157
+ const maxAttempts = 120; // 2 minutes with 1s interval
158
+ let attempts = 0;
159
+ while (attempts < maxAttempts) {
160
+ const status = await this.client.getQRCodeStatus(qrToken);
161
+ switch (status.status) {
162
+ case 'wait':
163
+ onStatus?.('Waiting for scan...');
164
+ break;
165
+ case 'scaned':
166
+ onStatus?.('QR code scanned! Waiting for confirmation...');
167
+ break;
168
+ case 'confirmed':
169
+ if (status.bot_token && status.ilink_bot_id && status.ilink_user_id) {
170
+ const credentials = {
171
+ bot_token: status.bot_token,
172
+ baseUrl: status.baseurl || 'https://ilinkai.weixin.qq.com',
173
+ accountId: status.ilink_bot_id,
174
+ userId: status.ilink_user_id,
175
+ savedAt: new Date().toISOString(),
176
+ };
177
+ // Save credentials
178
+ await this.saveCredentials(credentials);
179
+ this.client.setCredentials(credentials);
180
+ onStatus?.('Login successful!');
181
+ return credentials;
182
+ }
183
+ break;
184
+ case 'expired':
185
+ onStatus?.('QR code expired');
186
+ return null;
187
+ }
188
+ // Wait 1 second before next poll
189
+ await new Promise((resolve) => setTimeout(resolve, 1000));
190
+ attempts++;
191
+ }
192
+ return null;
193
+ }
194
+ // ============================================
195
+ // Polling
196
+ // ============================================
197
+ startPolling() {
198
+ if (this.pollState.isPolling)
199
+ return;
200
+ this.pollState.isPolling = true;
201
+ this.pollLoop();
202
+ }
203
+ async pollLoop() {
204
+ log.info('Polling started');
205
+ let consecutiveFailures = 0;
206
+ let lastHeartbeatTime = Date.now();
207
+ const HEARTBEAT_INTERVAL_MS = 60 * 1000; // 60 seconds
208
+ // M9: shared exponential-with-jitter helper. The previous inline
209
+ // `1000 * Math.pow(2, n - 1)` was deterministic, so a fleet of im-hub
210
+ // instances recovering from a shared network event would all
211
+ // reconnect in lock-step. Backoff(jitter=0.5) spreads the storm
212
+ // across a ±50% window without changing the underlying cadence.
213
+ // Base 2_000 / cap 30_000 preserves the prior schedule's ballpark
214
+ // (2s, 4s, 8s, 16s, 30s capped) — first failure still has no
215
+ // extra wait via the `consecutiveFailures > 1` gate below.
216
+ const backoff = new Backoff({ baseMs: 2_000, capMs: 30_000, jitter: 0.5 });
217
+ while (this.isRunning) {
218
+ try {
219
+ const response = await this.client.getUpdates(this.pollState.getUpdatesBuf);
220
+ // Success — reset failure counter and backoff
221
+ consecutiveFailures = 0;
222
+ backoff.reset();
223
+ if (response.msgs?.length) {
224
+ log.debug({ count: response.msgs.length }, 'Received messages');
225
+ }
226
+ const isSuccess = response.ret === 0 || response.ret === undefined;
227
+ if (isSuccess) {
228
+ this.pollState.getUpdatesBuf = response.get_updates_buf;
229
+ if (response.msgs) {
230
+ for (const msg of response.msgs) {
231
+ this.handleIncomingMessage(msg).catch(err => log.error({ err: err instanceof Error ? err.message : String(err) }, 'Message handler error'));
232
+ }
233
+ }
234
+ }
235
+ else if (response.ret === ILINK_ERRORS.SESSION_EXPIRED) {
236
+ log.warn('WeChat session expired, attempting recovery...');
237
+ const recovered = await this.tryRefreshSession();
238
+ if (!recovered) {
239
+ log.error('WeChat session expired. Token refresh failed after retries — polling stopped.');
240
+ this.isRunning = false;
241
+ break;
242
+ }
243
+ log.info('Token refreshed successfully, resuming polling');
244
+ }
245
+ else {
246
+ log.warn({ ret: response.ret }, 'Unexpected getUpdates response code');
247
+ }
248
+ }
249
+ catch (err) {
250
+ const errMsg = err instanceof Error ? err.message : String(err);
251
+ consecutiveFailures++;
252
+ log.error({ err: errMsg, consecutiveFailures }, 'Poll error');
253
+ // First failure: no extra wait, just the regular 1s tail sleep.
254
+ // From the second consecutive failure on, draw the next delay
255
+ // from the jittered exponential schedule (2s/4s/8s/16s/30s ±50%).
256
+ if (consecutiveFailures > 1) {
257
+ const delayMs = backoff.nextDelayMs();
258
+ log.warn({ delayMs }, `Backing off after ${consecutiveFailures} consecutive failures`);
259
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
260
+ continue;
261
+ }
262
+ }
263
+ // Periodic heartbeat: getconfig probe (no side effect on the
264
+ // long-poll cursor, so it's safe to run alongside the loop).
265
+ if (Date.now() - lastHeartbeatTime >= HEARTBEAT_INTERVAL_MS) {
266
+ lastHeartbeatTime = Date.now();
267
+ this.sendHeartbeat().catch(err => log.warn({ err: err instanceof Error ? err.message : String(err) }, 'Heartbeat error'));
268
+ // L14: piggyback context-token sweep on the heartbeat tick (1/min).
269
+ // Cheap full scan — bounded by active user count.
270
+ this.cleanupExpiredContextTokens();
271
+ }
272
+ await new Promise((resolve) => setTimeout(resolve, 1000));
273
+ }
274
+ this.pollState.isPolling = false;
275
+ log.info('Polling stopped');
276
+ }
277
+ /**
278
+ * Attempt to recover from SESSION_EXPIRED. Tries a fresh getUpdates
279
+ * up to 3 times with linear backoff so a single network blip during
280
+ * recovery doesn't tear down the whole adapter (W-3).
281
+ */
282
+ async tryRefreshSession() {
283
+ const MAX_ATTEMPTS = 3;
284
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
285
+ try {
286
+ const response = await this.client.getUpdates('');
287
+ if (response.ret === 0 || response.ret === undefined) {
288
+ this.pollState.getUpdatesBuf = response.get_updates_buf;
289
+ if (response.msgs) {
290
+ for (const msg of response.msgs) {
291
+ if (msg.from_user_id && msg.context_token) {
292
+ this.setContextToken(msg.from_user_id, msg.context_token);
293
+ }
294
+ }
295
+ }
296
+ log.info({ attempt }, 'Session recovered after SESSION_EXPIRED');
297
+ return true;
298
+ }
299
+ log.warn({ attempt, ret: response.ret }, 'Session refresh attempt returned error');
300
+ }
301
+ catch (err) {
302
+ log.warn({ attempt, err: err instanceof Error ? err.message : String(err) }, 'Session refresh attempt threw');
303
+ }
304
+ if (attempt < MAX_ATTEMPTS) {
305
+ await new Promise((resolve) => setTimeout(resolve, attempt * 5_000)); // 5s, 10s
306
+ }
307
+ }
308
+ return false;
309
+ }
310
+ /**
311
+ * Periodic keep-alive: probe getconfig instead of getUpdates so we don't
312
+ * race the polling loop on the same long-poll cursor (W-1).
313
+ */
314
+ async sendHeartbeat() {
315
+ if (!this.client.hasCredentials())
316
+ return;
317
+ const ok = await this.client.pingConfig();
318
+ if (ok) {
319
+ log.debug('Heartbeat OK');
320
+ }
321
+ else {
322
+ log.warn('Heartbeat failed (getconfig returned non-OK)');
323
+ }
324
+ }
325
+ async handleIncomingMessage(msg) {
326
+ log.debug({ messageId: msg.message_id, type: msg.message_type }, 'handleIncomingMessage');
327
+ if (!this.messageHandler) {
328
+ log.warn('No message handler registered');
329
+ return;
330
+ }
331
+ if (msg.message_type === 2) {
332
+ log.debug('Skipping bot message');
333
+ return;
334
+ }
335
+ const msgId = String(msg.message_id);
336
+ if (msgId && this.processedMessages.has(msgId)) {
337
+ log.debug({ messageId: msgId }, 'Skipping duplicate message');
338
+ return;
339
+ }
340
+ if (msgId) {
341
+ this.processedMessages.set(msgId, Date.now());
342
+ this.cleanupProcessedMessages();
343
+ }
344
+ if (!msg.item_list?.length) {
345
+ log.debug('No item_list in message');
346
+ return;
347
+ }
348
+ // Store context token for replies
349
+ if (msg.from_user_id && msg.context_token) {
350
+ this.setContextToken(msg.from_user_id, msg.context_token);
351
+ }
352
+ const textParts = [];
353
+ const userId = msg.from_user_id || 'unknown';
354
+ for (const item of msg.item_list) {
355
+ const part = await this.extractItemContent(item, msgId, userId);
356
+ if (part)
357
+ textParts.push(part);
358
+ }
359
+ if (!textParts.length) {
360
+ log.debug('No extractable content in message items');
361
+ return;
362
+ }
363
+ const text = textParts.join('\n');
364
+ log.debug({
365
+ text: text.substring(0, 200),
366
+ fromUserId: msg.from_user_id,
367
+ contextToken: msg.context_token ? 'present' : 'missing',
368
+ itemTypes: msg.item_list.map(i => i.type),
369
+ }, 'Extracted message content');
370
+ const message = {
371
+ id: String(msg.message_id || Date.now()),
372
+ threadId: msg.group_id ? `room:${msg.group_id}` : `user:${msg.from_user_id}`,
373
+ userId,
374
+ text,
375
+ timestamp: new Date(msg.create_time_ms || Date.now()),
376
+ channelId: this.client.getCredentials()?.accountId || 'default',
377
+ };
378
+ const ctx = {
379
+ message,
380
+ platform: 'wechat',
381
+ channelId: this.client.getCredentials()?.accountId || 'default',
382
+ };
383
+ log.debug('Calling message handler');
384
+ await this.messageHandler(ctx);
385
+ log.debug('Message handler completed');
386
+ }
387
+ // ============================================
388
+ // Media Item Handlers
389
+ // ============================================
390
+ /**
391
+ * Extract displayable text content from a single message item. Text items
392
+ * return their text directly; media items are downloaded and surfaced as
393
+ * path markers that downstream agents can read.
394
+ */
395
+ async extractItemContent(item, msgId, userId) {
396
+ switch (item.type) {
397
+ case ITEM_TYPE.TEXT:
398
+ return item.text_item?.text || null;
399
+ case ITEM_TYPE.IMAGE:
400
+ return this.handleImageItem(item, msgId, userId);
401
+ case ITEM_TYPE.VOICE:
402
+ return this.handleVoiceItem(item, msgId, userId);
403
+ case ITEM_TYPE.FILE:
404
+ return this.handleFileItem(item, msgId, userId);
405
+ case ITEM_TYPE.VIDEO:
406
+ return this.handleVideoItem(item, msgId, userId);
407
+ default:
408
+ log.debug({ type: item.type }, 'Ignoring unsupported item type');
409
+ return null;
410
+ }
411
+ }
412
+ async handleImageItem(item, msgId, userId) {
413
+ if (!item.image_item)
414
+ return '[图片消息:无图片数据]';
415
+ const cdn = extractCdnMedia(item.image_item);
416
+ if (!cdn) {
417
+ log.warn({ event: 'wechat.media.image_no_cdn', msgId, image_item: item.image_item }, 'Image item has no downloadable CDN media');
418
+ return '[图片消息:无法下载]';
419
+ }
420
+ try {
421
+ const ext = pickExtensionForImage(item.image_item);
422
+ const { path } = await downloadWechatMedia(this.client, cdn, userId, `${msgId}.${ext}`);
423
+ return `[图片附件:${path}]`;
424
+ }
425
+ catch (err) {
426
+ const reason = err instanceof Error ? err.message : String(err);
427
+ log.warn({ event: 'wechat.media.image_failed', err: reason, msgId }, 'image download failed');
428
+ return `[图片附件下载失败:${reason}]`;
429
+ }
430
+ }
431
+ async handleVoiceItem(item, msgId, userId) {
432
+ if (!item.voice_item)
433
+ return '[语音消息:无语音数据]';
434
+ // WeChat may provide its own transcription via voice_item.text
435
+ if (item.voice_item.text) {
436
+ const dur = item.voice_item.playtime != null
437
+ ? `${Math.round(item.voice_item.playtime / 1000)}s, `
438
+ : '';
439
+ return `[语音转写(${dur}微信识别):${item.voice_item.text}]`;
440
+ }
441
+ // No built-in transcription — download and run STT ourselves
442
+ const cdn = extractCdnMedia(item.voice_item);
443
+ if (!cdn) {
444
+ log.warn({ event: 'wechat.media.voice_no_cdn', msgId, voice_item: item.voice_item }, 'Voice item has no downloadable CDN media and no text');
445
+ return '[语音消息:无法下载且无转写文本]';
446
+ }
447
+ let savedPath = null;
448
+ try {
449
+ const ext = pickExtensionForVoice(item.voice_item);
450
+ const { path } = await downloadWechatMedia(this.client, cdn, userId, `${msgId}.${ext}`);
451
+ savedPath = path;
452
+ }
453
+ catch (err) {
454
+ const reason = err instanceof Error ? err.message : String(err);
455
+ log.warn({ event: 'wechat.media.voice_download_failed', err: reason, msgId }, 'voice download failed');
456
+ return `[语音附件下载失败:${reason}]`;
457
+ }
458
+ if (detectProvider() === 'none') {
459
+ return `[语音附件未转写(未配置 OPENAI_API_KEY 或 IMHUB_WHISPERCPP_BIN):${savedPath}]`;
460
+ }
461
+ try {
462
+ const result = await transcribe(savedPath, { language: 'zh' });
463
+ const dur = item.voice_item.playtime != null
464
+ ? `${Math.round(item.voice_item.playtime / 1000)}s, `
465
+ : '';
466
+ return [
467
+ `[语音转写(${dur}provider=${result.provider}, ${result.elapsedMs}ms):`,
468
+ result.text || '(空)',
469
+ `源文件:${savedPath}]`,
470
+ ].join('\n');
471
+ }
472
+ catch (err) {
473
+ const reason = err instanceof TranscribeError
474
+ ? `${err.provider}: ${err.reason}`
475
+ : err instanceof Error ? err.message : String(err);
476
+ return `[语音转写失败(${reason})\n源文件:${savedPath}]`;
477
+ }
478
+ }
479
+ async handleFileItem(item, msgId, userId) {
480
+ if (!item.file_item)
481
+ return '[文件消息:无文件数据]';
482
+ const cdn = extractCdnMedia(item.file_item);
483
+ if (!cdn) {
484
+ log.warn({ event: 'wechat.media.file_no_cdn', msgId, file_item: item.file_item }, 'File item has no downloadable CDN media');
485
+ return `[文件消息:无法下载${item.file_item.file_name ? ` (${item.file_item.file_name})` : ''}]`;
486
+ }
487
+ const displayName = item.file_item.file_name || '未命名文件';
488
+ try {
489
+ const ext = pickExtensionForFile(item.file_item);
490
+ const filename = item.file_item.file_name || `${msgId}.${ext}`;
491
+ const safeName = filename.replace(/[/\\]/g, '_');
492
+ const { path, bytes } = await downloadWechatMedia(this.client, cdn, userId, safeName);
493
+ const sizeStr = bytes >= 1024 * 1024
494
+ ? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
495
+ : `${(bytes / 1024).toFixed(0)} KB`;
496
+ return `[文件附件:${path} (${displayName}, ${sizeStr})]`;
497
+ }
498
+ catch (err) {
499
+ const reason = err instanceof Error ? err.message : String(err);
500
+ log.warn({ event: 'wechat.media.file_failed', err: reason, msgId }, 'file download failed');
501
+ return `[文件附件下载失败:${reason} (${displayName})]`;
502
+ }
503
+ }
504
+ async handleVideoItem(item, msgId, userId) {
505
+ if (!item.video_item)
506
+ return '[视频消息:无视频数据]';
507
+ const cdn = extractCdnMedia(item.video_item);
508
+ if (!cdn) {
509
+ log.warn({ event: 'wechat.media.video_no_cdn', msgId, video_item: item.video_item }, 'Video item has no downloadable CDN media');
510
+ return '[视频消息:无法下载]';
511
+ }
512
+ try {
513
+ const ext = pickExtensionForVideo(item.video_item);
514
+ const { path, bytes } = await downloadWechatMedia(this.client, cdn, userId, `${msgId}.${ext}`);
515
+ const dur = item.video_item.play_length != null
516
+ ? `${item.video_item.play_length}s, `
517
+ : '';
518
+ const sizeStr = bytes >= 1024 * 1024
519
+ ? `${(bytes / (1024 * 1024)).toFixed(1)} MB`
520
+ : `${(bytes / 1024).toFixed(0)} KB`;
521
+ return `[视频附件:${path} (${dur}${sizeStr})]`;
522
+ }
523
+ catch (err) {
524
+ const reason = err instanceof Error ? err.message : String(err);
525
+ log.warn({ event: 'wechat.media.video_failed', err: reason, msgId }, 'video download failed');
526
+ return `[视频附件下载失败:${reason}]`;
527
+ }
528
+ }
529
+ // ============================================
530
+ // Context Token Management
531
+ // ============================================
532
+ getContextToken(userId) {
533
+ const now = Date.now();
534
+ const cached = this.contextTokens.get(userId);
535
+ if (cached) {
536
+ if (now - cached.timestamp > CONTEXT_TOKEN_TTL) {
537
+ this.contextTokens.delete(userId);
538
+ // Fall through to DB — last-resort same-user token might be even
539
+ // older, but the DB-backed cleanup will reject expired rows too.
540
+ }
541
+ else {
542
+ return cached.contextToken;
543
+ }
544
+ }
545
+ // Map miss (or just-expired) → fall back to persistent store. This makes
546
+ // first-send-after-restart succeed when the user messaged before the
547
+ // restart and the token is still within its 30-min window.
548
+ const stored = loadContextToken(userId, CONTEXT_TOKEN_TTL, now);
549
+ if (!stored)
550
+ return null;
551
+ // Re-populate the Map so subsequent sends in this run skip the DB.
552
+ this.contextTokens.set(userId, {
553
+ userId: stored.userId,
554
+ contextToken: stored.token,
555
+ timestamp: stored.fetchedAt,
556
+ });
557
+ return stored.token;
558
+ }
559
+ setContextToken(userId, token) {
560
+ const now = Date.now();
561
+ this.contextTokens.set(userId, {
562
+ userId,
563
+ contextToken: token,
564
+ timestamp: now,
565
+ });
566
+ // Write-through. Best-effort: store calls fail-soft on SQLite issues.
567
+ persistContextToken(userId, token, now);
568
+ }
569
+ cleanupProcessedMessages() {
570
+ const now = Date.now();
571
+ for (const [msgId, timestamp] of this.processedMessages) {
572
+ if (now - timestamp > PROCESSED_MESSAGES_TTL) {
573
+ this.processedMessages.delete(msgId);
574
+ }
575
+ }
576
+ }
577
+ /**
578
+ * L14: drop expired context-token entries. The original implementation
579
+ * only removed an entry on read (`getContextToken` checked TTL), so a
580
+ * userId that messaged once and never came back left its row in the
581
+ * Map indefinitely. Called from the polling loop at the same cadence
582
+ * as cleanupProcessedMessages so memory stays bounded under big group
583
+ * sizes / churning user populations.
584
+ */
585
+ cleanupExpiredContextTokens() {
586
+ const now = Date.now();
587
+ for (const [userId, cached] of this.contextTokens) {
588
+ if (now - cached.timestamp > CONTEXT_TOKEN_TTL) {
589
+ this.contextTokens.delete(userId);
590
+ }
591
+ }
592
+ // Mirror the cleanup into the persistent store so its row count stays
593
+ // bounded too. Cheap (indexed by fetched_at).
594
+ cleanupExpiredTokensInStore(CONTEXT_TOKEN_TTL, now);
595
+ }
596
+ // ============================================
597
+ // Credentials Persistence
598
+ // ============================================
599
+ async loadCredentials() {
600
+ try {
601
+ const data = await readFile(CREDENTIALS_FILE, 'utf-8');
602
+ // Defense in depth: nag in logs if the file isn't 0o600. We can't
603
+ // safely chmod it on the user's behalf at load time (concurrent ops,
604
+ // odd ownership) but the warning prompts an operator to fix legacy
605
+ // credentials files written before H9.
606
+ try {
607
+ const st = await stat(CREDENTIALS_FILE);
608
+ if (process.platform !== 'win32' && (st.mode & 0o077) !== 0) {
609
+ log.warn({
610
+ file: CREDENTIALS_FILE,
611
+ mode: (st.mode & 0o777).toString(8),
612
+ }, 'Credentials file is group/world readable — chmod 600 recommended');
613
+ }
614
+ }
615
+ catch { /* stat failure is non-fatal */ }
616
+ return JSON.parse(data);
617
+ }
618
+ catch {
619
+ return null;
620
+ }
621
+ }
622
+ async saveCredentials(credentials) {
623
+ const dir = join(CREDENTIALS_FILE, '..');
624
+ // Restrict the parent dir to the current user. recursive:true is a no-op
625
+ // when the dir already exists with looser perms — accept that compromise
626
+ // (we don't want to chmod existing dirs out from under the operator) and
627
+ // rely on the file-level 0o600 below as the primary line of defense.
628
+ await mkdir(dir, { recursive: true, mode: 0o700 });
629
+ await writeFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { mode: 0o600 });
630
+ log.info({ file: CREDENTIALS_FILE }, 'Credentials saved');
631
+ }
632
+ // ============================================
633
+ // Utilities
634
+ // ============================================
635
+ splitMessage(text, maxLength = 2000) {
636
+ if (text.length <= maxLength) {
637
+ return [text];
638
+ }
639
+ const chunks = [];
640
+ let remaining = text;
641
+ while (remaining.length > maxLength) {
642
+ // Try to split at newline
643
+ let splitPoint = remaining.lastIndexOf('\n', maxLength);
644
+ if (splitPoint < maxLength / 2) {
645
+ splitPoint = maxLength;
646
+ }
647
+ chunks.push(remaining.slice(0, splitPoint));
648
+ remaining = remaining.slice(splitPoint).trim();
649
+ }
650
+ if (remaining) {
651
+ chunks.push(remaining);
652
+ }
653
+ // Add continuation markers
654
+ if (chunks.length > 1) {
655
+ for (let i = 0; i < chunks.length - 1; i++) {
656
+ chunks[i] += '\n\n[continued...]';
657
+ }
658
+ }
659
+ return chunks;
660
+ }
661
+ }
662
+ // Singleton instance
663
+ export const ilinkWeChatAdapter = new ILinkWeChatAdapter();
664
+ //# sourceMappingURL=ilink-adapter.js.map