@turmind/halo 0.1.9

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 (296) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/admin-out/404.html +1 -0
  4. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
  5. package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
  6. package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
  7. package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
  8. package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
  9. package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
  10. package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
  11. package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
  12. package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
  13. package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
  14. package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
  15. package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
  16. package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
  17. package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
  18. package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
  19. package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
  20. package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
  21. package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
  22. package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
  23. package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
  24. package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
  25. package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
  26. package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
  27. package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
  28. package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
  29. package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
  30. package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
  31. package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
  32. package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  33. package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
  34. package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
  35. package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
  36. package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
  37. package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
  38. package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
  39. package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
  40. package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
  41. package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
  42. package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
  43. package/admin-out/favicon.ico +0 -0
  44. package/admin-out/icon.svg +18 -0
  45. package/admin-out/index.html +1 -0
  46. package/admin-out/index.txt +23 -0
  47. package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
  48. package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
  49. package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
  50. package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
  51. package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
  52. package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
  53. package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
  54. package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
  55. package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
  56. package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  57. package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
  58. package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
  59. package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
  60. package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
  61. package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
  62. package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
  63. package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
  64. package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
  65. package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
  66. package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
  67. package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
  68. package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
  69. package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  70. package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
  71. package/admin-out/monaco/vs/editor/editor.main.css +1 -0
  72. package/admin-out/monaco/vs/editor/editor.main.js +5 -0
  73. package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
  74. package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
  75. package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
  76. package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
  77. package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  78. package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
  79. package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
  80. package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
  81. package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
  82. package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
  83. package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
  84. package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
  85. package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
  86. package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
  87. package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
  88. package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
  89. package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  90. package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
  91. package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
  92. package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
  93. package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  94. package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
  95. package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
  96. package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
  97. package/admin-out/monaco/vs/loader.js +1368 -0
  98. package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
  99. package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
  100. package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
  101. package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
  102. package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
  103. package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
  104. package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
  105. package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
  106. package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
  107. package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
  108. package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
  109. package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
  110. package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
  111. package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
  112. package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
  113. package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
  114. package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
  115. package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
  116. package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
  117. package/admin-out/monaco/vs/nls.messages.js.js +10 -0
  118. package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
  119. package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
  120. package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
  121. package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
  122. package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
  123. package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
  124. package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
  125. package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
  126. package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
  127. package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  128. package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
  129. package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
  130. package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
  131. package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
  132. package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  133. package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  134. package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
  135. package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  136. package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
  137. package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
  138. package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  139. package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
  140. package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
  141. package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
  142. package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
  143. package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  144. package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
  145. package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
  146. package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
  147. package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
  148. package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
  149. package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
  150. package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
  151. package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  152. package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  153. package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
  154. package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
  155. package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
  156. package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
  157. package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  158. package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
  159. package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
  160. package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
  161. package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
  162. package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
  163. package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
  164. package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  165. package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
  166. package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
  167. package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
  168. package/bin/halo.js +9 -0
  169. package/bundled-docs/design/agent.md +145 -0
  170. package/bundled-docs/design/architecture.md +270 -0
  171. package/bundled-docs/design/background-dispatch.md +151 -0
  172. package/bundled-docs/design/command.md +58 -0
  173. package/bundled-docs/design/cron.md +88 -0
  174. package/bundled-docs/design/evolution.md +383 -0
  175. package/bundled-docs/design/express-self.md +145 -0
  176. package/bundled-docs/design/feishu.md +224 -0
  177. package/bundled-docs/design/halo-city.md +112 -0
  178. package/bundled-docs/design/prompt-system.md +411 -0
  179. package/bundled-docs/design/session.md +321 -0
  180. package/bundled-docs/design/slack.md +212 -0
  181. package/bundled-docs/design/storage.md +578 -0
  182. package/bundled-docs/design/telegram.md +161 -0
  183. package/bundled-docs/design/web.md +222 -0
  184. package/bundled-docs/design/wechat.md +146 -0
  185. package/bundled-docs/design/ws.md +144 -0
  186. package/bundled-docs/dev/acp-adapter.md +337 -0
  187. package/bundled-docs/dev/add-channel.md +376 -0
  188. package/bundled-docs/dev/add-model-provider.md +386 -0
  189. package/bundled-docs/dev/api.md +686 -0
  190. package/bundled-docs/dev/deploy.md +198 -0
  191. package/bundled-docs/dev/desktop-packaging.md +327 -0
  192. package/bundled-docs/dev/env.md +158 -0
  193. package/bundled-docs/dev/logging.md +85 -0
  194. package/bundled-docs/dev/previews.md +225 -0
  195. package/bundled-docs/dev/tools.md +331 -0
  196. package/bundled-docs/guide/README.md +29 -0
  197. package/bundled-docs/guide/agents.md +264 -0
  198. package/bundled-docs/guide/channels/README.md +62 -0
  199. package/bundled-docs/guide/channels/acp.md +150 -0
  200. package/bundled-docs/guide/channels/feishu.md +154 -0
  201. package/bundled-docs/guide/channels/slack.md +156 -0
  202. package/bundled-docs/guide/channels/telegram.md +112 -0
  203. package/bundled-docs/guide/channels/web.md +149 -0
  204. package/bundled-docs/guide/channels/wechat.md +110 -0
  205. package/bundled-docs/guide/chat.md +112 -0
  206. package/bundled-docs/guide/cli.md +197 -0
  207. package/bundled-docs/guide/getting-started.md +93 -0
  208. package/bundled-docs/guide/secrets-and-credentials.md +161 -0
  209. package/bundled-docs/guide/sessions.md +104 -0
  210. package/bundled-docs/guide/skills.md +302 -0
  211. package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
  212. package/bundled-docs/guide/workspace.md +131 -0
  213. package/bundled-docs/plans/cron-tasks.md +362 -0
  214. package/bundled-docs/plans/design-suggestions.md +203 -0
  215. package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
  216. package/bundled-docs/plans/launch-plan.md +111 -0
  217. package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
  218. package/bundled-docs/plans/remote-mac-worker.md +196 -0
  219. package/bundled-docs/plans/roadmap.md +40 -0
  220. package/bundled-docs/plans/self-evolution.md +1379 -0
  221. package/bundled-docs/plans/todo.md +32 -0
  222. package/bundled-docs/plans/weixin-channel.md +255 -0
  223. package/bundled-docs/requirements/agent.md +92 -0
  224. package/bundled-docs/requirements/chat.md +81 -0
  225. package/bundled-docs/requirements/command.md +130 -0
  226. package/bundled-docs/requirements/editor.md +82 -0
  227. package/bundled-docs/requirements/explorer.md +89 -0
  228. package/bundled-docs/requirements/overview.md +62 -0
  229. package/bundled-docs/requirements/session.md +73 -0
  230. package/bundled-docs/requirements/settings.md +224 -0
  231. package/bundled-docs/requirements/skill.md +125 -0
  232. package/bundled-docs/requirements/source-control.md +82 -0
  233. package/bundled-docs/requirements/terminal.md +118 -0
  234. package/bundled-docs/requirements/workspace.md +80 -0
  235. package/bundled-docs/test/docs-seeding.md +101 -0
  236. package/bundled-docs/test/session.md +374 -0
  237. package/dist/index.js +26750 -0
  238. package/package.json +67 -0
  239. package/templates/INSTRUCTIONS.md +25 -0
  240. package/templates/agents/__apply_agent__/AGENT.md +229 -0
  241. package/templates/agents/__apply_agent__/agent.yaml +30 -0
  242. package/templates/agents/__evo_agent__/AGENT.md +323 -0
  243. package/templates/agents/__evo_agent__/agent.yaml +29 -0
  244. package/templates/agents/__score__/AGENT.md +177 -0
  245. package/templates/agents/__score__/agent.yaml +29 -0
  246. package/templates/agents/deep-executor/AGENT.md +60 -0
  247. package/templates/agents/deep-executor/agent.yaml +25 -0
  248. package/templates/agents/default/AGENT.md +38 -0
  249. package/templates/agents/default/agent.yaml +51 -0
  250. package/templates/agents/executor/AGENT.md +38 -0
  251. package/templates/agents/executor/agent.yaml +25 -0
  252. package/templates/aliases.yaml +26 -0
  253. package/templates/canvas/self.html +660 -0
  254. package/templates/config.yaml +69 -0
  255. package/templates/models/anthropic.yaml +47 -0
  256. package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
  257. package/templates/models/aws-bedrock-mantle.yaml +89 -0
  258. package/templates/models/deepseek.yaml +62 -0
  259. package/templates/models/doubao.yaml +137 -0
  260. package/templates/models/hunyuan.yaml +56 -0
  261. package/templates/models/kimi.yaml +41 -0
  262. package/templates/models/mimo-token-plan-china.yaml +61 -0
  263. package/templates/models/minimax.yaml +80 -0
  264. package/templates/models/openai.yaml +51 -0
  265. package/templates/models/qwen.yaml +78 -0
  266. package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
  267. package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
  268. package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
  269. package/templates/prompts/all/TOOL_SHELL.md +4 -0
  270. package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
  271. package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
  272. package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
  273. package/templates/schema.sql +30 -0
  274. package/templates/skills/acp/SKILL.md +236 -0
  275. package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
  276. package/templates/skills/acp/templates/ask.py +176 -0
  277. package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
  278. package/templates/skills/agent/SKILL.md +128 -0
  279. package/templates/skills/aws-knowledge/SKILL.md +82 -0
  280. package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
  281. package/templates/skills/cron/SKILL.md +217 -0
  282. package/templates/skills/cron/manage-cron.py +434 -0
  283. package/templates/skills/halo/SKILL.md +98 -0
  284. package/templates/skills/nova-web-search/SKILL.md +65 -0
  285. package/templates/skills/self/SKILL.md +139 -0
  286. package/templates/skills/send-file/SKILL.md +28 -0
  287. package/templates/skills/send-file/feishu.md +16 -0
  288. package/templates/skills/send-file/slack.md +7 -0
  289. package/templates/skills/send-file/telegram.md +9 -0
  290. package/templates/skills/send-file/web.md +6 -0
  291. package/templates/skills/send-file/wechat.md +8 -0
  292. package/templates/skills/skill/SKILL.md +131 -0
  293. package/templates/skills/workspace/SKILL.md +28 -0
  294. package/templates/skills/workspace/setup-tidy.md +265 -0
  295. package/templates/skills/workspace/share.md +187 -0
  296. package/templates/skills/workspace/stage.py +487 -0
