用免费模型、几分钟、零成本,在钉钉群里上线一个能对话、看图、读文件的数字员工。
下载 opencode + 装 dws + 钉钉扫码授权 —— 三步就能让机器人上线。跑在 opencode 的免费模型上(文本对话),图片识别可选免费或 gemini(质量更好)。起步成本为 0。
当前版本见 VERSION。
自己从零搭一个"群消息监听 → LLM 生成回复 → 发回群"的数字员工,你要处理进程守护、断线重连、图片/文件多模态、会话注入、测试隔离一堆脏活。这个 harness 把这些生产环境打磨过的坑全封装好了,你只填几个配置就能上线,想定制业务再写自己的能力插件。
- 🆓 零成本起步:默认跑 opencode 免费模型(文本
deepseek-v4-flash-free),图片识别可选免费mimo-v2.5-free或质量更好的 gemini(需配置 proxy)。 - ⚡ 几分钟上线:装两个工具 + 钉钉扫码,填一个群 ID 就能收发消息(群 / 单聊 / @我 三种订阅任选)。
- 🧩 开箱即用的 7 个能力:文本对话、Question 交互、群消息聚合、图片识别、文件解读、合并转发、已读+状态回执 —— 都是可开关的插件。
- 🛡️ 生产级守护:launchd / systemd 托管,崩溃自愈、健康检查、熔断、
/reboot远程重启。 - 🔧 可定制、可 merge、可换平台:core/custom 分层,你只改 custom;生成/发送走 core 协议 + 注册点,换 IM 平台只替换 custom 的发送实现。
-
🤖 对 Coding Agent 友好的 Harness 工程,功能开发 100% AI Coding 代码库刻意做成"给 AI 写代码"友好的形态:core/custom 物理分层、能力插件契约清晰、每个能力自带单测、边界写进 AGENTS.md。本项目的功能全部由 AI 编码完成 —— 人给方向和验收,AI 探查、实现、真实链路验证、提 PR。你要加能力,也可以直接把需求丢给 Coding Agent,它照着现有插件范式就能写。
-
🧬 能力按需交付,背后是 opencode 的生态 数字员工的"推理 + 任务执行"这件重活,交给更完备的 opencode(它有模型生态、工具、会话、权限一整套)。本项目只做"人机协同"那一层的最佳实践 —— 钉钉侧的收发、富媒体受控处理(图片识别 / 文件解读 / 合并转发)、Question 人在回路作答、群消息聚合、进程守护自愈。分工清晰:opencode 负责"想和做",本 harness 负责"人怎么跟它协同"。能力可组装、可选配,按业务需要一个个交付。
每个能力是一个插件,用 CAP_<NAME>_ENABLED 开关,可组装、可选配。core 自带的是平台无关的通用原语(src/core/builtin_caps/),custom 的是钉钉强耦合、供 FDE 定制(src/custom/capabilities/):
| 能力 | 做什么 | 归属 | 默认 |
|---|---|---|---|
| 文本对话 | 群/单聊发消息,数字员工用 LLM 回复 | core | 开 |
| Question 交互 | agent 反问时,你在群里回复序号/选项作答 | core | 开 |
| 群消息聚合 | 短时多条消息合并成一次摘要回复,不逐条打扰 | core | 关 |
| 图片识别 | 发图片 → 免费多模态模型识别内容 → 基于内容回应 | custom | 开 |
| 文件解读 | 发文档(txt/md/csv/json/代码…)→ 受控下载读正文 → 解读 | custom | 开 |
| 合并转发 | 转发一段聊天记录 → 反查逐条解析(含图/文件)→ 总结 | custom | 开 |
| 已读+状态回执 | 收到消息即标记已读;单聊/被@时贴「处理中→完成」状态表情 | custom | 开 |
富媒体都是受控处理:harness 主动下载、识别、注入,不让 agent 自己乱下东西或执行 shell。 加能力零样板:
Capability(..., dedup=True, loop_guard=True)一行即得 msgId 去重 + 防自问自答(core 统一处理)。
# 1. opencode(数字员工的"大脑",自带免费模型)
curl -fsSL https://opencode.ai/install | bash # 或见 https://opencode.ai
# 2. dws(钉钉工作台 CLI,负责收发消息)
# 安装见 https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli
# 装好后确认可用:
dws --version数字员工本质是一个企业里的钉钉账号(用它的身份收发消息)。链路是"授权 → 组织 → 数字员工专属账号":
先授权本机:
dws auth login # 浏览器/扫码登录钉钉,把一个组织账号加成本机 profile
# SSH / 容器 / 无头环境(本机没浏览器)用设备流:
dws auth login --device # 显示 user_code + 短链接,手机钉钉扫码授权
dws auth status # 确认 authenticated: true
dws profile list # 列出本机已登录的全部组织账号(corpId / userId / 组织名)macOS 用户注意:dws 使用 macOS Keychain 存储登录凭据。如果遇到
keychain_unavailable错误,需要解锁钥匙串:security unlock-keychain ~/Library/Keychains/login.keychain-db # 或设置钥匙串永不锁定: security set-keychain-settings ~/Library/Keychains/login.keychain-db # 或使用环境变量绕过 Keychain(测试环境): export DWS_DISABLE_KEYCHAIN=1 dws auth login --device # 重新登录,token 将存储在文件中
创建组织(没有现成企业时,用 dws contact org):
dws contact org create --org-name "我的企业" --creator-username "你的名字"
# 建好后组织信息里会返回 corpId;已有企业就跳过这步入职数字员工专属账号(推荐,用 dws contact account):
给数字员工建一个独立的企业登录账号(和真人分开,身份清晰、可单独管权限):
dws contact account create \
--org-user-name "数字员工" \ # 它在企业里的显示名
--login-id "opencode-bot-01" \ # 登录号(别含手机号,否则短信可能被拦)
--dept-ids "1" \ # 加入的部门(可选)
--send-pwd-via-sms # 通过短信/邮件发登录邀请(可选)
# 需要在**已授权的企业**下执行;corpId 由系统按当前 profile 自动注入然后把这个账号拉进目标群,并让它授权本机:
dws auth login # 这次用「数字员工账号」扫码登录(不是你本人)
dws profile list # 应能看到它: <组织名> | <corpId> | 数字员工 | <userId>- 一个组织可以有多个账号(真人 + 数字员工各一份 profile)。业务命令用
--profile <corpId>:<userId>指定用谁的身份;本项目的DWS_PROFILE/AGENT_PROFILE填数字员工账号的corpId:userId。
只是先跑通、还没建专属账号?用你本人账号也能上线(回复以你的身份发出),
AGENT_SELF_NAMES填你的显示名防自问自答即可,正式交付再换成专属账号。
cp config/constants.sh config/constants.local.sh # *.local.* 被 gitignore
# 找到目标群的 openConversationId:
dws chat search --query "你的群名"编辑 config/constants.local.sh,最少填这几个:
export DWS_EVENT_GROUP="cid...==" # 上面查到的群 ID(订阅群消息)
# 可选:订阅单聊——填给数字员工发私聊的真人 userId(逗号分隔)。
# 钉钉单聊(o2o)事件只能按「对端 userId」订阅;留空=不订阅单聊。
# export DWS_EVENT_O2O_USERS="0420506555"
# 可选:只在被 @ 时才响应、又不想逐个配置群?打开「@我」订阅(跨所有群捕获被 @ 的消息):
# export DWS_EVENT_AT=1
# 群 / 单聊 / @我 三种订阅可任意组合,至少开一种。
export DWS_PROFILE="dinga...:<userId>" # 数字员工账号的 corpId:userId(见 dws profile list)
export AGENT_PROFILE="$DWS_PROFILE" # 同上(数字员工以此身份回复)
export AGENT_BRAIN="opencode" # 用 opencode 大脑
export AGENT_OPENCODE_MODEL="opencode/deepseek-v4-flash-free" # 免费文本模型
export AGENT_VISION_MODEL="" # 留空使用外部 gemini(识别质量更好)
export AGENT_REPLY_MODE="user" # 以该账号身份回复到群
export AGENT_SELF_NAMES="数字员工的显示名" # 防自问自答,填数字员工自己的名字托管 monitor.sh 进程(开机自启 + 崩溃自愈),它会自动拉起 opencode serve + 群消息订阅 + 事件监听。按你的系统选一种:
macOS(launchd)
cp bin/custom/agent-template.plist ~/Library/LaunchAgents/com.<你的组织>.<你的agent>.plist
# 编辑 plist 的 Label / ProgramArguments 指向本目录的 bin/core/monitor.sh、PATH
launchctl load -w ~/Library/LaunchAgents/com.<你的组织>.<你的agent>.plistLinux(systemd --user,无需 root)
mkdir -p ~/.config/systemd/user
cp bin/custom/agent-template.service ~/.config/systemd/user/dingtalk-agent.service
# 编辑 .service 里的 <PROJECT_DIR>(本目录绝对路径)和 <USER_LOCAL_BIN>(dws/opencode 所在目录)
systemctl --user daemon-reload
systemctl --user enable --now dingtalk-agent.service
loginctl enable-linger "$USER" # 让服务在未登录时也开机自启
# 状态/日志/停止/重启:
systemctl --user status dingtalk-agent.service
journalctl --user -u dingtalk-agent.service -f # 或看 monitor.log
systemctl --user restart dingtalk-agent.service不想装服务、先手动跑一下?
nohup bash bin/core/monitor.sh --foreground >> monitor.log 2>&1 &(跨会话存活,但机器重启不自启)。
monitor 起来后,去群里发条消息试试 —— 数字员工就回你了。
调试期想先不真发消息?把
AGENT_REPLY_MODE=log,回复只写日志不发群,验证链路无误再开真发。
bash bin/core/healthcheck.sh # 7 项健康检查,应 ✅ 健康
# 群里发 "1+1" → 数字员工回 "2"跑测试(不依赖网络/钉钉):
bash tests/core/unit_test.sh # shell 单测
for t in tests/core/*.py tests/custom/*.py; do python3 "$t"; done # Python 单测本项目功能全部由 AI 编码完成(见项目理念)。加能力的推荐姿势就是:把需求用一句话丢给 Claude Code 或 opencode,让它照现有插件范式写。
在项目根目录起一个 Coding Agent(Claude Code / opencode 都行),给它这样的提示词:
在 src/custom/capabilities/ 下新增一个能力:<描述你的能力,例如:
"收到含关键词 '排班' 的群消息时,查考勤 API 并回复本周排班表">。
要求:
- 参照现有能力的写法(如 src/custom/capabilities/image.py / file.py),
声明一个 Capability 并 register(),挂到合适的钩子(on_inbound / on_sse_event / on_cleanup)。
- 在 src/custom/capabilities/__init__.py 里 import 它。
- 加一个 CAP_<NAME>_ENABLED 开关(默认值自定),并在 config/constants.sh 文档化。
- 在 tests/custom/ 加对应单测(mock 掉网络/CLI,参照 test_image_capability.py)。
- 不要改 src/core/。遵守 AGENTS.md 里的边界。
- 跑一遍单测确认通过。
Agent 会照着现有 7 个能力(core 原语 text_reply / question / aggregation,custom 定制 ack / forward / image / file)的范式实现、写测试、验证。AGENTS.md 里写好了边界(哪些能改、约定),Agent 读了就知道怎么改不越界。 你只负责给方向和验收(最好去真实群里发条消息端到端验一下)。
想更省事:把上面这段连同"发一条 XX 消息测试一下效果"一起给 Agent,它能自己触发真实链路验证。
一个能力就是一个 Capability,挂到入站/SSE 钩子上,注册即生效。core 只认注册表,加/删能力不碰 core,upstream 修复能干净 merge。三步范例见 FORKING.md。
# src/custom/capabilities/my_cap.py
from core.capabilities import Capability, register
from core.inbound import KIND_TEXT
from core.brain import generate_reply # 生成回复(走 core 协议,平台无关)
from core.replier import send_reply # 发回来源会话
def on_inbound(msg): # msg: InboundMessage(user/text/conv_id/msg_id/kind…)
... # 处理并回复;return True=已消费,False=放行给下一个能力
return True
register(Capability(name="my_cap", on_inbound=on_inbound,
handles_kinds={KIND_TEXT}, priority=50, default_enabled=True,
dedup=True, loop_guard=True)) # msgId 去重 + 防自问自答由 core 统一处理然后在 src/custom/capabilities/__init__.py 里 import 它即生效。
一条消息从钉钉群进来、经能力处理、再回到群里的完整数据流(GitHub 上渲染为流程图):
flowchart TB
G(["💬 钉钉群<br/>数字员工账号在群里"])
subgraph DWS["dws CLI · 钉钉侧收发"]
direction TB
C["connect:dws event consume<br/>→ dws_event_bridge.py"]
R["replier:dws chat message send"]
end
subgraph EW["event_watcher · core 事件监听主进程"]
direction TB
LT["log-tail → inbound.parse_line<br/>→ InboundMessage(kind)"]
REG{{"能力注册表 core.capabilities<br/>按 kind + priority 分发"}}
CAPS["能力(各自 CAP_*_ENABLED 开关)<br/>core 原语:text_reply · question · aggregation<br/>custom 定制:ack · forward · image · file"]
LT --> REG --> CAPS
end
subgraph OC["opencode serve · 本机常驻 · 想+做的大脑"]
BRAIN["POST /session/id/message → 免费模型<br/>deepseek 文本 · mimo 看图 · 推理/工具/会话/权限"]
end
G -->|"① 消息"| C
C -->|"② connect-log"| LT
CAPS -->|"④ brain.generate_reply"| BRAIN
BRAIN -->|"⑤ 回复文本"| R
R -->|"⑥ 发回来源群"| G
BRAIN -.->|"SSE /event · question 人在回路作答"| LT
MON["🛡️ monitor.sh(launchd / systemd 托管)· 全程守护<br/>拉起兜底 serve·connect·event_watcher · healthcheck 自检<br/>崩溃自愈 · 熔断 · /reboot 远程重启"]
MON -.->|"托管"| C
MON -.->|"托管"| LT
MON -.->|"托管"| BRAIN
classDef core fill:#dbeafe,stroke:#3b82f6,color:#1e3a8a;
classDef custom fill:#dcfce7,stroke:#22c55e,color:#14532d;
classDef ext fill:#fef9c3,stroke:#eab308,color:#713f12;
classDef mon fill:#fee2e2,stroke:#ef4444,color:#7f1d1d;
class LT,REG core;
class CAPS,C,R custom;
class BRAIN ext;
class MON mon;
分工:dws 管钉钉侧收发与富媒体下载;event_watcher + 能力插件做人机协同层(受控识别/解读、路由、作答、聚合);opencode serve 做推理与任务执行;monitor 保证全程在线。
🟦 core(不改) · 🟩 custom(FDE 改这里) · 🟨 opencode 生态 · 🟥 守护
FDE 交付时通过物理分层实现"改得动 + merge 得回":
| 层 | 路径 | FDE 改? | merge 回 upstream |
|---|---|---|---|
| core | src/core/ bin/core/ tests/core/ |
❌ | ✅ bug fix 贡献回 |
| custom | src/custom/ bin/custom/ tests/custom/ |
✅ 在这里改 | ❌ 业务特定 |
| config | config/*.local.* |
✅ 填真实值 | ❌ gitignored |
src/
├── core/ ← harness 核心(不改)
│ ├── event_watcher.py ← 事件监听主进程(SSE 重连 + log-tail + 能力分发)
│ ├── capabilities.py ← 能力注册表(可组装/可选配的插件框架 + 声明式去重/防回环)
│ ├── inbound.py ← 统一 InboundMessage(消息归一 + kind 分类)
│ ├── brain.py / replier.py ← 生成/发送**协议** + 注册点(默认 echo/log;custom 注入实现)
│ ├── builtin_caps/ ← 自带通用能力原语(text_reply / question / aggregation,0 平台耦合)
│ └── agent_common.py ← 共享工具(serve 凭据+HTTP 出口 / 通知)
├── custom/ ← FDE 改这里
│ ├── capabilities/ ← 钉钉强耦合能力(ack / forward / image / file)+ 启用清单 __init__
│ ├── brain.py ← 注册 opencode serve 生成实现(免费模型)
│ └── replier.py ← 注册 dws 发送实现
bin/
├── core/ ← 守护/健康检查(不改):monitor / healthcheck / reboot / lib
└── custom/ ← start_funcs.sh(组件启动)/ dws-connect.sh(群订阅)/ plist + service(托管模板)
config/ ← constants.sh(模板)+ constants.local.sh(真实值,gitignored)
- 运维手册(启动/停止/状态查询)见 SKILL.md
- 派生指南(哪些改/不改/同步 upstream)见 FORKING.md
- 架构 + 最佳实践见 ARCHITECTURE.md
默认配置使用外部 gemini 模型进行图片识别(识别质量更好):
| 用途 | 模型 | 说明 |
|---|---|---|
| 文本对话 | opencode/deepseek-v4-flash-free |
✅ 免费文本模型 |
| 图片识别 | gemini-3.1-flash-image (via proxy) |
✅ 识别质量好,需配置 PROXY_URL |
| 图片识别(备选) | opencode/mimo-v2.5-free |
✅ 免费但识别能力较弱 |
| 语音转写 | —— | ❌ 免费模型不支持,需外部 STT(见 issue #42) |
配置说明:
AGENT_VISION_MODEL=""(留空): 使用外部 proxy 的 gemini 模型,需配置PROXY_URL和PROXY_KEYAGENT_VISION_MODEL="opencode/mimo-v2.5-free": 使用 opencode 内置免费模型,无需外部依赖
想换更强的模型?改 AGENT_OPENCODE_MODEL / AGENT_VISION_MODEL 即可。
- 平台:支持 macOS(launchd)和 Linux(systemd)。Windows 需自行适配(用服务/任务计划器托管
bin/core/monitor.sh)。core 脚本已做 macOS/Linux 双兼容(stat/date/锁)+ bash 3.2 兼容。 - 依赖 dws CLI:收发消息、下载媒体都用 dws。换平台需在 custom 层替换为对应 SDK。
- 语音消息:opencode 免费模型不支持音频转写,需接外部 STT,见 issue #42。
- serve 密码经
ps可见:.serve.pwd为明文文件、密码在进程环境变量里,多用户主机上同机其他用户可见。详见 FORKING.md 安全说明。
MIT