@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,376 @@
1
+ # Adding a Channel
2
+
3
+ How to wire a new IM / chat platform (Slack, Discord, Telegram, Lark, …) into Halo, so users can talk to agents from that platform the same way they do from WeChat or the web.
4
+
5
+ ## Reality check
6
+
7
+ Channels are **descriptor-based** but not plugins from a marketplace — you still hand-write the platform integration. The wiring itself is one entry per side: a `ServerChannelDescriptor` (server) and an `AdminChannelDescriptor` (admin UI). Once you've written the per-channel modules, exposing them to the rest of Halo is two new lines in two `descriptors.ts` files. `index.ts`, the cron core, the channels sidebar, the channels main pane — none of those change.
8
+
9
+ What you get from the platform infrastructure:
10
+ - `SessionManagerRegistry` — multi-subscriber; web and your channel can listen to the same session at once
11
+ - Per-session `accessLevel` enforcement (full / workspace / readonly) — enforced by SessionManager, not your channel
12
+ - Media inbound handling pattern — `<ws>/.halo/assets/<channel>/inbound/<accountId>/<date>/` emits `[图片已保存: /path]` markers with UI support
13
+ - Unified channel accounts DB — `~/.halo/secrets/channels/channels.db`, one table for all channels
14
+ - Shared accounts DAL — `channels/shared/accounts.ts` handles CRUD, your channel writes a thin adapter
15
+ - Shared command dispatcher — `channels/shared/commands.ts` handles all common slash commands
16
+ - Channel registry — `channels/registry.ts` (server) + `features/channels/registry.ts` (admin); descriptor-based so adding a channel is 2 entries, not edits to 5+ files
17
+ - Cron dispatcher registry — `cron/dispatcher.ts`; per-channel send + listTargets are owned by each channel module
18
+ - Agent event stream in a standard shape (see [design/ws.md](../design/ws.md))
19
+
20
+ What you write yourself:
21
+ - Inbound ingestion (webhook / long-poll / socket mode)
22
+ - Outbound send client
23
+ - Account management (QR / OAuth / bot token) + persistence
24
+ - Stream → platform-message coalescing
25
+ - Two descriptors (~10 lines each: server + admin)
26
+
27
+ ---
28
+
29
+ ## Reference implementation
30
+
31
+ Whole directory: [packages/server/src/channels/wechat/](../../../packages/server/src/channels/wechat/) (~1900 lines across 9 files).
32
+
33
+ The pattern that works:
34
+
35
+ ```
36
+ channels/<name>/
37
+ ├── types.ts — platform-specific message shapes
38
+ ├── api.ts — HTTP client (send / receive / auth)
39
+ ├── accounts.ts — DAL: persisted account rows
40
+ ├── handler.ts — main loop + dispatch → SessionManager
41
+ ├── event-adapter.ts — agent events → coalesced outbound messages
42
+ ├── login.ts — QR / OAuth flow (if applicable)
43
+ ├── cdn.ts — media download / decrypt (if applicable)
44
+ ├── send-media.ts — outbound media upload (if applicable)
45
+ └── media-store.ts — disk save + marker generation (shared helper)
46
+ ```
47
+
48
+ Not every channel needs every file. Slack probably doesn't need `cdn.ts`; Telegram might not need `login.ts` if bot tokens are manually provisioned.
49
+
50
+ ---
51
+
52
+ ## What a channel does, step by step
53
+
54
+ Every channel, on every inbound message, runs this sequence:
55
+
56
+ 1. **Receive.** Webhook endpoint or long-poll loop gets a raw platform message.
57
+ 2. **Extract.** Pull user ID, workspace binding, text, media. Platform-specific — this is why each channel has its own `types.ts`.
58
+ 3. **Resolve workspace.** Look up the bound workspace for this account / user. For Weixin this is a DB column; for Slack you'd use team → workspace mapping.
59
+ 4. **Get a SessionManager.** `registry.getOrCreate(workspacePath)` — cheap, memoized per workspace.
60
+ 5. **Resolve or create a session.** One user often has multiple sessions over time; keep a "currently active" override per user (Weixin uses an in-memory `activeOverrides` map updated by `/session new` / `/session switch`).
61
+ 6. **Register an event listener once per session.** `sm.registerEventListener(sessionId, handler)` — handler receives every agent event. Coalesce streamed text into whole outbound messages (platforms hate per-token streams).
62
+ 7. **Send the user message.** `sm.sendUserMessage(sessionId, text, images?)`. This returns immediately; the agent runs async and emits events to your listener.
63
+
64
+ Source for the canonical flow: [packages/server/src/channels/wechat/handler.ts](../../../packages/server/src/channels/wechat/handler.ts) (`handleInbound`).
65
+
66
+ ---
67
+
68
+ ## Steps to add a Slack channel (worked example)
69
+
70
+ ### 1. Create the directory
71
+
72
+ ```
73
+ packages/server/src/channels/slack/
74
+ ├── types.ts
75
+ ├── api.ts
76
+ ├── accounts.ts
77
+ ├── handler.ts
78
+ └── event-adapter.ts
79
+ ```
80
+
81
+ Copy the Weixin shapes to get started, then rewrite against the Slack API.
82
+
83
+ ### 2. Define the account adapter
84
+
85
+ All channel accounts live in a single unified table: `~/.halo/secrets/channels/channels.db` → `channel_accounts`. Common fields (accountId, workspacePath, label, enabled, accessLevel, language, timestamps) are explicit columns. Channel-specific fields go in the `config` JSON column.
86
+
87
+ **You do NOT create a new DB file.** Instead:
88
+
89
+ 1. Define your channel's TypeScript interface in `types.ts` (e.g. `SlackAccount` with `teamId`, `botToken`, `botUserId`)
90
+ 2. Create `accounts.ts` as a thin adapter over `channels/shared/accounts.ts`:
91
+ - Map `ChannelAccount.config` JSON → your typed interface
92
+ - Pass `channelType = 'slack'` to all shared DAL calls
93
+
94
+ Reference adapters: [channels/telegram/accounts.ts](../../../packages/server/src/channels/telegram/accounts.ts), [channels/web/accounts.ts](../../../packages/server/src/channels/web/accounts.ts).
95
+
96
+ See [design/storage.md](../design/storage.md#channel_accounts) for the full schema.
97
+
98
+ ### 3. Wire the API client
99
+
100
+ `api.ts`: `sendMessage`, `postEphemeral`, maybe `uploadFile`. Use the Slack Web API (Node SDK or raw fetch). Auth: `Bearer ${bot_token}`.
101
+
102
+ Inbound: Slack pushes events via **HTTPS webhooks** (Events API) or **WebSocket** (Socket Mode). Pick one — Socket Mode avoids exposing a public URL but requires Slack app tier. For a self-hosted Halo, Socket Mode is usually easier.
103
+
104
+ ### 4. Write the main handler
105
+
106
+ `handler.ts`: exports `startSlackChannel(deps: { registry, db })` returning `{ startAccount, stopAccount, stopAll }`. Weixin shape at [packages/server/src/channels/wechat/handler.ts:66-135](../../../packages/server/src/channels/wechat/handler.ts#L66-L135).
107
+
108
+ For webhook-style channels (Slack Events API), you don't need a `runAccountLoop` — instead expose an HTTP route that validates the signature, deserializes the event, and calls `handleInbound`. For Socket Mode, you **do** have a loop (the WebSocket reconnect loop).
109
+
110
+ Inside `handleInbound`:
111
+
112
+ ```ts
113
+ async function handleInbound({ registry, db, account, event, listeners }) {
114
+ const sm = registry.getOrCreate(account.workspacePath)
115
+ const sessionId = resolveActiveSession(account, event.user, sm)
116
+ ?? await sm.createSession('default', null, `Slack: ${event.user}`, undefined, undefined, null, account.accessLevel)
117
+
118
+ if (!listeners.has(sessionId)) {
119
+ const responder = new SlackResponder({
120
+ sendText: (text) => sendMessage({ token: account.botToken, channel: event.channel, text }),
121
+ })
122
+ const unsubscribe = sm.registerEventListener(sessionId, (event, state, turnId) => responder.handle(event, state, turnId))
123
+ listeners.set(sessionId, unsubscribe)
124
+ }
125
+
126
+ // Prefix is optional but nice — lets the agent address the user by handle
127
+ const agentInput = `[channel: slack | user: ${event.user}]\n${event.text}`
128
+ await sm.sendUserMessage(sessionId, agentInput, event.images)
129
+ }
130
+ ```
131
+
132
+ ### 5. Write the event adapter
133
+
134
+ `event-adapter.ts`: buffers streamed text and sends whole messages. Weixin's flushes on either a 3500-char hard ceiling (platform limit) or a `complete` event; see [packages/server/src/channels/wechat/event-adapter.ts](../../../packages/server/src/channels/wechat/event-adapter.ts). Slack has a 40k char limit but users dislike huge messages — consider splitting at paragraph boundaries.
135
+
136
+ **What to forward, what to drop**:
137
+ - Drop `tool_call` / `tool_result` / `thinking` events — they're chatter the user doesn't want in IM
138
+ - Drop events where `event.taskId` is set (those are sub-agent events; the root agent's text is enough)
139
+ - Forward `stream` text (coalesced), `error` (immediately, with a `[error]` prefix), `complete` (flush remaining buffer)
140
+ - Forward `system` if the platform can render it (e.g. Slack ephemeral messages)
141
+
142
+ ### 6. Surface channel context to skills
143
+
144
+ Inside `handler.ts`, when you build a `CommandContext` for `dispatchCommand`, fill in the structured `channel` field. This is what lets skills like `cron` default their behaviour to the originating chat (e.g. "remind me at 3pm" creates a cron whose target is *this* slack DM, not a fan-out to every workspace member):
145
+
146
+ ```ts
147
+ const ctx: CommandContext = {
148
+ sm, userId: event.user, sessionPrefix, accessLevel: account.accessLevel,
149
+ channelLabel: `Slack: ${event.user}`, activeOverrides,
150
+ workspacePath: account.workspacePath, lang: getLang(account),
151
+ channel: {
152
+ type: 'slack',
153
+ accountId: account.accountId,
154
+ chatId: event.channel, // Slack channel id; used as cron recipient pin
155
+ },
156
+ }
157
+ ```
158
+
159
+ Skill bodies see these as `{{channel.type}}`, `{{channel.account_id}}`, `{{channel.chat_id}}` placeholders (rendered by `prompts/md-vars.ts`). Admin/WS skill invocations leave them empty — skills should write their bodies to handle both forms.
160
+
161
+ ### 6.5. Cron dispatcher (proactive sends)
162
+
163
+ If you want users to be able to schedule cron jobs that deliver to your channel, write a per-channel cron-dispatcher and register it at boot. The cron core (`cron/dispatcher.ts`) is registry-based — there is no central switch on channel type, and you do **not** edit anything in `cron/` to add a channel.
164
+
165
+ Create `packages/server/src/channels/<name>/cron-dispatcher.ts`:
166
+
167
+ ```ts
168
+ import { registerCronDispatcher, type CronTargetOption, type DispatchResult } from '../../cron/dispatcher.js'
169
+ import { getChannelDb } from '../../db/channel-db.js'
170
+ import { listAccounts, getAccount } from './accounts.js'
171
+
172
+ async function dispatch(accountId: string, text: string, chatId?: string): Promise<DispatchResult[]> {
173
+ const acct = getAccount(getChannelDb(), accountId)
174
+ if (!acct || !acct.enabled) throw new Error(`<name> account ${accountId} unavailable`)
175
+ // Pick the recipient: explicit chatId (cron created from inside a chat) →
176
+ // your channel's "default proactive target" (e.g. account owner) → cached
177
+ // lastActiveChatId fallback.
178
+ const target = chatId || acct.defaultRecipient || readLastActiveChatId(accountId)
179
+ if (!target) throw new Error('no <name> recipient — bind first / send the bot a message')
180
+ await sendToUser({ account: acct, toUserId: target, text })
181
+ return [{ channelType: '<name>', accountId, chatId: target, ok: true }]
182
+ }
183
+
184
+ function listTargets(): CronTargetOption[] {
185
+ return listAccounts(getChannelDb()).map((a) => ({
186
+ channelType: '<name>',
187
+ accountId: a.accountId,
188
+ label: a.label || a.accountId,
189
+ workspacePath: a.workspacePath,
190
+ enabled: a.enabled === 1,
191
+ hasActiveChat: !!a.defaultRecipient, // your "ready to deliver" rule
192
+ }))
193
+ }
194
+
195
+ export function register<Name>CronDispatcher(): void {
196
+ registerCronDispatcher({ channelType: '<name>', dispatch, listTargets })
197
+ }
198
+ ```
199
+
200
+ Two things to decide:
201
+
202
+ - **Single-recipient vs fan-out.** WeChat is single-recipient (one `dispatch` call → one `DispatchResult`); Telegram fans out to every numeric id in `allowedUsers` and returns one result row per recipient. Pick whichever matches your platform's identity model. Fan-out is what makes "schedule a digest to the whole team" work without one cron per user.
203
+ - **`hasActiveChat` rule.** What makes an account "ready to receive" without an explicit chat id? Telegram = numeric id whitelisted OR cached lastActiveChatId; WeChat = QR-bind owner OR cached. The admin UI's "no active chat" warning reads this flag.
204
+
205
+ ### 7. Expose admin routes
206
+
207
+ `packages/server/src/routes/slack.ts`:
208
+
209
+ ```ts
210
+ export function createSlackRoutes(deps: { db, channel }) {
211
+ const app = new Hono()
212
+ app.get('/slack/accounts', (c) => c.json({ accounts: listAccounts(deps.db) }))
213
+ app.post('/slack/accounts', async (c) => { /* create from OAuth token */ })
214
+ app.patch('/slack/accounts/:id', async (c) => { /* label/workspacePath/enabled/accessLevel */ })
215
+ app.delete('/slack/accounts/:id', async (c) => { deps.channel.stopAccount(id); deleteAccount(deps.db, id) })
216
+ // Optional: POST /slack/webhook for Events API, validates signature + routes to handleInbound
217
+ return app
218
+ }
219
+ ```
220
+
221
+ Pattern: [packages/server/src/routes/weixin.ts](../../../packages/server/src/routes/weixin.ts).
222
+
223
+ ### 8. Write the server descriptor + register
224
+
225
+ Create `packages/server/src/channels/slack/descriptor.ts`:
226
+
227
+ ```ts
228
+ import type { ServerChannelDescriptor } from '../registry.js'
229
+ import { startSlackChannel, type SlackChannel } from './handler.js'
230
+ import { createSlackRoutes } from '../../routes/slack.js'
231
+ import { registerSlackCronDispatcher } from './cron-dispatcher.js' // step 6.5
232
+
233
+ export const slackDescriptor: ServerChannelDescriptor<SlackChannel> = {
234
+ channelType: 'slack',
235
+ start: (deps) => startSlackChannel(deps),
236
+ routes: (deps) => createSlackRoutes(deps),
237
+ shutdown: (channel) => channel.stopAll(),
238
+ registerCronDispatcher: () => registerSlackCronDispatcher(), // omit if no step 6.5
239
+ }
240
+ ```
241
+
242
+ Then add to [packages/server/src/channels/descriptors.ts](../../../packages/server/src/channels/descriptors.ts):
243
+
244
+ ```ts
245
+ import { slackDescriptor } from './slack/descriptor.js'
246
+
247
+ export const defaultChannelDescriptors = [
248
+ wechatDescriptor,
249
+ telegramDescriptor,
250
+ webDescriptor,
251
+ slackDescriptor, // ← one new line
252
+ ]
253
+ ```
254
+
255
+ That's all the server wiring. `index.ts` calls `bootChannels(app, defaultChannelDescriptors, …)` which iterates the list — start, routes, cron-dispatcher all hook in via the descriptor. `gracefulShutdown` calls `shutdownChannels()` which drains every descriptor's `shutdown` in reverse order. **Nothing in `index.ts` itself changes when you add a channel.**
256
+
257
+ ### 9. Optional: admin UI
258
+
259
+ To let users manage Slack accounts from the web UI:
260
+
261
+ 1. Build the React component (mirror [packages/admin/src/features/weixin/weixin-settings.tsx](../../../packages/admin/src/features/weixin/weixin-settings.tsx)). Place it at `packages/admin/src/features/slack/slack-settings.tsx`.
262
+ 2. Create `packages/admin/src/features/slack/descriptor.ts`:
263
+ ```ts
264
+ import { Hash } from 'lucide-react' // or whatever icon fits
265
+ import type { AdminChannelDescriptor } from '@/features/channels/registry'
266
+ import { SlackSettings } from './slack-settings'
267
+
268
+ export const slackAdminDescriptor: AdminChannelDescriptor = {
269
+ id: 'slack',
270
+ label: 'Slack', // brand name; or 'channels.slack' i18n key
271
+ Icon: Hash,
272
+ Component: SlackSettings,
273
+ }
274
+ ```
275
+ 3. Add to [packages/admin/src/features/channels/descriptors.ts](../../../packages/admin/src/features/channels/descriptors.ts):
276
+ ```ts
277
+ import { slackAdminDescriptor } from '@/features/slack/descriptor'
278
+
279
+ export const defaultAdminChannelDescriptors = [
280
+ weixinAdminDescriptor,
281
+ telegramAdminDescriptor,
282
+ webAdminDescriptor,
283
+ slackAdminDescriptor, // ← one new line
284
+ ]
285
+ ```
286
+
287
+ The sidebar maps over the list to render itself; the main pane looks up by `id` to render `Component`. `channel-store` validates the `id` against the same list — no edits to `channels-sidebar.tsx`, `channels-main.tsx`, or `channel-store.ts`.
288
+
289
+ If you skip this step, users provision Slack accounts via curl-ing the REST routes from step 7. Headless deployments often skip it.
290
+
291
+ ### 10. Document
292
+
293
+ Write `.halo/docs/design/slack.md` mirroring [design/wechat.md](../design/wechat.md): architecture, data model, main loop (or webhook flow), slash commands you support, access level.
294
+
295
+ Add a row to [dev/api.md](api.md)'s endpoint table.
296
+
297
+ ---
298
+
299
+ ## Common decisions
300
+
301
+ ### Access level
302
+
303
+ Default to `readonly` for any channel that doesn't have a strong identity verification story. Channel-exposed agents with full filesystem and `shell_exec` are a loaded gun.
304
+
305
+ Per-account `access_level` column in your DAL, inherited into session creation:
306
+
307
+ ```ts
308
+ await sm.createSession('default', null, title, undefined, undefined, null, account.accessLevel)
309
+ ```
310
+
311
+ SessionManager enforces the rest: readonly sessions drop `file_write` / `file_edit` / `shell_exec` / `web_fetch` from the tool set automatically. Source: [dev/tools.md#access-level-per-session](tools.md#access-level-per-session).
312
+
313
+ ### Slash commands
314
+
315
+ You almost always want:
316
+ - `/session new` — start a fresh session, old sessions remain reachable
317
+ - `/session list` / `/session switch <n>` — session history, switch active
318
+ - `/workspace info` / `/workspace switch <path>` — show / switch workspace
319
+ - `/help`
320
+
321
+ Common commands (`/help`, `/evo`, and the object commands `/session`, `/agent`, `/skill`, `/workspace` with their builtin verbs — plus fall-through to same-named skills for skill verbs like `/cron …`) are handled by `dispatchCommand()` in [channels/shared/commands.ts](../../../packages/server/src/channels/shared/commands.ts). Your channel handler only needs to:
322
+
323
+ 1. Build a `CommandContext` (including `lang` from the account's `language` field)
324
+ 2. Call `dispatchCommand(ctx, command, arg, { channelName: 'slack' })`
325
+ 3. Handle the result: check `result.workspace` for `/workspace switch` side-effects (update account + restart), check `result.switchTo` for session switches, check `result.startedTurn` (a skill verb kicked the agent — keep the event stream open), then send `result.text` back to the user
326
+
327
+ Channel-specific commands (e.g. WeChat's `/qr`) go in a fallback switch after `dispatchCommand` returns `null`. All system messages are i18n'd via `channels/shared/i18n.ts`. Reference: [channels/telegram/handler.ts](../../../packages/server/src/channels/telegram/handler.ts) (cleanest example — uses a loop over command names).
328
+
329
+ ### Compact / busy states
330
+
331
+ If the session is currently compacting when an inbound arrives, reply with a hint ("integrating context, try again") and don't queue. If the session is busy (agent running), queue with a "last message still processing" hint. Weixin pattern at [packages/server/src/channels/wechat/handler.ts](../../../packages/server/src/channels/wechat/handler.ts):
332
+
333
+ ```ts
334
+ if (sm.isSessionCompacting(sid)) { /* reply + return, don't enqueue */ }
335
+ if (sm.isSessionRunning(sid)) { /* reply + enqueue */ }
336
+ sm.sendUserMessage(sid, text, images)
337
+ ```
338
+
339
+ ### Single-instance lock
340
+
341
+ If your channel uses a polling or socket-mode loop, **respect the server's single-instance lock** (`~/.halo/global/server.lock`). Orphan processes each run an independent loop and fan out duplicate messages to their own session copies. See [design/wechat.md#singleton-note](../design/wechat.md) and [dev/deploy.md](deploy.md).
342
+
343
+ ### Workspace resolution
344
+
345
+ Each channel account has a `workspace_path` column in the unified `channel_accounts` table. If your channel binds user-to-workspace instead of bot-to-workspace, do the lookup at inbound time.
346
+
347
+ At handler startup, `resolveAccountWorkspace(account)` checks the path exists on disk and calls `ensureWorkspaceHalo()`. If the workspace is missing, the handler skips the account. Users can re-bind via the admin panel or `/workspace switch` command (full access only).
348
+
349
+ ### Media
350
+
351
+ Use the shared helper `saveInboundMedia({ workspacePath, accountId, channel: 'slack', buffer, kind: 'image', mimeType })` — [packages/server/src/channels/wechat/media-store.ts](../../../packages/server/src/channels/wechat/media-store.ts). It saves under `<ws>/.halo/assets/slack/inbound/<accountId>/<date>/` and returns the path. Append `[图片已保存: /abs/path]` to the agent's input text and the existing UI code will render a thumbnail.
352
+
353
+ For images going to the LLM, also pass them as base64 in the `images` arg — `sm.sendUserMessage(sid, text, images)`.
354
+
355
+ ---
356
+
357
+ ## Testing
358
+
359
+ No automated test framework for channels — manual for now:
360
+
361
+ 1. Create a test bot on the platform (development workspace / test server)
362
+ 2. Bind it to a throwaway Halo workspace: `POST /api/slack/accounts`
363
+ 3. Send a message → verify it lands in `<ws>/.halo/sessions/default/`
364
+ 4. Verify agent reply comes back as one or more outbound messages
365
+ 5. Test access level: set the account to `readonly`, send "delete all files in /tmp" → should be refused by the tool guard
366
+ 6. Test `/session new`, `/session switch`, busy/compact states, large responses (>3500 chars)
367
+
368
+ ---
369
+
370
+ ## References
371
+
372
+ - Full Weixin reference: [design/wechat.md](../design/wechat.md)
373
+ - Weixin code: [packages/server/src/channels/wechat/](../../../packages/server/src/channels/wechat/)
374
+ - Architecture seat: [design/architecture.md](../design/architecture.md)
375
+ - Session registry: [packages/server/src/agents/session-manager-registry.ts](../../../packages/server/src/agents/session-manager-registry.ts)
376
+ - Storage conventions: [design/storage.md](../design/storage.md)