weclaude
v0.3.3
Published
Bridge Claude Code and WeCom: PreToolUse approval cards + bidirectional tmux session mirror with image paste
Maintainers
Readme
weclaude
把 Claude Code 装进企业微信。 在地铁上、被窝里、开会摸鱼时,照样能跟你电脑上的 Claude 干活。

- 🛎 远程审批 — Claude 要跑
Bash/Edit?审批卡片直推 IM,点 ✅/❌/⏱(放行 N 分钟)。 - 📋 计划审批 — Claude 在 plan mode 结束(
ExitPlanMode)时,把计划摘要 + 审批卡推到 IM:点 ✅同意 让它退出 plan mode 开始执行,或 ✏️继续改 让它留在 plan mode 继续完善。同AskUserQuestion多选题也镜像为投票卡。 - 🪞 会话镜像 — 你电脑上跑的 Claude 流式打字、tool_use、思考过程,实时同步到企业微信;IM 里发消息原样落进 CLI 输入框。
- 🖼 图片直贴 — 企业微信发图,自动走 macOS 剪贴板 + tmux 粘贴,Claude 当贴图处理(不走 Read,不耗 token)。
- 🔍 细节页 — 工具调用 / 审批请求都生成本地 HTML 详情页,IM 里点链接看完整 input / result / git diff。
- 📡 MCP 主动推送 — Claude 通过
wecom__send_markdown/wecom__send_card/wecom__ask_user主动汇报或问询。 - 📄 文档读写 — Claude 通过
wecom_doc_list_tools/wecom_doc_call直接调企业微信智能机器人的 doc / smartsheet / smartpage MCP,新建在线文档、写 Markdown、读链接、操作智能表格——全程在内网,不需要 corp access_token。 - 🗂 多会话发现/切换 — Claude 通过
list_claude_sessions/switch_claude_session/new_claude_session列出本机 tmux 内所有在跑的 Claude 会话(带摘要 + 稳定动物 emoji 标签)、把 IM 镜像切到其中任一个、或在指定路径新开一个会话。审批卡标题也带同一枚 emoji,多个会话兜底到同一 IM 时一眼区分。 - 🔄 重启即续 — 电脑重启 / tmux 全没了 / daemon 崩了都不掉档:IM ↔ 会话绑定持久化在
~/.weclaude/mirror-attachments.json,下一条 IM 消息自动claude --resume拉起新 tmux pane,历史完整继承;tmux attach -t weclaude接管即可。
快速开始
前置:macOS / Linux、Node ≥ 20、PATH 里能找到 claude 或 claude-internal、企业微信「智能机器人」的 botId + secret。镜像模式额外需要 tmux。
npm install -g weclaude
weclaude initinit 会交互式问你 4 个问题,把配置落到 ~/.weclaude/:
| 问什么 | 落到哪 |
| --- | --- |
| botId / secret | ~/.weclaude/secrets.json |
| 用哪个 Claude(claude / claude-internal / 自定义路径) | ~/.weclaude/config.jsonc |
| 用哪种模式(headless / mirror,见下文) | ~/.weclaude/config.jsonc |
| 是否开启 PreToolUse 远程审批 | ~/.weclaude/config.jsonc |
然后自动:编译 → 注入 hook/MCP → 装常驻 daemon(macOS launchd / Linux systemd --user)→ 等 WebSocket 鉴权。
最后一步:绑定默认会话。 CLI 提示后,在企业微信里给机器人发:
将本对话设置为默认会话这是唯一绕过白名单的入口,10 分钟窗口,消费完立刻关。后续所有消息都按白名单鉴权。
其它安装方式 / 内网与无 systemd 环境
从某个 fork / 分支装(自带改动的版本):dist/ 不入库,但 prepare 脚本会在 git 安装时自动编译,所以可以直接:
npm i -g github:<你的用户名>/weclaude # 默认分支
npm i -g github:<你的用户名>/weclaude#<分支名> # 指定分支内网 / 需要正向代理:weclaude 的出站(WeCom WebSocket、智能机器人文档 MCP)会读环境变量里的代理。安装与运行都带上 HTTPS_PROXY:
HTTPS_PROXY=http://your-proxy:port npm i -g github:<你的用户名>/weclaude
# 也可在 config.jsonc 写死: bot.proxy = "http://your-proxy:port"(优先级高于环境变量)daemon 进程同样需要能读到代理变量(见下方守护脚本里 export HTTPS_PROXY)。
无 systemd 的环境(容器等):init 装 daemon 这步在 macOS 走 launchd、Linux 走 systemd --user。若机器没有 systemd user session(很多容器:PID 1 非 systemd、无 XDG_RUNTIME_DIR),这步会失败——其余配置(hook / MCP / 插件)已生效,只差 daemon 没被托管。用一个简单的重启循环守护即可,存成 ~/.weclaude/daemonctl.sh:
#!/usr/bin/env bash
# 无 systemd 时的 weclaude daemon 守护:setsid 脱离终端的重启循环。
set -uo pipefail
REPO="$(npm root -g)/weclaude"
NODE="$(command -v node)"
WC="$HOME/.weclaude"; PIDFILE="$WC/supervisor.pid"; LOG="$WC/daemon.log"
mkdir -p "$WC"
# 内网:daemon 出站要走代理。按需改成你的代理地址,或删掉这两行。
export HTTPS_PROXY="${HTTPS_PROXY:-http://your-proxy:port}"; export https_proxy="$HTTPS_PROXY"
export NO_PROXY="${NO_PROXY:-127.0.0.1,localhost}"; export no_proxy="$NO_PROXY"
is_running(){ [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; }
case "${1:-start}" in
start) is_running && { echo "already running"; exit 0; }
setsid bash "$0" __loop </dev/null >>"$LOG" 2>&1 & echo $! >"$PIDFILE"
sleep 2; echo "[daemonctl] started (pid $(cat "$PIDFILE"))" ;;
__loop) while [[ -f "$PIDFILE" ]]; do "$NODE" "$REPO/dist/daemon/index.js" >>"$LOG" 2>&1
[[ -f "$PIDFILE" ]] || break; sleep 5; done ;;
stop) [[ -f "$PIDFILE" ]] && { pid=$(cat "$PIDFILE"); rm -f "$PIDFILE"
pkill -P "$pid" 2>/dev/null||true; kill "$pid" 2>/dev/null||true; }
pkill -f "$REPO/dist/daemon/index.js" 2>/dev/null||true; echo "[daemonctl] stopped" ;;
restart) bash "$0" stop; sleep 1; bash "$0" start ;;
status) is_running && echo "running (pid $(cat "$PIDFILE"))" || echo "stopped"
curl -sS -m2 http://127.0.0.1:17890/status 2>/dev/null && echo || echo "(HTTP :17890 无响应)" ;;
esacchmod +x ~/.weclaude/daemonctl.sh
~/.weclaude/daemonctl.sh start # 起 daemon(自带崩溃重启)
~/.weclaude/daemonctl.sh status # 看状态容器重启不会自动拉起 daemon——把 ~/.weclaude/daemonctl.sh start 加进 shell profile 或容器入口即可(幂等,已在跑就 no-op)。
两种模式怎么选
| | headless | mirror 🌟 |
| --- | --- | --- |
| 怎么跑 | IM 来消息 → 后台 claude -p 跑一轮 | IM 来消息 → tmux 粘进活的 TUI |
| CLI 看得见吗 | 看不见(headless 子进程) | 看得见(IM 消息像你自己敲进去的) |
| 状态续接 | 靠 --resume <sid> 续 session | 一对一绑定 IM 聊天 ↔ tmux 窗口,原地累计 |
| 流式输出 | 转发 assistant 文本块 | 完整流式:打字机、tool_use、思考过程都看得到 |
| 适合谁 | 简单单轮问答 / CI 类自动化 | 真·远程结对编程 |
镜像模式下,IM 里发 /new 直接开新 tmux 窗口 + 新 Claude 会话;/clear 清当前上下文;带图消息自动注入剪贴板。所有 IM 聊天共享一个 tmux session(默认名 weclaude),每个聊天一个独立 window,关 tmux / daemon 崩了 / 整机重启都能自愈:IM↔会话绑定 write-through 落到 ~/.weclaude/mirror-attachments.json,daemon 起来就 eager restore;重启后 pane 全死,下一条 IM 消息触发 claude --resume <sid> 拉起新 pane,--resume fork 出的新 jsonl 由 watcher 从 EOF 无缝接管(不会把整段历史再推一遍到 IM)。中途在别处 /clear 把 jsonl rotate 掉也不丢绑定,会自愈到同项目目录下最新的 jsonl。
💡 mirror 不要求你必须先在 CLI 里开 tmux:在企业微信里直接发
/new就能从零起一个新 tmux 窗口 + Claude 会话;甚至首次发任意消息都会自动 spawn + 绑定(首条消息既是绑定信号也是第一句 prompt)。回家打开终端tmux attach -t weclaude接管即可。
体验是什么样
审批场景:
你正在地铁上,电脑上的 Claude 想
rm -rf node_modules重装。企业微信叮一声弹卡片:🛎 授权请求: Bash
rm -rf node_modules[✅ 允许] [❌ 拒绝] [⏱ 5 分钟内自动允许]你点 ✅,卡片立刻刷新成
✅ Bash · 已允许,电脑上的 Claude 解除阻塞继续跑。
镜像场景:
你 tmux 里开着 Claude 在写代码。出门后给机器人发:
把刚才那个函数改成异步的
这条消息自动粘进 CLI 输入框 + 回车提交。Claude 的回应、调用了哪些工具、改了哪些文件,逐字流式推回你 IM。回家打开终端,对话一字不少都在那里。
文档场景:
你给 Claude 说:"周报给我整理成一篇企业微信文档"。Claude 自己调
wecom_doc_list_tools看可用方法,再调wecom_doc_call走create_doc新建文档、edit_doc_content写入 Markdown,最后把链接贴回会话——全程不离开 Claude,文档归属到你的 userid,每日 20 篇限额按 userid 计。
文档 / 智能表格 / 智能文档
weclaude 把企业微信智能机器人的远端 MCP(doc / smartsheet / contact 等)桥接到本地 Claude,不走 corp access_token:daemon 直接复用 botId+secret 的 WS 长连,通过 aibot_get_mcp_config 命令拉到每个 category 的 Streamable HTTP MCP URL,再以 JSON-RPC 调 tools/list / tools/call,x-openclaw-wecom-userid header 透传文档归属人。
前置一次性授权:在企业微信「工作台 - 智能机器人 - 你的机器人 - 可使用权限」里勾选「文档」「智能表格」对应能力。第一次调用如果未授权,远端会返回结构化错误(errcode 851013)+ 授权链接,原样转给 Claude,照着点同意即可。
Claude 侧自动发现:模型先调一次 wecom_doc_list_tools(category="doc") 拿到工具列表(create_doc / edit_doc_content / smartpage_create / upload_doc_image ...)和入参 schema,再用 wecom_doc_call(category, method, args) 执行。category 当前可选:
| category | 能干嘛 |
| --- | --- |
| doc | 在线文档、智能文档、文档图片上传 |
| smartsheet | 智能表格的字段 / 记录读写 |
| contact | 通讯录查询(按需授权) |
curl 验证:
# 列 doc 类目下所有可用工具
curl -sS -X POST http://127.0.0.1:17890/wedoc/list \
-H 'content-type: application/json' \
-d '{"category":"doc"}'
# 创建一篇文档
curl -sS -X POST http://127.0.0.1:17890/wedoc/call \
-H 'content-type: application/json' \
-d '{"category":"doc","method":"create_doc","args":{"doc_type":3,"doc_name":"demo"}}'
# 写入 Markdown
curl -sS -X POST http://127.0.0.1:17890/wedoc/call \
-H 'content-type: application/json' \
-d '{"category":"doc","method":"edit_doc_content",
"args":{"docid":"<返回的 docid>","content_type":1,"content":"# 标题\n正文"}}'
# 授权变更后清缓存
curl -sS -X POST http://127.0.0.1:17890/wedoc/invalidate -H 'content-type: application/json' -d '{}'requesterUserId 解析顺序:调用方显式传入 → defaultChat 的 user:<id> 部分 → 不传(远端会拒,错误原样回到 Claude,比 daemon 提前判更透明)。
事件订阅 / 定时广播
一个轻量 pub/sub:任意群或单聊都能订阅一个 topic(自定义事件名),任何授权用户都能广播;daemon 内置分钟级调度器,每天定点自动推送。订阅关系与定时任务持久化到 ~/.weclaude/config.jsonc 的 topics 段,weclaude reload 后自动恢复。
IM 命令(在已授权的会话里说,中英文均可):
订阅 sync-daily # 当前会话订阅 topic(群里说→群订阅,单聊里说→个人订阅)
退订 sync-daily
广播 sync-daily 内容:xxx # 立即广播给所有订阅者
每天 08:00 广播 sync-daily 内容:xxx # 定时(也接受「每天8点」「每天8:30」「每日 08:00」)
广播列表 / 订阅列表 # 查看当前状态
取消广播 sync-daily # 删掉该 topic 的所有定时典型用法:在群 A 说 订阅 sync-daily,在群 B 说 每天8点广播 sync-daily 内容:早会 10 分钟后开始——第二天早 8 点群 A 自动收到。
外部触发(CI / 监控 / 脚本):
curl -sS -X POST http://127.0.0.1:17890/publish \
-H 'content-type: application/json' \
-d '{"topic":"ci-fail","markdown":"🔴 build #1234 failed on main"}'调用方只关心事件名,运维通过 IM 命令改订阅者,代码零改动。
消息是怎么同步的
审批流(headless / mirror 都走这条):
sequenceDiagram
autonumber
participant CC as Claude Code
participant Hook as PreToolUse hook
participant D as weclaude daemon
participant WS as WeCom WS
participant U as 你 (IM)
CC->>Hook: 触发 Bash "rm -rf foo"
Hook->>D: POST /approval (长轮询 ≤30min)
D->>WS: sendCard(按钮卡片)
WS->>U: 推送审批卡片
Note over D,U: daemon 在内存里 park 这个 HTTP 请求<br/>等 click 事件 / 自动窗口 / 超时三选一
U->>WS: 点 ✅
WS->>D: template_card_event
D->>WS: updateTemplateCard("已允许")
D-->>Hook: 200 {decision: allow}
Hook-->>CC: stdout 决策 JSON
CC->>CC: 放行 Bash镜像流(mirror 模式,IM ↔ tmux 双向):
sequenceDiagram
autonumber
participant U as 你 (IM)
participant WS as WeCom WS
participant D as weclaude daemon
participant T as tmux + claude
participant J as session.jsonl
U->>WS: "改成异步的"
WS->>D: message.text
D->>D: allowFrom 鉴权
D->>T: load-buffer + paste-buffer + Enter
T->>J: assistant 流式追加
par tail
D->>J: fs.watch 增量读
J-->>D: 新 assistant / tool_use 块
D->>WS: replyStream(流式片段)
WS->>U: 推送流式回复
and 工具拦截
T->>D: PreToolUse hook (走上面的审批流)
end要点:
- daemon 是唯一和 WeCom 说话的进程,hook 和 MCP 都只是它的 HTTP 客户端
- 审批是长轮询 + 内存 park,没有 webhook、不依赖外网回调
- 镜像是单向 paste + 单向 tail:IM→tmux 走粘贴键,tmux→IM 走读 jsonl,两条管道独立、互不阻塞
一个聊天里跑多个会话(#tag 路由)
同一个 WeCom 聊天里可以同时挂多个并行 Claude session,靠消息里的 #tag 前缀路由。不带 tag 就是默认 session,与旧行为一致。
创建 & 切换
/new #docs 新开一个标签为 docs 的会话(tmux 窗口名也叫 docs)
/new #api 再开一个,与 #docs 完全独立(独立 sessionId / jsonl / cwd)
/new 默认 session,老玩法消息路由
只要消息文本里任意位置带 #tag(空白/句首/句尾分隔),就路由到那个 tagged session:
#docs 帮我把 README 的目录补一下
帮我看下这个报错 #api
/pwd #docs → 只看 docs 会话的项目路径
/stop #api → 只打断 api 会话
/clear #docs → 只清 docs 会话上下文不带 tag 的消息始终落到默认 session。
回复标识
tagged session 的每条回复自带 emoji #tag 前缀(emoji 由 tag 名 hash 决定,稳定),一眼分辨来自哪个 session:
🦊 `#docs`
(这里是 docs 会话的 Claude 回复……)默认 session 无前缀,视觉上保持简洁。
tag 语法:[\p{L}\p{N}_-]{1,32},支持中英文数字与 _、-;一条消息里只识别第一个 #tag,后续的 #foo 原样透传给 Claude(不会误伤代码里的 #include 或 issue 引用)。
cwd 是聊天级的,不是 session 级:同一聊天里所有 tagged / 默认 session 共用一个 cwd。/new #foo 会在当前聊天绑定的 cwd 下起 pane;任意 session 里让 AI 调 cd MCP,写入的 pendingCwd 也挂在聊天层,下次 /new(无论带不带 tag)都会读到。这样多 session 天然对齐到同一个项目根,切换 tag 时不用重新指路径。
多 CLI 后端(claude / claude-internal / codebuddy)
daemon 同时挂载所有已安装的 CLI,不是二选一:你可以一个 tmux 窗口跑 claude、另一个跑 codebuddy,各自绑不同的 IM 聊天。会话身份就是它的 jsonl 路径,daemon 由路径反推是哪个 CLI 写的,--resume 用哪个二进制、jsonl 用哪套 schema 解析、project-dir 怎么编码,全部由此派生。
/new 沿用「当前会话」的 CLI 新开
/new codebuddy 换到 codebuddy 新开
/new claude-internal 换到 claude-internal 新开默认后端由 wrc.defaultCli 决定(缺省 claude),二进制路径可用 wrc.cliBackends.<name>.bin 覆盖。
和 #tag 完全正交,两者可以任意组合、顺序不限:
/new codebuddy #docs 用 codebuddy 起一个 docs 标签会话
/new #docs codebuddy 等价写法
#docs 帮我改 README → 路由到那个 codebuddy 会话
/clear #docs → 只清它,且仍留在 codebuddy 上切换 CLI 后 tag 路由的所有行为都保持不变:
/clear #tagrotate 出的新 jsonl 仍落在该 CLI 的 projects 目录,watcher 按该后端的 dialect 迁移绑定;- pane 挂了自愈
--resume用的是该会话所属的二进制,不会串到defaultCli; - 首次
/new #tag没有自己的历史记录时,继承本聊天基础会话的 CLI(与 cwd 的聊天级继承规则一致),不会悄悄退回默认后端; /sessions列表在混用多个 CLI 时,每行自动标注(codebuddy)之类的来源。
常用命令
IM 里发 /help 可随时拉出完整命令表;每次 /new、/clear 之后,回执会随机附一条功能提示,用来慢慢摊开命令面。
/new · /clear · /stop · /n 会话控制
/sessions [emoji|id] 列出 / 切换 live 会话
/new <cli> [#tag] 切换 CLI 后端 / 开并行会话
/id · /pwd · /usage · /cost · /audit 信息查询(免授权)
/help 全部命令本机 shell:
weclaude status # 看 daemon + WS 健康
weclaude logs -f # 实时日志
weclaude send <chat> <text> # 主动推消息
weclaude reload # 重启 daemon(改了配置后用)
weclaude unsync # 卸载 hook/MCP(保留 daemon)
weclaude uninstall # 完整卸载(先于 npm uninstall)⚠️ 卸载顺序:先
weclaude uninstall再npm uninstall -g weclaude。否则 launchd/systemd 会一直尝试拉起已删除的二进制。~/.weclaude/下的 config/secrets 不会被清,二次安装可无缝复用。
常见问题
Q: hook 不触发?
cat ~/.claude/settings.json | jq .hooks.PreToolUse,没东西就跑 weclaude sync 重写。
Q: 卡片点了没反应? 企业微信卡片就地刷新只有 5 秒窗口,超时不刷新是正常的,决策本身仍然生效。
Q: daemon 起不来?
weclaude logs -f 看;常见是 botId / secret 写错卡在 WebSocket 鉴权。
Q: 多机部署?
config.jsonc 可以纳入 dotfiles;secrets.json 每台机器独立填。第二台机器跑 weclaude init 会跳过覆盖提示,但仍要重新走 claim 步骤拿本机 IM principal。
架构一瞥
三个进程走 127.0.0.1:17890 + ~/.weclaude/:
WeCom ── WebSocket ──► daemon (常驻, launchd/systemd)
│
├── HTTP :17890 ◄── hook (pre-tool-use.sh, 长轮询)
├── HTTP :17890 ◄── MCP server (claude 的 stdio 子进程)
└── spawn `claude -p` (headless) 或 tail jsonl + tmux paste (mirror)daemon 是唯一持有 WeCom WS 连接的进程,hook 和 MCP 都是它的薄 HTTP 客户端。
详情看 CLAUDE.md。
License
MIT
