@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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/admin-out/404.html +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_buildManifest.js +1 -0
- package/admin-out/_next/static/IBP_mwBDf-cPwZSZZGpi8/_ssgManifest.js +1 -0
- package/admin-out/_next/static/chunks/1d2d5650-810a3ca1ae20cab5.js +18 -0
- package/admin-out/_next/static/chunks/200.3b8f8a74f005a134.js +1 -0
- package/admin-out/_next/static/chunks/220.3dc41cd6269b88e6.js +1 -0
- package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +1 -0
- package/admin-out/_next/static/chunks/304eda7d.b1ee7f523653e027.js +1 -0
- package/admin-out/_next/static/chunks/330.3b784cf0dcd69163.js +1 -0
- package/admin-out/_next/static/chunks/442.f4302087bddb1ec1.js +1 -0
- package/admin-out/_next/static/chunks/443.d1b289781793f8de.js +25 -0
- package/admin-out/_next/static/chunks/503-7c6d9935df6e0cea.js +1 -0
- package/admin-out/_next/static/chunks/533.5f2d4bfe3d076095.js +1 -0
- package/admin-out/_next/static/chunks/579.a2718ddd395a2f3a.js +1 -0
- package/admin-out/_next/static/chunks/635-3dd0043ed0520b9d.js +1 -0
- package/admin-out/_next/static/chunks/65.859665ba6717967a.js +1 -0
- package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +1 -0
- package/admin-out/_next/static/chunks/688.4e014ac6cfc7e152.js +1 -0
- package/admin-out/_next/static/chunks/868.0c95dfe283400a00.js +1 -0
- package/admin-out/_next/static/chunks/app/_not-found/page-8e35399c8fd5016d.js +1 -0
- package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +1 -0
- package/admin-out/_next/static/chunks/db534642.09d3c650ff587175.js +1 -0
- package/admin-out/_next/static/chunks/fb4ec4d6-3fa44f69484f91b9.js +1 -0
- package/admin-out/_next/static/chunks/framework-c3031ae54a63fe16.js +1 -0
- package/admin-out/_next/static/chunks/main-14b48e3c2588354f.js +1 -0
- package/admin-out/_next/static/chunks/main-app-483b436873410ddc.js +1 -0
- package/admin-out/_next/static/chunks/pages/_app-b2fc6e5f920d1b1a.js +1 -0
- package/admin-out/_next/static/chunks/pages/_error-f3696a8f3d67d150.js +1 -0
- package/admin-out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/admin-out/_next/static/chunks/webpack-8bd7ef88a6527feb.js +1 -0
- package/admin-out/_next/static/css/659eccb5db697b76.css +1 -0
- package/admin-out/_next/static/css/8fce4e1a54dac12a.css +3 -0
- package/admin-out/_next/static/media/19cfc7226ec3afaa-s.woff2 +0 -0
- package/admin-out/_next/static/media/21350d82a1f187e9-s.woff2 +0 -0
- package/admin-out/_next/static/media/8e9860b6e62d6359-s.woff2 +0 -0
- package/admin-out/_next/static/media/ba9851c3c22cd980-s.woff2 +0 -0
- package/admin-out/_next/static/media/c5fe6dc8356a8c31-s.woff2 +0 -0
- package/admin-out/_next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/admin-out/_next/static/media/e4af272ccee01ff0-s.p.woff2 +0 -0
- package/admin-out/favicon.ico +0 -0
- package/admin-out/icon.svg +18 -0
- package/admin-out/index.html +1 -0
- package/admin-out/index.txt +23 -0
- package/admin-out/monaco/vs/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/admin-out/monaco/vs/abap-D-t0cyap.js +1 -0
- package/admin-out/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/admin-out/monaco/vs/assets/css.worker-HnVq6Ewq.js +93 -0
- package/admin-out/monaco/vs/assets/editor.worker-Be8ye1pW.js +26 -0
- package/admin-out/monaco/vs/assets/html.worker-B51mlPHg.js +470 -0
- package/admin-out/monaco/vs/assets/json.worker-DKiEKt88.js +58 -0
- package/admin-out/monaco/vs/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/admin-out/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/admin-out/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/bat-C397hTD6.js +1 -0
- package/admin-out/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/admin-out/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/admin-out/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/admin-out/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/admin-out/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/admin-out/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/admin-out/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/admin-out/monaco/vs/css-CaeNmE3S.js +3 -0
- package/admin-out/monaco/vs/cssMode-CjiAH6dQ.js +1 -0
- package/admin-out/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/admin-out/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/admin-out/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/admin-out/monaco/vs/ecl-30fUercY.js +1 -0
- package/admin-out/monaco/vs/editor/editor.main.css +1 -0
- package/admin-out/monaco/vs/editor/editor.main.js +5 -0
- package/admin-out/monaco/vs/editor.api-CalNCsUg.js +903 -0
- package/admin-out/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/admin-out/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/admin-out/monaco/vs/freemarker2-Cz_sV6Md.js +3 -0
- package/admin-out/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/admin-out/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/admin-out/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/admin-out/monaco/vs/handlebars-OwglfO-1.js +1 -0
- package/admin-out/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/admin-out/monaco/vs/html-Pa1xEWsY.js +1 -0
- package/admin-out/monaco/vs/htmlMode-Bz67EXwp.js +1 -0
- package/admin-out/monaco/vs/ini-CsNwO04R.js +1 -0
- package/admin-out/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/admin-out/monaco/vs/javascript-PczUCGdz.js +1 -0
- package/admin-out/monaco/vs/jsonMode-DULH5oaX.js +7 -0
- package/admin-out/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/admin-out/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/admin-out/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/admin-out/monaco/vs/less-C0eDYdqa.js +2 -0
- package/admin-out/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/admin-out/monaco/vs/liquid-DqKjdPGy.js +1 -0
- package/admin-out/monaco/vs/loader.js +1368 -0
- package/admin-out/monaco/vs/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/admin-out/monaco/vs/lua-DtygF91M.js +1 -0
- package/admin-out/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/admin-out/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/admin-out/monaco/vs/mdx-DEWtB1K5.js +1 -0
- package/admin-out/monaco/vs/mips-CiYP61RB.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-D2OdxNBt.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-DO3azKX8.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-EcChJV6a.js +1 -0
- package/admin-out/monaco/vs/monaco.contribution-qLAYrEOP.js +1 -0
- package/admin-out/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/admin-out/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/admin-out/monaco/vs/nls.messages-loader.js +1 -0
- package/admin-out/monaco/vs/nls.messages.cs.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.de.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.es.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.fr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.it.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.ja.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.js.js +10 -0
- package/admin-out/monaco/vs/nls.messages.ko.js.js +25 -0
- package/admin-out/monaco/vs/nls.messages.pl.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.pt-br.js.js +6 -0
- package/admin-out/monaco/vs/nls.messages.ru.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.tr.js.js +15 -0
- package/admin-out/monaco/vs/nls.messages.zh-cn.js.js +17 -0
- package/admin-out/monaco/vs/nls.messages.zh-tw.js.js +15 -0
- package/admin-out/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/admin-out/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/admin-out/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/admin-out/monaco/vs/perl-DABw_TcH.js +1 -0
- package/admin-out/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/admin-out/monaco/vs/php-D_kh-9LK.js +1 -0
- package/admin-out/monaco/vs/pla-VfZjczW0.js +1 -0
- package/admin-out/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/admin-out/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/admin-out/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/admin-out/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/admin-out/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/admin-out/monaco/vs/python-Cr0UkIbn.js +1 -0
- package/admin-out/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/admin-out/monaco/vs/r-f8dDdrp4.js +1 -0
- package/admin-out/monaco/vs/razor-BYAHOTkz.js +1 -0
- package/admin-out/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/admin-out/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/admin-out/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/admin-out/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/admin-out/monaco/vs/rust-Bfetafyc.js +1 -0
- package/admin-out/monaco/vs/sb-3GYllVck.js +1 -0
- package/admin-out/monaco/vs/scala-foMgrKo1.js +1 -0
- package/admin-out/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/admin-out/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/admin-out/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/admin-out/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/admin-out/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/admin-out/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/admin-out/monaco/vs/sql-32GpJSV2.js +1 -0
- package/admin-out/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/admin-out/monaco/vs/swift-n-2HociN.js +3 -0
- package/admin-out/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/admin-out/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/admin-out/monaco/vs/tsMode-CZz1Umrk.js +11 -0
- package/admin-out/monaco/vs/twig-C6taOxMV.js +1 -0
- package/admin-out/monaco/vs/typescript-DfOrAzoV.js +1 -0
- package/admin-out/monaco/vs/typespec-D-PIh9Xw.js +1 -0
- package/admin-out/monaco/vs/vb-Dyb2648j.js +1 -0
- package/admin-out/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/admin-out/monaco/vs/workers-DcJshg-q.js +1 -0
- package/admin-out/monaco/vs/xml-CdsdnY8S.js +1 -0
- package/admin-out/monaco/vs/yaml-DYGvmE88.js +1 -0
- package/bin/halo.js +9 -0
- package/bundled-docs/design/agent.md +145 -0
- package/bundled-docs/design/architecture.md +270 -0
- package/bundled-docs/design/background-dispatch.md +151 -0
- package/bundled-docs/design/command.md +58 -0
- package/bundled-docs/design/cron.md +88 -0
- package/bundled-docs/design/evolution.md +383 -0
- package/bundled-docs/design/express-self.md +145 -0
- package/bundled-docs/design/feishu.md +224 -0
- package/bundled-docs/design/halo-city.md +112 -0
- package/bundled-docs/design/prompt-system.md +411 -0
- package/bundled-docs/design/session.md +321 -0
- package/bundled-docs/design/slack.md +212 -0
- package/bundled-docs/design/storage.md +578 -0
- package/bundled-docs/design/telegram.md +161 -0
- package/bundled-docs/design/web.md +222 -0
- package/bundled-docs/design/wechat.md +146 -0
- package/bundled-docs/design/ws.md +144 -0
- package/bundled-docs/dev/acp-adapter.md +337 -0
- package/bundled-docs/dev/add-channel.md +376 -0
- package/bundled-docs/dev/add-model-provider.md +386 -0
- package/bundled-docs/dev/api.md +686 -0
- package/bundled-docs/dev/deploy.md +198 -0
- package/bundled-docs/dev/desktop-packaging.md +327 -0
- package/bundled-docs/dev/env.md +158 -0
- package/bundled-docs/dev/logging.md +85 -0
- package/bundled-docs/dev/previews.md +225 -0
- package/bundled-docs/dev/tools.md +331 -0
- package/bundled-docs/guide/README.md +29 -0
- package/bundled-docs/guide/agents.md +264 -0
- package/bundled-docs/guide/channels/README.md +62 -0
- package/bundled-docs/guide/channels/acp.md +150 -0
- package/bundled-docs/guide/channels/feishu.md +154 -0
- package/bundled-docs/guide/channels/slack.md +156 -0
- package/bundled-docs/guide/channels/telegram.md +112 -0
- package/bundled-docs/guide/channels/web.md +149 -0
- package/bundled-docs/guide/channels/wechat.md +110 -0
- package/bundled-docs/guide/chat.md +112 -0
- package/bundled-docs/guide/cli.md +197 -0
- package/bundled-docs/guide/getting-started.md +93 -0
- package/bundled-docs/guide/secrets-and-credentials.md +161 -0
- package/bundled-docs/guide/sessions.md +104 -0
- package/bundled-docs/guide/skills.md +302 -0
- package/bundled-docs/guide/testing-agents-and-skills.md +136 -0
- package/bundled-docs/guide/workspace.md +131 -0
- package/bundled-docs/plans/cron-tasks.md +362 -0
- package/bundled-docs/plans/design-suggestions.md +203 -0
- package/bundled-docs/plans/issue-session-deep-nesting.md +208 -0
- package/bundled-docs/plans/launch-plan.md +111 -0
- package/bundled-docs/plans/md-and-agent-refactor.md +197 -0
- package/bundled-docs/plans/remote-mac-worker.md +196 -0
- package/bundled-docs/plans/roadmap.md +40 -0
- package/bundled-docs/plans/self-evolution.md +1379 -0
- package/bundled-docs/plans/todo.md +32 -0
- package/bundled-docs/plans/weixin-channel.md +255 -0
- package/bundled-docs/requirements/agent.md +92 -0
- package/bundled-docs/requirements/chat.md +81 -0
- package/bundled-docs/requirements/command.md +130 -0
- package/bundled-docs/requirements/editor.md +82 -0
- package/bundled-docs/requirements/explorer.md +89 -0
- package/bundled-docs/requirements/overview.md +62 -0
- package/bundled-docs/requirements/session.md +73 -0
- package/bundled-docs/requirements/settings.md +224 -0
- package/bundled-docs/requirements/skill.md +125 -0
- package/bundled-docs/requirements/source-control.md +82 -0
- package/bundled-docs/requirements/terminal.md +118 -0
- package/bundled-docs/requirements/workspace.md +80 -0
- package/bundled-docs/test/docs-seeding.md +101 -0
- package/bundled-docs/test/session.md +374 -0
- package/dist/index.js +26750 -0
- package/package.json +67 -0
- package/templates/INSTRUCTIONS.md +25 -0
- package/templates/agents/__apply_agent__/AGENT.md +229 -0
- package/templates/agents/__apply_agent__/agent.yaml +30 -0
- package/templates/agents/__evo_agent__/AGENT.md +323 -0
- package/templates/agents/__evo_agent__/agent.yaml +29 -0
- package/templates/agents/__score__/AGENT.md +177 -0
- package/templates/agents/__score__/agent.yaml +29 -0
- package/templates/agents/deep-executor/AGENT.md +60 -0
- package/templates/agents/deep-executor/agent.yaml +25 -0
- package/templates/agents/default/AGENT.md +38 -0
- package/templates/agents/default/agent.yaml +51 -0
- package/templates/agents/executor/AGENT.md +38 -0
- package/templates/agents/executor/agent.yaml +25 -0
- package/templates/aliases.yaml +26 -0
- package/templates/canvas/self.html +660 -0
- package/templates/config.yaml +69 -0
- package/templates/models/anthropic.yaml +47 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +156 -0
- package/templates/models/aws-bedrock-mantle.yaml +89 -0
- package/templates/models/deepseek.yaml +62 -0
- package/templates/models/doubao.yaml +137 -0
- package/templates/models/hunyuan.yaml +56 -0
- package/templates/models/kimi.yaml +41 -0
- package/templates/models/mimo-token-plan-china.yaml +61 -0
- package/templates/models/minimax.yaml +80 -0
- package/templates/models/openai.yaml +51 -0
- package/templates/models/qwen.yaml +78 -0
- package/templates/optional-skills/tavily-web-search/SKILL.md +91 -0
- package/templates/optional-skills/tavily-web-search/config.yaml +10 -0
- package/templates/prompts/all/TOOL_GUIDELINES.md +35 -0
- package/templates/prompts/all/TOOL_SHELL.md +4 -0
- package/templates/prompts/all/TOOL_SHELL.windows.md +7 -0
- package/templates/prompts/bootstrap/BOOTSTRAP.md +26 -0
- package/templates/prompts/root/WORKSPACE_MEMORY.md +11 -0
- package/templates/schema.sql +30 -0
- package/templates/skills/acp/SKILL.md +236 -0
- package/templates/skills/acp/templates/SKILL.md.tmpl +74 -0
- package/templates/skills/acp/templates/ask.py +176 -0
- package/templates/skills/acp/templates/config.yaml.tmpl +33 -0
- package/templates/skills/agent/SKILL.md +128 -0
- package/templates/skills/aws-knowledge/SKILL.md +82 -0
- package/templates/skills/aws-knowledge/scripts/aws_kb.py +61 -0
- package/templates/skills/cron/SKILL.md +217 -0
- package/templates/skills/cron/manage-cron.py +434 -0
- package/templates/skills/halo/SKILL.md +98 -0
- package/templates/skills/nova-web-search/SKILL.md +65 -0
- package/templates/skills/self/SKILL.md +139 -0
- package/templates/skills/send-file/SKILL.md +28 -0
- package/templates/skills/send-file/feishu.md +16 -0
- package/templates/skills/send-file/slack.md +7 -0
- package/templates/skills/send-file/telegram.md +9 -0
- package/templates/skills/send-file/web.md +6 -0
- package/templates/skills/send-file/wechat.md +8 -0
- package/templates/skills/skill/SKILL.md +131 -0
- package/templates/skills/workspace/SKILL.md +28 -0
- package/templates/skills/workspace/setup-tidy.md +265 -0
- package/templates/skills/workspace/share.md +187 -0
- 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)
|