cli-claw-kit 0.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 (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/config/default-groups.json +1 -0
  4. package/config/global-agents-md.template.md +37 -0
  5. package/config/mount-allowlist.json +11 -0
  6. package/container/Dockerfile +160 -0
  7. package/container/agent-runner/dist/.tsbuildinfo +1 -0
  8. package/container/agent-runner/dist/agent-definitions.js +22 -0
  9. package/container/agent-runner/dist/channel-prefixes.js +16 -0
  10. package/container/agent-runner/dist/codex-config.js +29 -0
  11. package/container/agent-runner/dist/image-detector.js +96 -0
  12. package/container/agent-runner/dist/index.js +2587 -0
  13. package/container/agent-runner/dist/mcp-tools.js +1076 -0
  14. package/container/agent-runner/dist/stream-event.types.js +5 -0
  15. package/container/agent-runner/dist/stream-processor.js +867 -0
  16. package/container/agent-runner/dist/types.js +6 -0
  17. package/container/agent-runner/dist/utils.js +115 -0
  18. package/container/agent-runner/package.json +36 -0
  19. package/container/agent-runner/prompts/security-rules.md +31 -0
  20. package/container/agent-runner/src/agent-definitions.ts +27 -0
  21. package/container/agent-runner/src/channel-prefixes.ts +16 -0
  22. package/container/agent-runner/src/codex-config.ts +40 -0
  23. package/container/agent-runner/src/image-detector.ts +116 -0
  24. package/container/agent-runner/src/index.ts +3107 -0
  25. package/container/agent-runner/src/mcp-tools.ts +1295 -0
  26. package/container/agent-runner/src/stream-event.types.ts +10 -0
  27. package/container/agent-runner/src/stream-processor.ts +932 -0
  28. package/container/agent-runner/src/types.ts +75 -0
  29. package/container/agent-runner/src/utils.ts +114 -0
  30. package/container/agent-runner/tsconfig.json +17 -0
  31. package/container/build.sh +28 -0
  32. package/container/entrypoint.sh +64 -0
  33. package/container/skills/agent-browser/SKILL.md +159 -0
  34. package/container/skills/install-skill/SKILL.md +64 -0
  35. package/container/skills/post-test-cleanup/SKILL.md +121 -0
  36. package/dist/.tsbuildinfo +1 -0
  37. package/dist/agent-output-parser.js +459 -0
  38. package/dist/app-root.js +52 -0
  39. package/dist/assistant-meta-footer.js +1 -0
  40. package/dist/auth.js +91 -0
  41. package/dist/billing.js +694 -0
  42. package/dist/channel-prefixes.js +16 -0
  43. package/dist/cli.js +86 -0
  44. package/dist/commands.js +79 -0
  45. package/dist/config.js +120 -0
  46. package/dist/container-runner.js +981 -0
  47. package/dist/daily-summary.js +210 -0
  48. package/dist/db.js +3683 -0
  49. package/dist/dingtalk.js +1347 -0
  50. package/dist/feishu-markdown-style.js +97 -0
  51. package/dist/feishu-streaming-card.js +1875 -0
  52. package/dist/feishu.js +1628 -0
  53. package/dist/file-manager.js +270 -0
  54. package/dist/group-queue.js +1070 -0
  55. package/dist/group-runtime.js +35 -0
  56. package/dist/host-workspace-cwd.js +85 -0
  57. package/dist/im-channel.js +384 -0
  58. package/dist/im-command-utils.js +142 -0
  59. package/dist/im-downloader.js +45 -0
  60. package/dist/im-manager.js +527 -0
  61. package/dist/im-utils.js +53 -0
  62. package/dist/image-detector.js +96 -0
  63. package/dist/index.js +5828 -0
  64. package/dist/logger.js +22 -0
  65. package/dist/mcp-utils.js +66 -0
  66. package/dist/message-attachments.js +69 -0
  67. package/dist/message-notifier.js +36 -0
  68. package/dist/middleware/auth.js +85 -0
  69. package/dist/mount-security.js +315 -0
  70. package/dist/permissions.js +67 -0
  71. package/dist/project-memory.js +6 -0
  72. package/dist/provider-pool.js +189 -0
  73. package/dist/qq.js +826 -0
  74. package/dist/reset-admin.js +42 -0
  75. package/dist/routes/admin.js +543 -0
  76. package/dist/routes/agent-definitions.js +241 -0
  77. package/dist/routes/agents.js +533 -0
  78. package/dist/routes/auth.js +675 -0
  79. package/dist/routes/billing.js +490 -0
  80. package/dist/routes/browse.js +210 -0
  81. package/dist/routes/bug-report.js +387 -0
  82. package/dist/routes/config.js +1868 -0
  83. package/dist/routes/files.js +671 -0
  84. package/dist/routes/groups.js +1367 -0
  85. package/dist/routes/mcp-servers.js +320 -0
  86. package/dist/routes/memory.js +523 -0
  87. package/dist/routes/monitor.js +307 -0
  88. package/dist/routes/skills.js +777 -0
  89. package/dist/routes/tasks.js +509 -0
  90. package/dist/routes/usage.js +64 -0
  91. package/dist/routes/workspace-config.js +458 -0
  92. package/dist/runtime-build.js +112 -0
  93. package/dist/runtime-command-handler.js +189 -0
  94. package/dist/runtime-command-registry.js +1 -0
  95. package/dist/runtime-config.js +1777 -0
  96. package/dist/runtime-identity.js +52 -0
  97. package/dist/schemas.js +590 -0
  98. package/dist/script-runner.js +64 -0
  99. package/dist/sdk-query.js +82 -0
  100. package/dist/skill-utils.js +145 -0
  101. package/dist/sqlite-compat.js +19 -0
  102. package/dist/stream-event.types.js +5 -0
  103. package/dist/streaming-runtime-meta.js +29 -0
  104. package/dist/task-scheduler.js +695 -0
  105. package/dist/task-utils.js +13 -0
  106. package/dist/telegram-pairing.js +59 -0
  107. package/dist/telegram.js +897 -0
  108. package/dist/terminal-manager.js +307 -0
  109. package/dist/tool-step-display.js +1 -0
  110. package/dist/types.js +1 -0
  111. package/dist/utils.js +85 -0
  112. package/dist/web-context.js +161 -0
  113. package/dist/web.js +1377 -0
  114. package/dist/wechat-crypto.js +182 -0
  115. package/dist/wechat.js +589 -0
  116. package/dist/workspace-runtime-reset.js +35 -0
  117. package/package.json +107 -0
  118. package/shared/assistant-meta-footer.ts +127 -0
  119. package/shared/channel-prefixes.ts +16 -0
  120. package/shared/dist/assistant-meta-footer.d.ts +29 -0
  121. package/shared/dist/assistant-meta-footer.js +85 -0
  122. package/shared/dist/channel-prefixes.d.ts +4 -0
  123. package/shared/dist/channel-prefixes.js +16 -0
  124. package/shared/dist/image-detector.d.ts +20 -0
  125. package/shared/dist/image-detector.js +96 -0
  126. package/shared/dist/runtime-command-registry.d.ts +38 -0
  127. package/shared/dist/runtime-command-registry.js +185 -0
  128. package/shared/dist/stream-event.d.ts +65 -0
  129. package/shared/dist/stream-event.js +8 -0
  130. package/shared/dist/tool-step-display.d.ts +4 -0
  131. package/shared/dist/tool-step-display.js +11 -0
  132. package/shared/image-detector.ts +116 -0
  133. package/shared/runtime-command-registry.ts +252 -0
  134. package/shared/stream-event.ts +67 -0
  135. package/shared/tool-step-display.ts +21 -0
  136. package/shared/tsconfig.json +24 -0
  137. package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
  138. package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
  139. package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
  140. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  141. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  142. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  143. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  144. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  145. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  146. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  147. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  148. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  149. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  150. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  151. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  152. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  153. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  154. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  155. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  156. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  157. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  158. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  159. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  160. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  161. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  162. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  163. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  164. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  165. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  166. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  167. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  168. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  169. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  170. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  171. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  172. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  173. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  174. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  175. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  176. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  177. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  178. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  179. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  180. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  181. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  182. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  183. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  184. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  185. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  186. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  187. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  188. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  189. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  190. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  191. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  192. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  193. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  194. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  195. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  196. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  197. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  198. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  199. package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
  200. package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
  201. package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
  202. package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
  203. package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
  204. package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
  205. package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
  206. package/web/dist/assets/band-CquvqAHh.js +1 -0
  207. package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
  208. package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
  209. package/web/dist/assets/channel-BOVj73LR.js +1 -0
  210. package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
  211. package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
  212. package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
  213. package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
  214. package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
  215. package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
  216. package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
  217. package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
  218. package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
  219. package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
  220. package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
  221. package/web/dist/assets/clone-BmaCesfa.js +1 -0
  222. package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
  223. package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  224. package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
  225. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  226. package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
  227. package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
  228. package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
  229. package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
  230. package/web/dist/assets/error-CGD5mp5f.js +1 -0
  231. package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
  232. package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
  233. package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
  234. package/web/dist/assets/graph-CeAEckur.js +1 -0
  235. package/web/dist/assets/index-CPnL1_qC.js +768 -0
  236. package/web/dist/assets/index-DVevCbcO.css +10 -0
  237. package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
  238. package/web/dist/assets/init-Dmth1JHB.js +1 -0
  239. package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
  240. package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
  241. package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
  242. package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
  243. package/web/dist/assets/linear-DiaJloY5.js +1 -0
  244. package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
  245. package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
  246. package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
  247. package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
  248. package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
  249. package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
  250. package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
  251. package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
  252. package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
  253. package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
  254. package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
  255. package/web/dist/assets/square-0CqMX1Q3.js +11 -0
  256. package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
  257. package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
  258. package/web/dist/assets/step-D51IIHGA.js +1 -0
  259. package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
  260. package/web/dist/assets/time-O8zIGux3.js +1 -0
  261. package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
  262. package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
  263. package/web/dist/assets/utils-KGAn0XTg.js +11 -0
  264. package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
  265. package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
  266. package/web/dist/assets/zap-_hKJYy7J.js +6 -0
  267. package/web/dist/favicon.svg +332 -0
  268. package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
  269. package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
  270. package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
  271. package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
  272. package/web/dist/fonts/DMSans-latin.woff2 +0 -0
  273. package/web/dist/icons/README.md +20 -0
  274. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  275. package/web/dist/icons/icon-128.png +0 -0
  276. package/web/dist/icons/icon-144.png +0 -0
  277. package/web/dist/icons/icon-152.png +0 -0
  278. package/web/dist/icons/icon-192.png +0 -0
  279. package/web/dist/icons/icon-192.svg +332 -0
  280. package/web/dist/icons/icon-384.png +0 -0
  281. package/web/dist/icons/icon-48.png +0 -0
  282. package/web/dist/icons/icon-512-maskable.png +0 -0
  283. package/web/dist/icons/icon-512.png +0 -0
  284. package/web/dist/icons/icon-512.svg +332 -0
  285. package/web/dist/icons/icon-72.png +0 -0
  286. package/web/dist/icons/icon-96.png +0 -0
  287. package/web/dist/icons/loading-logo.svg +332 -0
  288. package/web/dist/icons/logo-1024.png +0 -0
  289. package/web/dist/icons/logo-icon.svg +332 -0
  290. package/web/dist/icons/logo-text.svg +332 -0
  291. package/web/dist/index.html +30 -0
  292. package/web/dist/manifest.webmanifest +1 -0
  293. package/web/dist/registerSW.js +1 -0
  294. package/web/dist/sw.js +1 -0
  295. package/web/dist/workbox-08d6266a.js +1 -0
@@ -0,0 +1,527 @@
1
+ /**
2
+ * IM Connection Pool Manager
3
+ *
4
+ * Manages per-user IM connections using the unified IMChannel interface.
5
+ * Each user can have independent IM connections that route messages
6
+ * to their home container.
7
+ */
8
+ import { getChannelType, extractChatId, createFeishuChannel, createTelegramChannel, createQQChannel, createWeChatChannel, createDingTalkChannel, } from './im-channel.js';
9
+ import { getRegisteredGroup, getJidsByFolder } from './db.js';
10
+ import { logger } from './logger.js';
11
+ class IMConnectionManager {
12
+ connections = new Map();
13
+ adminUserIds = new Set();
14
+ /** Register a user ID as admin (for fallback routing) */
15
+ registerAdminUser(userId) {
16
+ this.adminUserIds.add(userId);
17
+ }
18
+ getOrCreate(userId) {
19
+ let conn = this.connections.get(userId);
20
+ if (!conn) {
21
+ conn = { userId, channels: new Map() };
22
+ this.connections.set(userId, conn);
23
+ }
24
+ return conn;
25
+ }
26
+ // ─── Generic Channel Methods ────────────────────────────────
27
+ /**
28
+ * Connect any IMChannel for a user.
29
+ */
30
+ async connectChannel(userId, channelType, channel, opts) {
31
+ // Disconnect existing channel of same type
32
+ await this.disconnectChannel(userId, channelType);
33
+ const conn = this.getOrCreate(userId);
34
+ const connected = await channel.connect(opts);
35
+ if (connected) {
36
+ conn.channels.set(channelType, channel);
37
+ logger.info({ userId, channelType }, 'IM channel connected');
38
+ }
39
+ return connected;
40
+ }
41
+ /**
42
+ * Disconnect a specific channel type for a user.
43
+ */
44
+ async disconnectChannel(userId, channelType) {
45
+ const conn = this.connections.get(userId);
46
+ const channel = conn?.channels.get(channelType);
47
+ if (channel) {
48
+ await channel.disconnect();
49
+ conn.channels.delete(channelType);
50
+ logger.info({ userId, channelType }, 'IM channel disconnected');
51
+ }
52
+ }
53
+ /**
54
+ * Send a message to an IM chat, auto-routing via JID prefix.
55
+ * Resolves the user by looking up chatJid -> registered_groups.created_by.
56
+ * Falls back to iterating sibling groups if no created_by is set.
57
+ */
58
+ async sendMessage(jid, text, localImagePaths) {
59
+ const channelType = getChannelType(jid);
60
+ if (!channelType) {
61
+ logger.debug({ jid }, 'Unknown channel type for JID, skip sending');
62
+ return;
63
+ }
64
+ const chatId = extractChatId(jid);
65
+ const channel = this.findChannelForJid(jid, channelType);
66
+ if (!channel) {
67
+ throw new Error(`No IM channel available for ${jid} (${channelType})`);
68
+ }
69
+ await channel.sendMessage(chatId, text, localImagePaths);
70
+ }
71
+ /**
72
+ * Send an image to an IM chat, auto-routing via JID prefix.
73
+ */
74
+ async sendImage(jid, imageBuffer, mimeType, caption, fileName) {
75
+ const channelType = getChannelType(jid);
76
+ if (!channelType) {
77
+ logger.debug({ jid }, 'Unknown channel type for JID, skip sending image');
78
+ return;
79
+ }
80
+ const chatId = extractChatId(jid);
81
+ const channel = this.findChannelForJid(jid, channelType);
82
+ if (channel?.sendImage) {
83
+ await channel.sendImage(chatId, imageBuffer, mimeType, caption, fileName);
84
+ return;
85
+ }
86
+ // Fallback: if channel doesn't support sendImage, send caption as text
87
+ if (caption && channel) {
88
+ await channel.sendMessage(chatId, `📷 ${caption}`);
89
+ return;
90
+ }
91
+ logger.warn({ jid, channelType }, 'No IM channel available to send image');
92
+ }
93
+ /**
94
+ * Send a file to an IM chat, auto-routing via JID prefix.
95
+ * @throws Error if the channel doesn't support file sending
96
+ */
97
+ async sendFile(jid, filePath, fileName) {
98
+ const channelType = getChannelType(jid);
99
+ if (!channelType) {
100
+ throw new Error(`无法识别 JID 的通道类型: ${jid}`);
101
+ }
102
+ const chatId = extractChatId(jid);
103
+ const channel = this.findChannelForJid(jid, channelType);
104
+ if (channel?.sendFile) {
105
+ await channel.sendFile(chatId, filePath, fileName);
106
+ }
107
+ else {
108
+ throw new Error(`通道 ${channelType} 不支持发送文件`);
109
+ }
110
+ }
111
+ /**
112
+ * Set typing indicator on an IM chat, auto-routing via JID prefix.
113
+ */
114
+ async setTyping(jid, isTyping) {
115
+ const channelType = getChannelType(jid);
116
+ if (!channelType)
117
+ return;
118
+ const chatId = extractChatId(jid);
119
+ const channel = this.findChannelForJid(jid, channelType);
120
+ if (channel) {
121
+ await channel.setTyping(chatId, isTyping);
122
+ }
123
+ // No fallback for typing — silently ignore if owner's connection is unavailable
124
+ }
125
+ /**
126
+ * Clear the ack reaction for a chat (e.g. when streaming card handled the reply).
127
+ */
128
+ clearAckReaction(jid) {
129
+ const channelType = getChannelType(jid);
130
+ if (!channelType)
131
+ return;
132
+ const chatId = extractChatId(jid);
133
+ const channel = this.findChannelForJid(jid, channelType);
134
+ if (channel?.clearAckReaction) {
135
+ channel.clearAckReaction(chatId);
136
+ }
137
+ }
138
+ /**
139
+ * Create a streaming card session for an IM chat (Feishu only).
140
+ * Returns undefined for non-Feishu channels or if not supported.
141
+ */
142
+ createStreamingSession(jid, onCardCreated) {
143
+ const channelType = getChannelType(jid);
144
+ if (channelType !== 'feishu')
145
+ return undefined;
146
+ const chatId = extractChatId(jid);
147
+ const channel = this.findChannelForJid(jid, channelType);
148
+ if (channel?.createStreamingSession) {
149
+ return channel.createStreamingSession(chatId, onCardCreated);
150
+ }
151
+ return undefined;
152
+ }
153
+ /**
154
+ * Find the appropriate IMChannel for a given JID, using group ownership lookup
155
+ * and sibling fallback.
156
+ */
157
+ findChannelForJid(jid, channelType) {
158
+ // Direct lookup via group ownership
159
+ const group = getRegisteredGroup(jid);
160
+ if (group?.created_by) {
161
+ const conn = this.connections.get(group.created_by);
162
+ const ch = conn?.channels.get(channelType);
163
+ if (ch?.isConnected())
164
+ return ch;
165
+ }
166
+ // Fallback: find owner via sibling groups sharing the same folder
167
+ if (group) {
168
+ const siblingJids = getJidsByFolder(group.folder);
169
+ for (const sibJid of siblingJids) {
170
+ if (sibJid === jid)
171
+ continue;
172
+ const sibling = getRegisteredGroup(sibJid);
173
+ if (sibling?.created_by) {
174
+ const conn = this.connections.get(sibling.created_by);
175
+ const ch = conn?.channels.get(channelType);
176
+ if (ch?.isConnected()) {
177
+ logger.warn({ jid, fallbackUserId: sibling.created_by, folder: group.folder }, 'IM message routed via sibling group owner connection');
178
+ return ch;
179
+ }
180
+ }
181
+ }
182
+ }
183
+ return undefined;
184
+ }
185
+ /**
186
+ * Get all connected channel types for a user.
187
+ * Used by scheduled task IM broadcast to discover available channels.
188
+ */
189
+ getConnectedChannelTypes(userId) {
190
+ const conn = this.connections.get(userId);
191
+ if (!conn)
192
+ return [];
193
+ const types = [];
194
+ for (const [type, ch] of conn.channels.entries()) {
195
+ if (ch.isConnected())
196
+ types.push(type);
197
+ }
198
+ return types;
199
+ }
200
+ /**
201
+ * Check if a specific JID has a connected channel available.
202
+ * Uses the same routing logic as sendMessage (group ownership + sibling fallback).
203
+ */
204
+ isChannelAvailableForJid(jid) {
205
+ const channelType = getChannelType(jid);
206
+ if (!channelType)
207
+ return false;
208
+ return !!this.findChannelForJid(jid, channelType);
209
+ }
210
+ // ─── Convenience Methods (API-compatible wrappers) ──────────
211
+ /**
212
+ * Connect a Feishu instance for a specific user.
213
+ */
214
+ async connectUserFeishu(userId, config, onNewChat, options) {
215
+ if (!config.appId || !config.appSecret) {
216
+ logger.info({ userId }, 'Feishu config empty, skipping connection');
217
+ return false;
218
+ }
219
+ const channel = createFeishuChannel({
220
+ appId: config.appId,
221
+ appSecret: config.appSecret,
222
+ });
223
+ return this.connectChannel(userId, 'feishu', channel, {
224
+ onReady: () => {
225
+ logger.info({ userId }, 'User Feishu WebSocket connected');
226
+ },
227
+ onNewChat,
228
+ ignoreMessagesBefore: options?.ignoreMessagesBefore,
229
+ onCommand: options?.onCommand,
230
+ resolveGroupFolder: options?.resolveGroupFolder,
231
+ resolveEffectiveChatJid: options?.resolveEffectiveChatJid,
232
+ onAgentMessage: options?.onAgentMessage,
233
+ onBotAddedToGroup: options?.onBotAddedToGroup,
234
+ onBotRemovedFromGroup: options?.onBotRemovedFromGroup,
235
+ shouldProcessGroupMessage: options?.shouldProcessGroupMessage,
236
+ onCardInterrupt: options?.onCardInterrupt,
237
+ onCardRuntimeUpdate: options?.onCardRuntimeUpdate,
238
+ });
239
+ }
240
+ /**
241
+ * Connect a Telegram instance for a specific user.
242
+ */
243
+ async connectUserTelegram(userId, config, onNewChat, isChatAuthorized, onPairAttempt, options) {
244
+ if (!config.botToken) {
245
+ logger.info({ userId }, 'Telegram config empty, skipping connection');
246
+ return false;
247
+ }
248
+ const channel = createTelegramChannel({
249
+ botToken: config.botToken,
250
+ proxyUrl: config.proxyUrl,
251
+ });
252
+ return this.connectChannel(userId, 'telegram', channel, {
253
+ onReady: () => {
254
+ logger.info({ userId }, 'User Telegram bot connected');
255
+ },
256
+ onNewChat,
257
+ isChatAuthorized,
258
+ onPairAttempt,
259
+ onCommand: options?.onCommand,
260
+ ignoreMessagesBefore: options?.ignoreMessagesBefore,
261
+ resolveGroupFolder: options?.resolveGroupFolder,
262
+ resolveEffectiveChatJid: options?.resolveEffectiveChatJid,
263
+ onAgentMessage: options?.onAgentMessage,
264
+ onBotAddedToGroup: options?.onBotAddedToGroup,
265
+ onBotRemovedFromGroup: options?.onBotRemovedFromGroup,
266
+ });
267
+ }
268
+ /**
269
+ * Connect a QQ instance for a specific user.
270
+ */
271
+ async connectUserQQ(userId, config, onNewChat, isChatAuthorized, onPairAttempt, options) {
272
+ if (!config.appId || !config.appSecret) {
273
+ logger.info({ userId }, 'QQ config empty, skipping connection');
274
+ return false;
275
+ }
276
+ const channel = createQQChannel({
277
+ appId: config.appId,
278
+ appSecret: config.appSecret,
279
+ });
280
+ return this.connectChannel(userId, 'qq', channel, {
281
+ onReady: () => {
282
+ logger.info({ userId }, 'User QQ bot connected');
283
+ },
284
+ onNewChat,
285
+ isChatAuthorized,
286
+ onPairAttempt,
287
+ onCommand: options?.onCommand,
288
+ resolveGroupFolder: options?.resolveGroupFolder,
289
+ resolveEffectiveChatJid: options?.resolveEffectiveChatJid,
290
+ onAgentMessage: options?.onAgentMessage,
291
+ });
292
+ }
293
+ async disconnectUserFeishu(userId) {
294
+ await this.disconnectChannel(userId, 'feishu');
295
+ }
296
+ async disconnectUserTelegram(userId) {
297
+ await this.disconnectChannel(userId, 'telegram');
298
+ }
299
+ async disconnectUserQQ(userId) {
300
+ await this.disconnectChannel(userId, 'qq');
301
+ }
302
+ /**
303
+ * Connect a WeChat iLink instance for a specific user.
304
+ */
305
+ async connectUserWeChat(userId, config, onNewChat, options) {
306
+ if (!config.botToken || !config.ilinkBotId) {
307
+ logger.info({ userId }, 'WeChat config empty, skipping connection');
308
+ return false;
309
+ }
310
+ const channel = createWeChatChannel({
311
+ botToken: config.botToken,
312
+ ilinkBotId: config.ilinkBotId,
313
+ baseUrl: config.baseUrl,
314
+ cdnBaseUrl: config.cdnBaseUrl,
315
+ getUpdatesBuf: config.getUpdatesBuf,
316
+ });
317
+ return this.connectChannel(userId, 'wechat', channel, {
318
+ onReady: () => {
319
+ logger.info({ userId }, 'User WeChat bot connected');
320
+ },
321
+ onNewChat,
322
+ ignoreMessagesBefore: options?.ignoreMessagesBefore,
323
+ onCommand: options?.onCommand,
324
+ resolveGroupFolder: options?.resolveGroupFolder,
325
+ resolveEffectiveChatJid: options?.resolveEffectiveChatJid,
326
+ onAgentMessage: options?.onAgentMessage,
327
+ });
328
+ }
329
+ async disconnectUserWeChat(userId) {
330
+ await this.disconnectChannel(userId, 'wechat');
331
+ }
332
+ /**
333
+ * Connect a DingTalk Stream instance for a specific user.
334
+ */
335
+ async connectUserDingTalk(userId, config, onNewChat, options) {
336
+ if (!config.clientId || !config.clientSecret) {
337
+ logger.info({ userId }, 'DingTalk config empty, skipping connection');
338
+ return false;
339
+ }
340
+ const channel = createDingTalkChannel({
341
+ clientId: config.clientId,
342
+ clientSecret: config.clientSecret,
343
+ });
344
+ return this.connectChannel(userId, 'dingtalk', channel, {
345
+ onReady: () => {
346
+ logger.info({ userId }, 'User DingTalk bot connected');
347
+ },
348
+ onNewChat,
349
+ ignoreMessagesBefore: options?.ignoreMessagesBefore,
350
+ onCommand: options?.onCommand,
351
+ resolveGroupFolder: options?.resolveGroupFolder,
352
+ resolveEffectiveChatJid: options?.resolveEffectiveChatJid,
353
+ onAgentMessage: options?.onAgentMessage,
354
+ onBotAddedToGroup: options?.onBotAddedToGroup,
355
+ onBotRemovedFromGroup: options?.onBotRemovedFromGroup,
356
+ shouldProcessGroupMessage: options?.shouldProcessGroupMessage,
357
+ });
358
+ }
359
+ async disconnectUserDingTalk(userId) {
360
+ await this.disconnectChannel(userId, 'dingtalk');
361
+ }
362
+ /**
363
+ * Send a message to a Feishu chat.
364
+ * @deprecated Use sendMessage(jid, text) which auto-routes.
365
+ */
366
+ async sendFeishuMessage(chatJid, text, localImagePaths) {
367
+ const chatId = extractChatId(chatJid);
368
+ const channel = this.findChannelForJid(chatJid, 'feishu');
369
+ if (channel) {
370
+ await channel.sendMessage(chatId, text, localImagePaths);
371
+ return;
372
+ }
373
+ logger.warn({ chatJid }, 'No Feishu connection available to send message');
374
+ }
375
+ /**
376
+ * Send a message to a Telegram chat.
377
+ * @deprecated Use sendMessage(jid, text) which auto-routes.
378
+ */
379
+ async sendTelegramMessage(chatJid, text, localImagePaths) {
380
+ const chatId = extractChatId(chatJid);
381
+ const channel = this.findChannelForJid(chatJid, 'telegram');
382
+ if (channel) {
383
+ await channel.sendMessage(chatId, text, localImagePaths);
384
+ return;
385
+ }
386
+ logger.warn({ chatJid }, 'No Telegram connection available to send message');
387
+ }
388
+ /**
389
+ * Set typing reaction on a Feishu chat.
390
+ * @deprecated Use setTyping(jid, isTyping) which auto-routes.
391
+ */
392
+ async setFeishuTyping(chatJid, isTyping) {
393
+ const chatId = extractChatId(chatJid);
394
+ const channel = this.findChannelForJid(chatJid, 'feishu');
395
+ if (channel) {
396
+ await channel.setTyping(chatId, isTyping);
397
+ }
398
+ }
399
+ /**
400
+ * Set Telegram typing chat action for a chat.
401
+ * @deprecated Use setTyping(jid, isTyping) which auto-routes.
402
+ */
403
+ async setTelegramTyping(chatJid, isTyping) {
404
+ const chatId = extractChatId(chatJid);
405
+ const channel = this.findChannelForJid(chatJid, 'telegram');
406
+ if (channel) {
407
+ await channel.setTyping(chatId, isTyping);
408
+ }
409
+ }
410
+ /**
411
+ * Sync Feishu groups via a specific user's connection.
412
+ */
413
+ async syncFeishuGroups(userId) {
414
+ const conn = this.connections.get(userId);
415
+ const channel = conn?.channels.get('feishu');
416
+ if (channel?.isConnected() && channel.syncGroups) {
417
+ await channel.syncGroups();
418
+ }
419
+ }
420
+ isFeishuConnected(userId) {
421
+ const conn = this.connections.get(userId);
422
+ return conn?.channels.get('feishu')?.isConnected() ?? false;
423
+ }
424
+ isTelegramConnected(userId) {
425
+ const conn = this.connections.get(userId);
426
+ return conn?.channels.get('telegram')?.isConnected() ?? false;
427
+ }
428
+ isQQConnected(userId) {
429
+ const conn = this.connections.get(userId);
430
+ return conn?.channels.get('qq')?.isConnected() ?? false;
431
+ }
432
+ /** Check if any user has an active Feishu connection */
433
+ isAnyFeishuConnected() {
434
+ for (const conn of this.connections.values()) {
435
+ if (conn.channels.get('feishu')?.isConnected())
436
+ return true;
437
+ }
438
+ return false;
439
+ }
440
+ /** Check if any user has an active Telegram connection */
441
+ isAnyTelegramConnected() {
442
+ for (const conn of this.connections.values()) {
443
+ if (conn.channels.get('telegram')?.isConnected())
444
+ return true;
445
+ }
446
+ return false;
447
+ }
448
+ isWeChatConnected(userId) {
449
+ const conn = this.connections.get(userId);
450
+ return conn?.channels.get('wechat')?.isConnected() ?? false;
451
+ }
452
+ isDingTalkConnected(userId) {
453
+ const conn = this.connections.get(userId);
454
+ return conn?.channels.get('dingtalk')?.isConnected() ?? false;
455
+ }
456
+ /** Get the Feishu channel for a user (for direct access like syncGroups) */
457
+ getFeishuConnection(userId) {
458
+ return this.connections.get(userId)?.channels.get('feishu');
459
+ }
460
+ /** Get the Telegram channel for a user */
461
+ getTelegramConnection(userId) {
462
+ return this.connections.get(userId)?.channels.get('telegram');
463
+ }
464
+ /** Get the QQ channel for a user */
465
+ getQQConnection(userId) {
466
+ return this.connections.get(userId)?.channels.get('qq');
467
+ }
468
+ /** Get chat info from the Feishu API for a specific user's connection */
469
+ async getFeishuChatInfo(userId, chatId) {
470
+ const channel = this.getFeishuConnection(userId);
471
+ if (!channel?.getChatInfo)
472
+ return null;
473
+ return channel.getChatInfo(chatId);
474
+ }
475
+ /**
476
+ * Get chat info for an IM group by JID, auto-routing to the correct connection.
477
+ * Used for health checks to detect disbanded groups.
478
+ *
479
+ * Returns:
480
+ * - object: chat info (reachable)
481
+ * - null: channel supports getChatInfo but chat is not reachable
482
+ * - undefined: channel does not support getChatInfo (e.g. Telegram, QQ)
483
+ */
484
+ async getChatInfo(jid) {
485
+ const channelType = getChannelType(jid);
486
+ if (!channelType)
487
+ return null;
488
+ const chatId = extractChatId(jid);
489
+ const channel = this.findChannelForJid(jid, channelType);
490
+ if (channel?.getChatInfo) {
491
+ return channel.getChatInfo(chatId);
492
+ }
493
+ // Channel doesn't implement getChatInfo — not a reachability failure
494
+ return undefined;
495
+ }
496
+ /** Get all user IDs with active connections */
497
+ getConnectedUserIds() {
498
+ const ids = [];
499
+ for (const [userId, conn] of this.connections.entries()) {
500
+ for (const ch of conn.channels.values()) {
501
+ if (ch.isConnected()) {
502
+ ids.push(userId);
503
+ break;
504
+ }
505
+ }
506
+ }
507
+ return ids;
508
+ }
509
+ /**
510
+ * Disconnect all IM connections for all users.
511
+ * Called during graceful shutdown.
512
+ */
513
+ async disconnectAll() {
514
+ const promises = [];
515
+ for (const [userId, conn] of this.connections.entries()) {
516
+ for (const [channelType, channel] of conn.channels.entries()) {
517
+ promises.push(channel.disconnect().catch((err) => {
518
+ logger.warn({ userId, channelType, err }, 'Error stopping IM channel');
519
+ }));
520
+ }
521
+ }
522
+ await Promise.allSettled(promises);
523
+ this.connections.clear();
524
+ logger.info('All IM connections disconnected');
525
+ }
526
+ }
527
+ export const imManager = new IMConnectionManager();
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared IM utilities extracted from qq.ts / dingtalk.ts / wechat.ts
3
+ * to eliminate code duplication.
4
+ *
5
+ */
6
+ // ── Markdown → Plain Text ────────────────────────────────
7
+ export function markdownToPlainText(md) {
8
+ let text = md;
9
+ // Code blocks: keep content, remove fences
10
+ text = text.replace(/```[\s\S]*?```/g, (match) => {
11
+ return match.replace(/^```\w*\n?/, '').replace(/\n?```$/, '');
12
+ });
13
+ // Inline code: remove backticks
14
+ text = text.replace(/`([^`]+)`/g, '$1');
15
+ // Links: [text](url) → text (url)
16
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)');
17
+ // Bold: **text** or __text__ → text
18
+ text = text.replace(/\*\*(.+?)\*\*/g, '$1');
19
+ text = text.replace(/__(.+?)__/g, '$1');
20
+ // Strikethrough: ~~text~~ → text
21
+ text = text.replace(/~~(.+?)~~/g, '$1');
22
+ // Italic: *text* → text
23
+ text = text.replace(/(?<!\w)\*(?!\s)(.+?)(?<!\s)\*(?!\w)/g, '$1');
24
+ // Headings: # text → text
25
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, '$1');
26
+ return text;
27
+ }
28
+ // ── Text Chunking ──────────────────────────────────────
29
+ export function splitTextChunks(text, limit) {
30
+ if (text.length <= limit)
31
+ return [text];
32
+ const chunks = [];
33
+ let remaining = text;
34
+ while (remaining.length > 0) {
35
+ if (remaining.length <= limit) {
36
+ chunks.push(remaining);
37
+ break;
38
+ }
39
+ let splitIdx = remaining.lastIndexOf('\n\n', limit);
40
+ if (splitIdx < limit * 0.3) {
41
+ splitIdx = remaining.lastIndexOf('\n', limit);
42
+ }
43
+ if (splitIdx < limit * 0.3) {
44
+ splitIdx = remaining.lastIndexOf(' ', limit);
45
+ }
46
+ if (splitIdx < limit * 0.3) {
47
+ splitIdx = limit;
48
+ }
49
+ chunks.push(remaining.slice(0, splitIdx));
50
+ remaining = remaining.slice(splitIdx).trimStart();
51
+ }
52
+ return chunks;
53
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Detect image MIME type from buffer magic bytes.
3
+ * Canonical source — synced to src/ and container/agent-runner/src/ via make sync-types.
4
+ *
5
+ * Returns the detected MIME type or null if unknown.
6
+ */
7
+ export function detectImageMimeTypeStrict(buffer) {
8
+ if (buffer.length < 12) {
9
+ return null;
10
+ }
11
+ // PNG: 89 50 4E 47 0D 0A 1A 0A
12
+ if (buffer[0] === 0x89 &&
13
+ buffer[1] === 0x50 &&
14
+ buffer[2] === 0x4e &&
15
+ buffer[3] === 0x47 &&
16
+ buffer[4] === 0x0d &&
17
+ buffer[5] === 0x0a &&
18
+ buffer[6] === 0x1a &&
19
+ buffer[7] === 0x0a) {
20
+ return 'image/png';
21
+ }
22
+ // JPEG: FF D8 FF
23
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
24
+ return 'image/jpeg';
25
+ }
26
+ // GIF: 47 49 46 38
27
+ if (buffer[0] === 0x47 &&
28
+ buffer[1] === 0x49 &&
29
+ buffer[2] === 0x46 &&
30
+ buffer[3] === 0x38) {
31
+ return 'image/gif';
32
+ }
33
+ // WebP: RIFF....WEBP
34
+ if (buffer[0] === 0x52 &&
35
+ buffer[1] === 0x49 &&
36
+ buffer[2] === 0x46 &&
37
+ buffer[3] === 0x46 &&
38
+ buffer.length >= 12 &&
39
+ buffer[8] === 0x57 &&
40
+ buffer[9] === 0x45 &&
41
+ buffer[10] === 0x42 &&
42
+ buffer[11] === 0x50) {
43
+ return 'image/webp';
44
+ }
45
+ // TIFF: 49 49 2A 00 (little-endian) or 4D 4D 00 2A (big-endian)
46
+ if ((buffer[0] === 0x49 &&
47
+ buffer[1] === 0x49 &&
48
+ buffer[2] === 0x2a &&
49
+ buffer[3] === 0x00) ||
50
+ (buffer[0] === 0x4d &&
51
+ buffer[1] === 0x4d &&
52
+ buffer[2] === 0x00 &&
53
+ buffer[3] === 0x2a)) {
54
+ return 'image/tiff';
55
+ }
56
+ // AVIF: ....ftypavif or ....ftypavis
57
+ if (buffer.length >= 12) {
58
+ const ftyp = buffer.toString('ascii', 4, 8);
59
+ if (ftyp === 'ftyp') {
60
+ const brand = buffer.toString('ascii', 8, 12);
61
+ if (brand === 'avif' || brand === 'avis') {
62
+ return 'image/avif';
63
+ }
64
+ }
65
+ }
66
+ // BMP: 42 4D
67
+ if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
68
+ return 'image/bmp';
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Detect image MIME type from buffer magic bytes with fallback.
74
+ */
75
+ export function detectImageMimeType(buffer) {
76
+ return detectImageMimeTypeStrict(buffer) || 'image/jpeg';
77
+ }
78
+ /**
79
+ * Detect image MIME type from base64 payload (using header bytes only).
80
+ * Returns detected MIME or null if unknown/invalid.
81
+ */
82
+ export function detectImageMimeTypeFromBase64Strict(base64Data) {
83
+ try {
84
+ const header = Buffer.from(base64Data.slice(0, 400), 'base64');
85
+ return detectImageMimeTypeStrict(header);
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Detect image MIME type from base64 payload with fallback.
93
+ */
94
+ export function detectImageMimeTypeFromBase64(base64Data) {
95
+ return detectImageMimeTypeFromBase64Strict(base64Data) || 'image/jpeg';
96
+ }