@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,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).
|