读写分离 · 一对多 · 安全闸Read/write split · one-to-many · human gate

在钉钉里发一句话,
就能指挥本机的 AI 编码会话
Command your local AI coding sessions
with one message from DingTalk

监控并控制本机上运行的多个 Claude Code / qodercli 会话。一个轻量「信使 Agent」理解你的意图、决定读还是写、定位到哪个会话;变更操作必须你确认后才执行。配套 Web 控制台。 Monitor and control many Claude Code / qodercli sessions on your machine. A lightweight “messenger agent” reads your intent, decides read vs. write, and targets the right session — every mutation waits for your confirmation. Ships with a web console.

npm install -g @tengxiaohtx/agent-connect
核心理念Core idea

三层,各司其职Three layers, one job each

🧭

信使 = manager 路由器Messenger = manager router

维护「当前会话」指针(像 cwd)。只做意图识别+路由:切换/列出/只读咨询/转发/接管/退出。咨询走 fork 只读副本问 worker 本身,绝不臆测代答。任意 OpenAI/Anthropic 兼容模型。 Keeps a “current session” pointer (like cwd). Pure intent-routing: switch / list / read-only consult / forward / takeover / exit. Consults fork a read-only copy and ask the worker itself — never guesses. Any OpenAI/Anthropic-compatible model.

路由,不干活routes, doesn't do the work
📖

读写双平面Read / write planes

读(list/read)只读落盘的 sessions 注册表与 transcript,零副作用、不碰 worker 进程——手机上狂刷也不污染上下文。写(send/takeover/run)经 tmux。 Reads only touch on-disk session registries and transcripts — zero side effects, never the worker process. Writes (send/takeover/run) go through tmux.

零污染zero pollution
🔒

安全闸Safety gate

任何变更 worker 的操作走「提议 → 人工确认 → 执行」状态机,带白名单、命令前缀与确认超时。信使永远无法擅自动手。 Every mutation runs a propose → confirm → execute state machine, with sender allowlist, command prefix, and TTL. The messenger can never act on its own.

人工确认human-in-the-loop
架构Architecture

消息怎么流动How a message flows

继续用 cc-connect 做钉钉 ↔ 本地传输。因 cc-connect 只能通过 acp 类型接入自定义程序,信使以一个 ACP 薄桥作为它的 agent,转发给 agent-connect serve 守护。 cc-connect stays the DingTalk ⇄ local transport. Since cc-connect only accepts custom programs via its acp agent type, the messenger plugs in as a thin ACP bridge that forwards to the agent-connect serve daemon.

钉钉DingTalk ─Stream─► cc-connect ─exec─► agent-connect acp ─HTTP─► agent-connect serve
                                                              │
   ┌──────────────────────────────────────────────────────┐ │
   │  Web 控制台console + SSE + 配置页config    闸门gate (allow/prefix)│◄┘
   │        │  共享 conductor / pending / 信使上下文shared conductor / pending / ctx │
   │  AgentConductor  ( propose → confirm → execute )     │
   │  Messenger Agent  ( AI SDK, OpenAI-compatible )        │
   │        │  read tools  /  propose tools                    │
   │  ControlPlane: listSessions·getMessages · send·takeover·run │
   └──┬─────────────────┬──────────────────┬───────────────┘
   registry.js       transcript.js        tmux.js
为什么这样设计Why this design

读写分离,是自然语言下的守门人The split is the gatekeeper for natural language

如果让被绑定的 worker 自己理解钉钉消息,一句「那任务咋样了」(读)就会进入它的工作上下文——读污染了写。独立信使 + 读写分离 + 人工确认,既保留「发一句话就行」的体验,又守住安全与上下文纯净。 If the bound worker interpreted your chat itself, a “how’s that task going?” (a read) would land in its working context — reads polluting writes. A separate messenger + plane split + human confirm keeps the “just say it” UX while protecting safety and context.

🟢 读平面(带外)Read plane (out-of-band)