@@ -0,0 +1,161 @@
1
+ # Telegram Channel — Design
2
+
3
+ Let the user talk to Halo from Telegram, sharing the same workspace + sessions as the web and WeChat sides.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌── ws/ (web channel) ─┐
9
+ ├── channels/weixin/ │
10
+ Halo server (9527) ──┤ ├── SessionManager
11
+ └── channels/telegram/ ─┘ (per workspace, via Registry)
12
+ ↕ grammy long-poll
13
+ api.telegram.org
14
+ ```
15
+
16
+ All channels are peers — each one is a subscriber + caller against SessionManager.
17
+
18
+ ## Data model
19
+
20
+ ### Workspace ↔ Bot mapping
21
+
22
+ One bot (= one BotFather token) is bound to one workspace. A workspace can bind multiple bots.
23
+
24
+ ### Session strategy
25
+
26
+ - **One Telegram user → many sessions (one active at a time)**
27
+ - Session ID format: `tg_<userId>_<createdAtBase36>` (e.g. `tg_123456789_m1abc`)
28
+ - Sessions live under the bot's bound workspace and use the highest-priority agent (falls back to `default` only when none exists)
29
+ - The web side sees these sessions in the session list (labelled `Telegram: <userId>`)
30
+ - Access level inherited from the bot account
31
+
32
+ ### Access level
33
+
34
+ Each account carries `accessLevel: 'full' | 'workspace' | 'readonly'` (default `readonly`). Same semantics as WeChat — `readonly` runs tools in a read-only bwrap sandbox, `workspace` runs tools in a read-write sandbox confined to the workspace, `full` is unrestricted (no sandbox).
35
+
36
+ ### Allowed users
37
+
38
+ Optional whitelist (`allowedUsers` in config JSON): comma-separated user IDs or @usernames. Empty = allow everyone.
39
+
40
+ ### Storage
41
+
42
+ Telegram bot accounts are stored in the unified channel DB: `~/.halo/secrets/channels/channels.db`, table `channel_accounts` with `channel_type = 'telegram'`. See [storage.md](storage.md#channel_accounts) for the full schema.
43
+
44
+ Telegram-specific config JSON fields: `botToken`, `botUsername`, `allowedUsers`, `lastActiveChatId`.
45
+
46
+ `lastActiveChatId` is a runtime cache of the most recent chat id the bot has exchanged messages with. Written by `rememberLastActiveChat()` in `channels/shared/accounts.ts` on every inbound message, but with a per-process hash so unchanged values never touch the db (idempotent — on the hot path we skip the read-modify-write entirely after the first sight). It is **not** used for cron dispatch (see below); it is kept as a runtime cache for any future "reply to whoever talked last" feature.
47
+
48
+ ### Proactive sending (cron)
49
+
50
+ Halo can send to a Telegram chat without that user having messaged the bot first — Telegram's Bot API allows `sendMessage(chatId, text)` for any known chat id. Halo only requires that the chat id come from a *trusted source*.
51
+
52
+ The telegram cron-dispatcher (`channels/telegram/cron-dispatcher.ts`) registers itself with `cron/dispatcher.ts`'s registry at boot and **requires an explicit `chatId`** (numeric — Telegram private-chat ids equal user ids):
53
+
54
+ - Cron jobs created from inside a telegram chat via the `cron` skill auto-pin the current chat id (target `telegram:<account>:<chatId>`).
55
+ - Admin-UI cron jobs that don't specify a target run silently — the result shows in the cron log, nothing is pushed.
56
+ - `@username` entries are not accepted by `sendMessage`; the dispatcher rejects non-numeric values with a clear error.
57
+
58
+ Earlier drafts considered "fan-out to every id in `allowedUsers`" or "fall back to `lastActiveChatId`", but in practice both pushed the cron output to strangers' chats. The cron creator's intent is "reach me, the person who set this up" — so the only target is what the caller passes explicitly.
59
+
60
+ ## Modules
61
+
62
+ Files: `packages/server/src/channels/telegram/`
63
+
64
+ - `types.ts` — TelegramAccount interface
65
+ - `accounts.ts` — thin adapter over `channels/shared/accounts.ts` (maps config JSON ↔ TelegramAccount)
66
+ - `handler.ts` — grammy Bot setup, command registration, message handling, polling loop
67
+ - `event-adapter.ts` — LLM streaming events → Telegram messages (coalesce same as WeChat)
68
+
69
+ Routes: `packages/server/src/routes/telegram.ts`
70
+
71
+ - `POST /api/telegram/accounts` — register a bot (validates token via `getMe`)
72
+ - `GET /api/telegram/accounts` — list bots
73
+ - `PATCH /api/telegram/accounts/:id` — update config
74
+ - `DELETE /api/telegram/accounts/:id` — remove + stop
75
+
76
+ ## Bot lifecycle
77
+
78
+ 1. User registers a bot via the API (provides token + workspace)
79
+ 2. Server calls `getMe` to validate; stores account; starts polling
80
+ 3. grammy's built-in long-polling handles reconnection / retries
81
+ 4. On graceful shutdown, `bot.stop()` is called for each active bot
82
+
83
+ ## Message handling flow
84
+
85
+ `bot.on('message:text')`:
86
+ 1. Check user against `allowedUsers` whitelist
87
+ 2. Resolve workspace (check path exists on disk)
88
+ 3. Resolve or create active session for this Telegram userId
89
+ 4. If session is compacting → reply "⏳ wait"
90
+ 5. If session is busy → reply "🔄 queued"
91
+ 6. Register a `TelegramResponder` event listener (once per session)
92
+ 7. `sm.sendUserMessage(sessionId, agentInput)` with channel prefix
93
+
94
+ Photos / documents trigger the same flow with file metadata in the text.
95
+
96
+ ## Slash commands (native Telegram /commands)
97
+
98
+ | Command | Purpose |
99
+ |---|---|
100
+ | `/start` | Welcome message |
101
+ | `/session new` | Create a new session |
102
+ | `/session list` | List recent sessions (up to 20) |
103
+ | `/session switch <index>` | Switch active session |
104
+ | `/session stop` | Abort running task |
105
+ | `/session compact` | Compress context |
106
+ | `/workspace info` | Show workspace; `/workspace switch <path>` changes it (full only) |
107
+ | `/help` | List commands |
108
+
109
+ ## Event coalescing (TelegramResponder)
110
+
111
+ Same strategy as WeChat:
112
+ - Buffer until `complete` event, then flush as one message
113
+ - Flush happens on **any** `complete` — the responder doesn't read its `batchBoundary` flag, so a multi-round queue drain ships each merged turn as its own message instead of one blob (see [session.md](session.md#message-queue-and-drain))
114
+ - If buffer exceeds 4000 chars (Telegram limit), split at paragraph boundary
115
+ - `MEDIA:<path>` markers are intercepted and sent as native Telegram media
116
+ - Errors flush immediately
117
+ - Sub-agent events are dropped (visible in web UI only)
118
+
119
+ ## Media support
120
+
121
+ Inbound: text, photos (file URL in text), documents (file URL in text).
122
+
123
+ Outbound: the `send-file` skill produces `MEDIA:<path>` markers. The responder sends files via grammy's `InputFile`:
124
+ - `.jpg/.png/.gif/.webp/.bmp` → `sendPhoto`
125
+ - `.mp4/.mov/.webm` → `sendVideo`
126
+ - `.ogg/.oga` → `sendVoice`
127
+ - anything else → `sendDocument`
128
+
129
+ ## Configuration
130
+
131
+ ### BotFather setup
132
+
133
+ 1. `/newbot` → get token
134
+ 2. Optional: `/setcommands` to register the command menu:
135
+ ```
136
+ start - 开始
137
+ session - 会话管理(new/list/switch/stop/compact…)
138
+ agent - Agent 管理
139
+ skill - Skill 管理
140
+ ws - 查看/切换workspace
141
+ help - 帮助
142
+ ```
143
+
144
+ ### Register to Halo
145
+
146
+ ```bash
147
+ curl -X POST http://localhost:9527/api/telegram/accounts \
148
+ -H 'Content-Type: application/json' \
149
+ -d '{
150
+ "botToken": "123456:ABC-DEF...",
151
+ "workspacePath": "/home/user/my-workspace",
152
+ "accessLevel": "full",
153
+ "allowedUsers": "123456789,@myusername"
154
+ }'
155
+ ```
156
+
157
+ ## Scope and out-of-scope
158
+
159
+ Supported: private chat text, photos, documents; slash commands; per-session access level; user whitelist; media sending.
160
+
161
+ Not supported: group chats, inline queries, callback buttons, webhook mode (polling only).
@@ -0,0 +1,222 @@
1
+ # Web Channel — Design
2
+
3
+ Browser-based access to Halo via token-authenticated HTTP API. Supports SSE streaming, slash commands, image/voice upload.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌── ws/ (admin channel) ─┐
9
+ ├── channels/wechat/ │
10
+ Halo server (9527) ──┤── channels/telegram/ ├── SessionManager
11
+ └── channels/web/ ─┘ (per workspace, via Registry)
12
+ ↕ HTTP + SSE
13
+ web-demo (9528) ── browser
14
+ or any HTTP client
15
+ ```
16
+
17
+ Common slash commands (`/help`, `/evo`, and the object commands `/session`, `/agent`, `/skill`, `/workspace`) live in `channels/shared/commands.ts`; each channel handler is a thin adapter.
18
+
19
+ Web channel is a public HTTP API on Halo. `packages/web-demo` is a standalone demo frontend that proxies requests through its own auth layer.
20
+
21
+ ## Data model
22
+
23
+ ### Account / Token
24
+
25
+ One token = one web access point, bound to one workspace.
26
+
27
+ Storage: `~/.halo/secrets/channels/channels.db`, table `channel_accounts` with `channel_type = 'web'`. See [storage.md](storage.md#channel_accounts) for the full schema.
28
+
29
+ Web-specific config JSON fields: `token` (auto-generated base64url, 24 bytes).
30
+
31
+ ### Session strategy
32
+
33
+ - **One account → many sessions (one active at a time)**
34
+ - Session ID format: `web_<accountId>_<createdAtBase36>`
35
+ - Active session tracked in memory (`activeOverrides` Map); defaults to most recent
36
+ - Sessions live under the account's bound workspace using the highest-priority agent (falls back to `default` only when none exists)
37
+ - Admin panel and other channels see these sessions in their `/session list` (tagged `[web]`)
38
+ - Access level inherited from the account
39
+
40
+ ### Commands
41
+
42
+ Slash commands are intercepted before reaching the agent:
43
+
44
+ | Command | Effect |
45
+ |---|---|
46
+ | `/help` | List available commands |
47
+ | `/session new` | Create a new session |
48
+ | `/session list` | List all sessions (show ownership tags) |
49
+ | `/session switch <n>` | Switch to session by number (readonly can only switch to own) |
50
+ | `/session stop` | Interrupt current running task |
51
+ | `/session compact` | Compress session context |
52
+ | `/workspace info` | Show current workspace |
53
+ | `/workspace switch <path>` | Switch workspace (full access only) |
54
+
55
+ ### Media handling
56
+
57
+ - **Images** (jpeg/png/gif/webp): passed directly to LLM via multimodal content blocks
58
+ - **Audio/other files**: saved to `<workspace>/.halo/assets/web/inbound/<accountId>/<yyyy-mm-dd>/` and path reported to agent in message text
59
+
60
+ ## Halo API — Public endpoints
61
+
62
+ File: `packages/server/src/routes/web.ts`
63
+
64
+ All public endpoints require `x-token` header (or `?token=` query param) with a valid account token. No cookie auth needed.
65
+
66
+ > These paths are listed in `PUBLIC_PATHS` (`middleware/auth.ts`) so they bypass the admin cookie gate: `/api/web/chat`, `/api/web/stop`, `/api/web/history`, `/api/web/subscribe`, `/api/web/file`, and `/api/show/state` (the [halo-city](../../../halo-city/) snapshot — see [dev/api.md](../dev/api.md#show-world-snapshot)). The server CORS allowlist includes the `x-token` header, so browser-based custom frontends (web-demo, halo-city) can call these cross-origin.
67
+
68
+ ### POST `/api/web/chat`
69
+
70
+ Send a message and receive streaming response via SSE.
71
+
72
+ ```json
73
+ // Request
74
+ {
75
+ "message": "hello",
76
+ "images": [{ "data": "<base64>", "mimeType": "image/png" }], // optional
77
+ "workspace": "/abs/path", // optional override, full-access tokens only
78
+ "sessionId": "web_explicit", // optional override, see below
79
+ "agentId": "default" // optional, only used when creating a new session
80
+ }
81
+
82
+ // Response: text/event-stream
83
+ data: {"type":"session","sessionId":"web_abc123_m1xyz"}
84
+ data: {"type":"thinking","text":"..."}
85
+ data: {"type":"tool_call","toolName":"read_file","toolInput":{...}}
86
+ data: {"type":"tool_result","toolName":"read_file","result":"..."}
87
+ data: {"type":"stream","text":"Hello! "}
88
+ data: {"type":"stream","text":"How can I help?"}
89
+ data: {"type":"switch","sessionId":"..."} // after /session switch or /session new command
90
+ data: {"type":"complete"}
91
+ data: {"type":"error","error":"..."}
92
+ ```
93
+
94
+ > **Batch-boundary `complete` (must be absorbed, never closes the stream)**: when a root session drains a queue of multiple messages, the server runs N merged turns and emits an internal `complete` with `batchBoundary: true` between rounds (see [session.md](session.md#message-queue-and-drain)). The SSE generators in `channels/web/handler.ts` flush the just-finished round's text on a `batchBoundary` complete but **do not** send a `complete` SSE frame and **do not** set `done` — the response stays open for the next round; only the **terminal** (unmarked) `complete` closes the HTTP stream. Without this guard a producer→sub-agent fan-out would truncate the web client after the first round. The `batchBoundary` flag is therefore an internal server-side event marker only — it is never serialized into the SSE payload a client sees, so a custom frontend just consumes one ordinary `complete` at the end. (ACP rides on this channel, so it inherits the same safe behavior.)
95
+
96
+ #### Per-request overrides (`workspace`, `sessionId`, `agentId`)
97
+
98
+ By default each token is bound 1:1 to the workspace its admin row configured, and `/web/chat` operates on the account's "active" session (most-recently-used or one set by `/session new` / `/session switch`). External integrations — currently the [ACP adapter](../dev/acp-adapter.md) — need finer control:
99
+
100
+ - `workspace` (string, optional): server-side absolute path. Overrides `account.workspacePath` for this request only. **Gated on `accessLevel === 'full'`** — readonly / workspace tokens cannot escape their account-bound workspace; the gate returns an SSE `error` event.
101
+ - `sessionId` (string, optional): explicit halo session id. Bypasses the account's active-session pointer entirely. If the session doesn't yet exist, the server creates it with the supplied id (so callers can pre-mint stable ids and address them across reconnects).
102
+ - `agentId` (string, optional): only consulted when the request is creating a new session (no row yet for `sessionId`). Picks the agent profile to bootstrap with. Defaults to `default`.
103
+
104
+ Browser web-demo doesn't use any of these — it relies on per-token defaults. Three accepted transports per request, lowest-priority first:
105
+
106
+ 1. Query string: `?workspace=…&sessionId=…&agentId=…`
107
+ 2. Headers: `x-workspace`, `x-session-id`, `x-agent-id`
108
+ 3. POST body fields (highest priority).
109
+
110
+ `/api/web/stop`, `/api/web/history`, `/api/web/subscribe` accept the same `workspace` + `sessionId` overrides via query string / header. They don't accept `agentId` (no session creation path).
111
+
112
+ ### POST `/api/web/stop`
113
+
114
+ Stop the currently running task. Accepts optional `workspace` / `sessionId` overrides as documented above.
115
+
116
+ ```json
117
+ // Response
118
+ { "stopped": true }
119
+ ```
120
+
121
+ ### GET `/api/web/history`
122
+
123
+ Get a session's message history. Without overrides, returns the account's active session; with `sessionId` (and optionally `workspace`), returns whichever session you address.
124
+
125
+ ```json
126
+ // Response
127
+ {
128
+ "sessionId": "web_abc123_m1xyz",
129
+ "messages": [ { "id": "...", "role": "user", "content": "..." }, ... ],
130
+ "running": false
131
+ }
132
+ ```
133
+
134
+ ### GET `/api/web/subscribe`
135
+
136
+ Reconnect to a running session's event stream (same SSE format as `/chat`). If session is not running, returns a single `complete` event immediately. Accepts the same `workspace` / `sessionId` overrides.
137
+
138
+ ## Halo API — Admin endpoints
139
+
140
+ Protected by cookie auth (admin panel). File: `packages/server/src/routes/web.ts`
141
+
142
+ | Method | Path | Purpose |
143
+ |---|---|---|
144
+ | GET | `/api/web/accounts` | List all accounts |
145
+ | POST | `/api/web/accounts` | Create account (body: `{workspacePath, label?, accessLevel?, language?}`) |
146
+ | PATCH | `/api/web/accounts/:id` | Update account fields (label, workspacePath, enabled, accessLevel, language) |
147
+ | DELETE | `/api/web/accounts/:id` | Delete account (token invalidated) |
148
+
149
+ ---
150
+
151
+ ## packages/web-demo — Standalone Demo Frontend
152
+
153
+ An independent Express app that provides a browser UI for the web channel. It holds the Halo token server-side and exposes its own password-based auth.
154
+
155
+ ### Deployment
156
+
157
+ ```bash
158
+ cd packages/web-demo
159
+ node server.js
160
+ ```
161
+
162
+ ### Environment variables
163
+
164
+ | Variable | Required | Description |
165
+ |---|---|---|
166
+ | `HALO_API` | Yes | Halo server URL (e.g. `http://localhost:9527`) |
167
+ | `HALO_TOKEN` | Yes | Token from Halo admin panel (Web settings) |
168
+ | `HALO_WEB_DEMO_PASSWORD` | No | Password for web-demo login. Empty = open access |
169
+ | `PORT` | No | Listen port (default: `9528`) |
170
+
171
+ ### Security model
172
+
173
+ - `HALO_TOKEN` and `HALO_API` are server-side only, never exposed to the browser
174
+ - Frontend uses same-origin relative paths (`/chat`, `/history`, etc.)
175
+ - web-demo server proxies all requests to Halo, injecting the token in `x-token` header
176
+ - Session auth: random hex token stored in `x-session` header + localStorage
177
+
178
+ ### Proxy routes
179
+
180
+ | Frontend path | Upstream Halo path |
181
+ |---|---|
182
+ | `POST /chat` | `POST /api/web/chat` |
183
+ | `POST /stop` | `POST /api/web/stop` |
184
+ | `GET /history` | `GET /api/web/history` |
185
+ | `GET /subscribe` | `GET /api/web/subscribe` |
186
+
187
+ ### Features
188
+
189
+ - SSE streaming with tool call display (collapsible)
190
+ - Auto-reconnect: if session is running on page load, subscribes to live stream
191
+ - Message queue: can send while agent is running, messages queue and send sequentially
192
+ - Slash commands (`/help`, `/session`, `/agent`, `/skill`, `/workspace`)
193
+ - Image upload (file picker + camera capture)
194
+ - Voice recording (webm audio saved to workspace, path given to agent)
195
+ - Stop/interrupt button
196
+ - i18n: auto-detects browser language (`navigator.language`), toggle button in header, persists to `localStorage`
197
+
198
+ ## Integration guide (custom frontend)
199
+
200
+ To build your own frontend against Halo's web channel:
201
+
202
+ 1. Create an account in the admin panel (Channels → Web → Create)
203
+ 2. Copy the generated token
204
+ 3. Make HTTP requests with `x-token: <your-token>` header:
205
+
206
+ ```bash
207
+ # Send a message (SSE response)
208
+ curl -N -H "x-token: YOUR_TOKEN" -H "Content-Type: application/json" \
209
+ -d '{"message":"hello"}' \
210
+ http://localhost:9527/api/web/chat
211
+
212
+ # Stop current task
213
+ curl -X POST -H "x-token: YOUR_TOKEN" http://localhost:9527/api/web/stop
214
+
215
+ # Get history
216
+ curl -H "x-token: YOUR_TOKEN" http://localhost:9527/api/web/history
217
+
218
+ # Subscribe to running session
219
+ curl -N -H "x-token: YOUR_TOKEN" http://localhost:9527/api/web/subscribe
220
+ ```
221
+
222
+ SSE events are newline-delimited `data: {json}\n\n` lines. Parse the `type` field to handle each event kind.
@@ -0,0 +1,146 @@
1
+ # Weixin Channel — Design
2
+
3
+ Let the user talk to Halo from WeChat on their phone, sharing the same workspace + session as the web side.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ ┌── ws/ (admin channel) ─┐
9
+ ├── channels/web/ │
10
+ Halo server (9527) ──┤── channels/telegram/ ├── SessionManager
11
+ └── channels/wechat/ ─┘ (per workspace, via Registry)
12
+ ↕ HTTPS long-poll
13
+ ilinkai.weixin.qq.com
14
+ ```
15
+
16
+ All channels are peers — each one is a subscriber + caller against SessionManager. Common slash commands (`/help`, `/evo`, and the object commands `/session`, `/agent`, `/skill`, `/workspace`) live in `channels/shared/commands.ts`; each channel handler is a thin adapter that builds a `CommandContext` and formats the result for its transport.
17
+
18
+ ## Data model
19
+
20
+ ### Workspace ↔ Bot mapping
21
+
22
+ One bot (= one `ilink_bot_id` from a QR scan) is bound to one workspace. A workspace can bind multiple bots, but 1:1 is recommended.
23
+
24
+ ### Session strategy
25
+
26
+ - **One WeChat user → many sessions (one active at a time)**
27
+ - Session ID format: `wx_<normalizedUserId>_<createdAtBase36>` (e.g. `a1b2c3@im.wechat` → `wx_a1b2c3-im-wechat_<ts>`)
28
+ - Sessions live under the bot's bound workspace and use the highest-priority agent (falls back to `default` only when none exists)
29
+ - The web side sees these sessions in the session list (labelled `WeChat: <user>`)
30
+ - Access level inherited from the bot account (see below)
31
+
32
+ ### Access level
33
+
34
+ Each account carries `accessLevel: 'full' | 'workspace' | 'readonly'` (default `readonly`). Inbound messages create sessions with the matching access level:
35
+ - `full` — no restrictions (no sandbox)
36
+ - `workspace` — tool execution runs inside a bwrap sandbox with the workspace mounted read-write; `~/.halo/secrets/` is not mounted
37
+ - `readonly` — tool execution runs inside a bwrap sandbox with the workspace mounted read-only; `~/.halo/secrets/` is not mounted
38
+
39
+ Additionally, readonly sessions only receive read-only tools (file_read, view_image, file_list, grep, glob) — write/exec/fetch tools are not injected.
40
+
41
+ When bwrap is not installed, app-level `assertPathAllowed` enforces the same path boundaries; `shell_exec` is blocked entirely without bwrap.
42
+
43
+ Sub-sessions inherit the parent's access level — a readonly channel can't delegate to a full-access child and escape the sandbox.
44
+
45
+ ### Storage
46
+
47
+ WeChat bot accounts are stored in the unified channel DB: `~/.halo/secrets/channels/channels.db`, table `channel_accounts` with `channel_type = 'wechat'`. See [storage.md](storage.md#channel_accounts) for the full schema.
48
+
49
+ WeChat-specific config JSON fields: `botToken`, `baseUrl`, `userId`, `syncBuf`, `lastActiveChatId`.
50
+
51
+ - `userId` is the QR-bind owner (the ilink_user_id of whoever scanned the QR code to register this bot). Used by the wechat cron-dispatcher as the default proactive-send target — sending here means cron output goes back to the bot's owner, which is the common-case "report to me on a schedule" intent.
52
+ - `lastActiveChatId` is a runtime cache of the most recent inbound `from_user_id`. Written by `rememberLastActiveChat()` in `channels/shared/accounts.ts` with a per-process hash so unchanged values never touch the db (idempotent — hot path stays in memory). Used as a fallback for cron sends in the shared-bot case where you want to reply to whoever talked last.
53
+
54
+ ### Proactive sending (cron)
55
+
56
+ The wechat cron-dispatcher (`channels/wechat/cron-dispatcher.ts`) registers itself with `cron/dispatcher.ts`'s registry at boot. On a fire it picks the recipient in this order:
57
+
58
+ 1. **Explicit `chatId` on the target** — when the cron was created from inside a wechat chat, the target carries the originating openId; dispatch sends only there.
59
+ 2. **`account.userId`** — the QR-bind owner, the "report to me on a schedule" default.
60
+ 3. **`lastActiveChatId`** — shared-bot fallback (reply to whoever talked last).
61
+
62
+ WeChat is single-recipient per dispatch (no fan-out across `allowedUsers` like telegram has). If none of the three yields an id, dispatch fails with a clear "no wechat target — bind the account first" message.
63
+
64
+ ## Modules
65
+
66
+ Files: `packages/server/src/channels/wechat/`
67
+
68
+ - `api.ts` — HTTP client (getUpdates long-poll / sendMessage / notifyStart / notifyStop)
69
+ - `login.ts` — QR login (startWeixinLoginWithQr + waitForWeixinLogin)
70
+ - `accounts.ts` — DAL (insertAccount / updateAccount / deleteAccount / list / normalize / saveSyncBuf)
71
+ - `cdn.ts` — media CDN client (download + decrypt)
72
+ - `media-store.ts` — saves inbound media to the workspace. Shared with the web chat channel: WeChat uses `<workspace>/.halo/assets/weixin/inbound/<accountId>/<date>/`, web pasted images land in `<workspace>/.halo/assets/web/inbound/<date>/`. Both emit `[图片已保存: /abs/path]` markers that the UI turns into media chips.
73
+ - `send-media.ts` — uploads and sends files/images out
74
+ - `handler.ts` — long-poll main loop, routes messages to SessionManager + slash-command dispatch
75
+ - `event-adapter.ts` — LLM streaming events → WeChat whole-message send (coalesce by 200 chars / 3s silence / complete flush)
76
+
77
+ ## SessionManager dependencies
78
+
79
+ - **Multi-subscriber listener**: `eventListeners: Map<rootId, Set<handler>>` — web and weixin can subscribe to the same session simultaneously
80
+ - **Registry**: `SessionManagerRegistry` caches SessionManager instances by workspace path; web and weixin share it
81
+
82
+ ## Long-poll main loop
83
+
84
+ Per enabled account:
85
+
86
+ ```
87
+ loop {
88
+ resp = await getUpdates({ baseUrl, token, sync_buf })
89
+ for msg in resp.msgs:
90
+ handleInbound(account, msg)
91
+ sync_buf = resp.get_updates_buf // also saveSyncBuf to DB
92
+ }
93
+ ```
94
+
95
+ > **单实例依赖**:长轮询循环跟 HTTP server 解耦 —— 即便 `:9527` 没绑上,只要进程活着,`runAccountLoop` 就继续拉消息。如果有孤儿 server 进程残留,每个进程都会独立收到同一条消息并 fan out 到不同 session(每个进程内存里的 `activeOverrides` 是独立的,`findActiveSessionId` 可能返回不同结果)。因此 `src/index.ts` 在启动时要抢 `~/.halo/global/server.pid` 单实例锁,详见 [dev/deploy.md](../dev/deploy.md#4-start-the-server)。
96
+
97
+ `handleInbound(account, msg)`:
98
+ 1. Extract `from_user_id` / text + media items / `context_token`
99
+ 2. Process inbound media: images → base64 attached to the agent message; voice / video / files → saved under the workspace with a `[voice saved: ...]` etc. marker appended to the text
100
+ 3. If the text begins with `/`, dispatch a slash command (see below) — if it returns `handled`, stop here
101
+ 4. sm = `registry.getOrCreate(account.workspacePath)`
102
+ 5. Resolve the active session for this user (override from `/session new`/`/session switch`; otherwise the most recent non-archived session matching the user prefix)
103
+ 6. If no active session exists, create one with the inherited access level
104
+ 7. If the session is compacting, reply with `⏳ integrating context, try again in ~30s` and skip queueing
105
+ 8. If the session is busy, reply with `🔄 still processing last message, queued` and still enqueue
106
+ 9. Register a WeixinResponder event listener (once per session) that coalesces outbound text
107
+ 10. `sm.sendUserMessage(sessionId, agentInput, images?)` — the actual agent input is prefixed with `[channel: wechat | user: <id>]` so the agent knows how to address the media-send skill
108
+
109
+ ## Slash commands
110
+
111
+ Implemented in `handler.ts`'s `handleSlashCommand()`:
112
+
113
+ | Command | Purpose |
114
+ |---|---|
115
+ | `/session new` | Create a new session; old sessions remain accessible via `/session list` + `/session switch` (nothing is archived) |
116
+ | `/session list` | List recent sessions (newest first, up to 20); the active one is marked `→` |
117
+ | `/session switch <index>` | Switch the active session to the indexed entry from `/session list`. Override is in-memory per user per account. Readonly bot 仅能切到自己 prefix 下的 session(跨用户会话拒绝)。 |
118
+ | `/workspace info` | Show the current workspace. `/workspace switch <path>` 切换(绝对路径),**仅 full 权限 bot 可用**;切换后重启 account loop。readonly 切换等于绕过沙箱,直接拒绝。 |
119
+ | `/workspace setup` / `/workspace tidy` | Triggers the `workspace` skill — setup for new workspaces (creates INDEX.md / INSTRUCTIONS.md / memory), tidy for cleanup of existing ones |
120
+ | `/help` | List available commands |
121
+
122
+ Unknown `/` input falls through to normal message handling.
123
+
124
+ ## Event coalescing (WeixinResponder)
125
+
126
+ WeChat `sendMessage` is block-send, while LLMs stream. The current strategy is "buffer the whole turn, flush as one message":
127
+ - `stream` events accumulate in a single buffer
128
+ - `complete` → flush the whole buffer as one (or more) messages. The responder flushes on **any** `complete` and does not read its `batchBoundary` flag — so when the root drains a multi-message queue across several merged turns (see [session.md](session.md#message-queue-and-drain)), each round's batch-boundary `complete` ships that round as its own message instead of buffering every round into one blob that lands only at the terminal `complete` (the "8 reports in one lump" bug)
129
+ - Hard cap `HARD_CHARS = 3500` (WeChat rejects >~4000) — when the buffer reaches it mid-stream, split at the nearest paragraph break and dispatch; remaining text keeps accumulating
130
+ - `error` → immediate flush + send a `[错误] …` message; `system` → immediate flush + send a `[系统] …` message
131
+ - Tool calls / tool results / thinking are dropped (detail lives in the web UI)
132
+ - Media: the agent emits `MEDIA: <path>` markers, which the responder extracts and turns into actual media uploads
133
+
134
+ ## Supported inbound media
135
+
136
+ Plain text + images (base64 attached to the agent message), voice, video, files. For voice/video/files, the content is decrypted (AES from CDN), saved under the workspace, and referenced in the agent's input text.
137
+
138
+ ## Scope and out-of-scope
139
+
140
+ Supported: text, images, voice, video, files; slash commands listed above; per-session access level.
141
+
142
+ Not supported: group chats, typing indicators.
143
+
144
+ ## References
145
+
146
+ Original rollout plan: [plans/weixin-channel.md](../plans/weixin-channel.md).