直接读 ~/.claude/sessions/*.json*.jsonl transcript,完全不碰 agent 进程。工具:list_sessions / read_session / get_statusReads ~/.claude/sessions/*.json and *.jsonl transcripts directly, never the process. Tools: list_sessions / read_session / get_status.

🟠 写平面(带内)Write plane (in-band)

经 tmux 注入/接管/新建,且只暂存为提议。工具:propose_send / propose_takeover / propose_run,确认后才落地。Injects / takes over / spawns via tmux, staged as proposals only. Tools: propose_send / propose_takeover / propose_run, applied after confirm.

工作流Workflow

一次请求怎么流动How a request flows

信使维护「当前会话」(cwd)。咨询走只读 fork、需要改动才建议接管、执行前人工确认;回复分清「信使」与「worker」两层。 The messenger keeps a “current session” (cwd). Consults are read-only forks; edits prompt a takeover; writes need confirmation; replies separate messenger vs. worker.

You ──"这个 bug 怎么改?how to fix this bug?"──►  Messengerconsult (read-only)
                              ▼
                     fork plan/只读 ──►  Worker  (原会话不动)(original untouched)
                              ◄── 用自身上下文作答answers from its context
   Messenger ──►  You:  > 🔁 来自from <worker>(只读read-only): …  + 建议接管suggest takeover
   You ──"接管take over"──► propose_takeover ──确认/confirm──► kill+resume 进 tmuxkill+resume in tmux
   You ──"按方案改apply the fix"──► propose_forward ──确认/confirm──► Worker 执行executes
🔎 只读咨询策略:小会话 full-fork(`--resume --fork-session`,全量上下文,最准);大会话 bounded-excerpt——给全新 agent 喂从最近一次压缩摘要开始的节选(避免重放超大历史,14MB 会话 90s+超时 → 约 12s,回复标注有损)。只读按 agent 保证:Claude=`--permission-mode plan`,qoder=`--tools ""`(比权限模式更强;`--yolo` 只用于控制、绝不用于咨询)。 🔎 Consult strategy: small sessions use full-fork (`--resume --fork-session`, full context, accurate); large sessions use bounded-excerpt — a fresh agent is fed an excerpt starting from the latest compaction summary (avoids replaying a huge transcript; a 14 MB session went 90 s+ timeout → ~12 s, answer labeled lossy). Read-only per agent: Claude `--permission-mode plan`, qoder `--tools ""` (stronger than a permission mode; `--yolo` is only for control, never consult).
🧵 引用回复:每条回复前都引用触发它的指令(`> 🗨️ 你:列出会话`),多指令在途也不混线;钉钉经 cc-connect 无原生引用回复,故在内容层实现,跨平台一致。同一会话按串行处理,杜绝共享上下文竞态。开关 `im.platforms.<平台>.quote_reply`(默认开)。 🧵 Quoted replies: every reply is prefixed with the command that triggered it (`> 🗨️ 你:list sessions`), so overlapping commands never cross wires. DingTalk has no native quote-reply via cc-connect, so it's done in the reply content — consistent across platforms. Turns are serialized per conversation to avoid shared-context races. Toggle `im.platforms.<name>.quote_reply` (on by default).
🌐 回复语言与自动翻译:信使回复语言可配(默认中文,`messenger.reply_language` 或 设置→LLM Provider→回复语言)。worker 回复若为其他语种,信使翻译一次并把「🌐 信使译文」附在原文之后,原文译文都给你看;同语种自动跳过、失败静默降级。支持 zh/en/ja/ko/fr/es/de/ru/pt/it。 🌐 Reply language & auto-translation: the messenger's reply language is configurable (default Chinese; `messenger.reply_language` or Settings→LLM Provider). If a worker replies in another language, the messenger translates once and appends a «🌐 信使译文» block after the original — you see both. Same-language is skipped; failures degrade silently. Supports zh/en/ja/ko/fr/es/de/ru/pt/it.
你 ▸ 切到 c233caaf
🧭 信使 ▸ 📍 已切到 agentmon(c233caaf)
你 ▸ 它最近完成了什么?            # 咨询 → 只读 fork
🧭 信使 ▸ > 🔁 来自 agentmon·claude(只读):发布了 v0.6.0……
你 ▸ 帮我把版本号改成 0.6.1        # 需要改动 → 建议接管
🧭 信使 ▸ 这需要编辑,建议先接管。要我提议接管吗?You ▸ switch to c233caaf
🧭 Messenger ▸ 📍 current = agentmon (c233caaf)
You ▸ what did it finish recently?      # consult → read-only fork
🧭 Messenger ▸ > 🔁 from agentmon·claude (read-only): shipped v0.6.0…
You ▸ bump the version to 0.6.1         # needs an edit → suggest takeover
🧭 Messenger ▸ that needs editing — take it over first? (propose?)
任意 IMAny IM

不止钉钉 —— 桥接任意聊天平台Not just DingTalk — bridge any chat platform

通信层用 cc-connect,它支持多种 IM;信使以平台无关的 ACP agent 接入,从 CC_SESSION_KEY 解析平台名并套用对应闸门。信使、读写平面、确认闸在所有平台完全一致,只有传输层不同。 Transport is cc-connect, which supports many IMs; the messenger plugs in as a platform-agnostic ACP agent, reads the platform from CC_SESSION_KEY, and applies its gate. Messenger, planes, and the confirm gate are identical everywhere — only the transport differs.

钉钉 DingTalk飞书 FeishuTelegram SlackDiscord企业微信 WeCom QQLINEWeixin

接入新 IM:① 在配置里配好该平台的 cc-connect 段(凭证等,见 cc-connect 文档);② 可选加一段闸门 [im.platforms.<名>](前缀 / 白名单 / 确认词),不配则用默认(启用、空白名单=允许所有)。 Add an IM: ① configure that platform's cc-connect block (credentials, per cc-connect docs); ② optionally add a gate [im.platforms.<name>] (prefix / allowlist / confirm words) — omit for defaults (enabled, empty allowlist = allow all).

🖌️ 流式 AI 卡片(钉钉,可选):在钉钉开放平台建「AI 卡片模板」,把 templateId 填到 card_template_id(或 Web 设置页),回复即打字机式流式呈现;不填则回退普通消息,功能不受影响。
📇 访问控制:发送者不在白名单时会收到明确的「无权限」提示,而非静默。
🖌️ Streaming AI card (DingTalk, optional): create an AI card template on the DingTalk Open Platform and set its card_template_id (or via the web settings) for typewriter-style streaming; otherwise replies fall back to normal messages.
📇 Access control: senders not in the allowlist get an explicit “not authorized” reply, not silence.
快速开始Quick start

四步跑起来Up and running in four steps

一键安装(装好 CLI + cc-connect,检查 Node/tmux 并 init): One-click install (CLI + cc-connect, checks Node/tmux, runs init):

curl -fsSL https://raw.githubusercontent.com/xinyuehtx/agent-connect/main/scripts/install.sh | bash

或用 npm:npm install -g @tengxiaohtx/agent-connect;或到 Releases 下载 .tgz 离线安装。装好后: Or via npm: npm install -g @tengxiaohtx/agent-connect; or grab a .tgz from Releases. Then:

1

初始化Init

生成默认配置Create default config
agent-connect init

2

起控制台Serve

仅本机 · 无需登录Localhost-only, no login
agent-connect serve

3

配置Configure

Web 设置页填 LLM 与钉钉Set LLM + DingTalk in the UI

4

连钉钉Connect

另开终端拉起 cc-connectStart cc-connect
agent-connect start

或全用 CLI 配置(等价于 Web 配置页): Or configure entirely via CLI (equivalent to the settings page):

# 信使 LLM(任意 OpenAI 兼容端点)messenger LLM (any OpenAI-compatible endpoint)
agent-connect config set messenger.base_url "https://your-gateway/v1"
agent-connect config set messenger.api_key  "sk-..."
agent-connect config set messenger.model    "gpt-4o-mini"

# 钉钉凭证(Stream 模式)DingTalk credentials (Stream mode)
agent-connect config set projects.0.platforms.0.options.client_id     "..."
agent-connect config set projects.0.platforms.0.options.client_secret "..."
💡 钉钉里默认要带前缀 /ai 才路由给信使(如 /ai 列出会话);待确认时直接回「确认 / 取消」。写平面需要 tmuxbrew install tmux),读平面不需要。 💡 In DingTalk, messages need the /ai prefix to reach the messenger (e.g. /ai list sessions); reply “确认/取消” (yes/no) to a pending action. The write plane needs tmux (brew install tmux); reads don’t.
CLI

命令参考Command reference

命令Command说明What it does
agent-connect init初始化配置目录与默认配置Create config dir + default config
agent-connect serve启动 Web 控制台 + 信使守护(读写平面 + 安全闸)Start web console + messenger daemon (planes + gate)
agent-connect acpACP 薄桥,供 cc-connect 拉起(勿手动运行)ACP bridge for cc-connect (not run by hand)
agent-connect start启动 cc-connect(钉钉 ↔ 本地)Start cc-connect (DingTalk ⇄ local)
agent-connect config set <k> <v>修改配置(支持点号路径)Set a config value (dot path)
agent-connect agent list列出运行中的 agent 会话List running agent sessions
agent-connect agent read <id>只读查看状态与最新回复Read status + latest reply (read-only)
agent-connect agent send <id> "…"向 tmux 会话注入指令Inject into a tmux session
agent-connect agent takeover <id>接管非 tmux 会话Adopt a non-tmux session
agent-connect agent run "…"在 tmux 中新建可远控会话Spawn a new remote-controllable session