上个月我在钉钉群里发了一句话:「给数字员工加一个查闲忙的能力。」
十分钟后,我收到了三条消息。第一条来自 Coding Agent:「✅ 已创建 Issue #12:日程能力——支持查询闲忙。」第二条还是它:「✅ 开发完成,15/15 测试通过,已 push,CI 运行中。」第三条来自数字员工本身:「🤖 v1.3.1 已上线,日程插件已更新,随时 @我 开始工作。」
从一句话到生产环境上线,我没有打开电脑,没有写一行代码,没有手动跑一次部署。
这不是 Demo。这是我过去三周每天在用的工作方式。
一个组织里的两个数字员工
我的钉钉组织里有两个数字员工,它们的主管都是我。
| Coding Agent | 数字员工 | |
|---|---|---|
| 职责 | 开发、测试、部署数字员工 | 服务组织里的真实用户 |
| 技术栈 | Claude Code + dws event consume | OpenCode + dws event consume |
| 运行环境 | 本机 | Docker 容器(远端服务器) |
| 入口 | 管理群消息(事件流) | 用户群消息(消息消费) |
| 产出 | git commit → CI/CD → Docker 镜像 | 用户问题的回答、任务执行 |
一个造,一个用。造的那个通过 GitHub CI/CD 把新的自己部署到服务器上,用的那个上线后主动向我报告状态。
这个架构不是设计出来的,是长出来的。我在 两个 Agent 的钉钉对话 里写过异构 Agent 通过钉钉消息总线协作的模式,在 让钉钉机器人自己开发自己 里写过 Coding Agent 在钉钉上修自己的 Bug。但那些都是单点突破——一个 Agent 做一件事。
这篇文章要解决的是一个完整的工程问题: 怎么让「开发数字员工」这件事本身也变成数字员工的工作?
为什么不用人写代码部署 AI
先回答一个看似显而易见的问题:为什么不让工程师写代码、跑 CI、手动部署?
因为数字员工的迭代节奏和传统软件完全不同。
传统软件是 项目制——需求评审、开发、测试、上线、验收,一个周期几周。数字员工是 运营制——用户在群里说一句「这个功能不好用」,你最好当天就改好。一个 FDE 在客户现场发现数字员工不会处理图片,他不可能提一个 Jira 等两周排期。
我在 三步上线一个数字员工 里把上线门槛降到了三分钟。但上线只是开始, 持续迭代才是数字员工的生命线。如果每次迭代都要人介入——人写需求、人改代码、人跑测试、人点部署——那数字员工的迭代速度就被人的响应速度锁死了。
解法不是「让人更快」,而是 让迭代本身也自动化。谁最了解数字员工怎么改?是另一个数字员工。
架构全景
四个组件,各司其职:
- 钉钉:唯一的人机界面。需求从钉钉来,结果回钉钉去。
- GitHub Issues:唯一的任务记录。每个需求、每个 Bug 都有编号、有状态、有验收标准。
- GitHub CI/CD:唯一的交付流水线。push 触发 build → test → docker → deploy,有审计日志,可回滚。
- Docker:唯一的运行环境。标准镜像,环境变量传参,换环境只改
docker run -e。
任务流:从一句话到生产环境
第一步:钉钉下发需求
我在管理群里说:
@Coding Agent 给数字员工加一个查闲忙的功能,要能查指定人未来 7 天的日程。
Coding Agent 通过 dws event consume 收到这条消息。它做的第一件事不是写代码,而是 创建 Issue:
import subprocess
import json
from dataclasses import dataclass
@dataclass
class TaskRequest:
"""从钉钉消息解析出的任务请求。"""
title: str
body: str
labels: list[str]
sender_id: str
conversation_id: str
def create_issue(task: TaskRequest) -> int:
"""创建 GitHub Issue 并返回编号。"""
result = subprocess.run(
[
"gh", "issue", "create",
"--title", task.title,
"--body", task.body,
"--label", ",".join(task.labels),
],
capture_output=True,
text=True,
check=True,
)
# gh issue create 输出格式: https://github.com/.../issues/12
issue_number = int(result.stdout.strip().rsplit("/", 1)[-1])
return issue_number
# generated by hugo AI
Issue 不是形式主义。它是 Coding Agent 的开发规格——标题是目标,body 是验收标准,label 是优先级。三个月后回头看,每个功能的「为什么做、怎么做的、什么时候上的」全在一条链上。
第二步:开发
Coding Agent 读取 Issue 内容,在 dingtalk-opencode-tag 仓库里开发。项目结构是 core/custom 分层:
dingtalk-opencode-tag/
├── core/ # 基础设施(不动)
│ ├── dws_consumer.py # dws consume 长连接
│ ├── message_router.py # 消息路由
│ └── state_manager.py # 状态管理
├── custom/ # 业务能力插件(改这里)
│ ├── calendar/
│ │ ├── freebusy.py # ← 新增:闲忙查询
│ │ └── test_freebusy.py
│ ├── todo/
│ ├── approval/
│ └── ...
├── Dockerfile
├── docker-compose.yml
└── .github/workflows/deploy.yml
关键约束: 改 custom/ 不需要动 core/。每个插件是独立的,有自己的测试。Coding Agent 只需要在 custom/calendar/ 下加文件,跑测试,确认通过。
第三步:Commit + Push → CI/CD 自动触发
import subprocess
from pathlib import Path
def commit_and_push(issue_number: int, message: str) -> None:
"""提交代码并推送,触发 CI/CD。"""
repo = Path("~/Projects/dingtalk-opencode-tag").expanduser()
subprocess.run(["git", "add", "-A"], cwd=repo, check=True)
subprocess.run(
["git", "commit", "-m", f"feat(calendar): 闲忙查询 Closes #{issue_number}"],
cwd=repo,
check=True,
)
subprocess.run(
["git", "push", "origin", "main"],
cwd=repo,
check=True,
env={
**__import__("os").environ,
"GIT_SSH_COMMAND": "ssh -i ~/.ssh/id_ed25519",
},
)
# generated by hugo AI
Push 之后,GitHub Actions 自动接管:
# .github/workflows/deploy.yml
name: Build & Deploy Digital Employee
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -r requirements.txt
- run: python -m pytest custom/ -v
build-and-deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t ${{ secrets.REGISTRY }}/digital-employee:${{ github.sha }} .
- name: Push image
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin
docker push ${{ secrets.REGISTRY }}/digital-employee:${{ github.sha }}
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: deploy
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
docker pull ${{ secrets.REGISTRY }}/digital-employee:${{ github.sha }}
docker stop digital-employee || true
docker rm digital-employee || true
docker run -d \
--name digital-employee \
--restart=always \
-e DWS_TOKEN=*** \
-e OPENCODE_MODEL=*** \
-e DINGTALK_CORP_ID=*** \
-e SUPERVISOR_USER_ID=*** \
-e IMAGE_TAG=${{ github.sha }} \
${{ secrets.REGISTRY }}/digital-employee:${{ github.sha }}
# generated by hugo AI
镜像不含任何密钥。 所有敏感参数通过环境变量注入。同一个镜像可以跑在任何环境——开发、测试、生产——只需要换 docker run -e 的值。
第四步:数字员工上线,主动打招呼
容器启动后,start.py 做的第一件事不是等用户消息,而是 向主管报告状态:
import os
import asyncio
import subprocess
import json
from datetime import datetime
async def report_startup() -> None:
"""数字员工上线后主动向主管打招呼。"""
supervisor_id = os.environ["SUPERVISOR_USER_ID"]
image_tag = os.environ.get("IMAGE_TAG", "unknown")
start_time = datetime.now()
# 自检:加载所有插件,检查状态
plugins = load_plugins()
plugin_status = []
for name, plugin in plugins.items():
try:
await plugin.health_check()
plugin_status.append(f"{name} ✅")
except Exception as e:
plugin_status.append(f"{name} ❌ ({e})")
startup_seconds = (datetime.now() - start_time).total_seconds()
message = (
f"🤖 数字员工 {image_tag[:7]} 已上线\n"
f"━━━━━━━━━━━━━━━━\n"
f"状态:{'✅ 正常' if all('✅' in s for s in plugin_status) else '⚠️ 部分异常'}\n"
f"插件:{' | '.join(plugin_status)}\n"
f"启动耗时:{startup_seconds:.1f}s\n"
f"━━━━━━━━━━━━━━━━\n"
f"随时 @我 开始工作。"
)
# 通过 dws 发送单聊消息给主管
subprocess.run(
[
"dws", "chat", "send",
"--user", supervisor_id,
"--content", message,
],
check=True,
)
# generated by hugo AI
我在钉钉里收到的消息长这样:
🤖 数字员工 abc1234 已上线 ━━━━━━━━━━━━━━━━ 状态:✅ 正常 插件:calendar ✅ | todo ✅ | approval ✅ | contact ✅ | chat ✅ | doc ✅ 启动耗时:3.2s ━━━━━━━━━━━━━━━━ 随时 @我 开始工作。
不是被动等查询,是主动报告。 我不用 ssh 到服务器看日志,钉钉里就知道「它活了、它健康、它准备好了」。
钉钉是唯一的控制面
这个架构里最反直觉的设计是: 没有 Dashboard,没有管理后台,没有 Web UI。所有管理操作都在钉钉里完成。
| 操作 | 怎么做 |
|---|---|
| 下发需求 | 管理群 @Coding Agent「加一个 XX 功能」 |
| 修 Bug | 管理群 @Coding Agent「修一下 #15」 |
| 查进度 | 管理群 @Coding Agent「status」 |
| 回滚 | 管理群 @Coding Agent「rollback abc1234」 |
| 看数字员工状态 | 等它主动报告,或直接 @数字员工 问 |
为什么不用 Jira + Jenkins + Grafana 那套?因为 数字员工的管理者不应该需要学一套新工具。钉钉是组织里每个人每天都在用的东西。把管理面放在钉钉里,意味着任何有权限的人都能管理数字员工——不需要会看 Kubernetes Dashboard,不需要会读 CI 日志。
这也是我在 面向 Agent 开发 里说的「可运维」的延伸:不只是 Agent 要能运维自己, 运维界面本身也要在用户已经在的地方。
环境变量:标准镜像的契约
Docker 镜像是通用的,环境差异全部通过环境变量解决:
| 变量 | 用途 | 示例 |
|---|---|---|
DWS_TOKEN | dws CLI 认证令牌 | *** |
OPENCODE_MODEL | OpenCode 使用的模型 | qwen3-8-max |
DINGTALK_CORP_ID | 组织 ID | ding... |
SUPERVISOR_USER_ID | 主管 userId(打招呼用) | 161095 |
IMAGE_TAG | 当前镜像版本(CI 注入) | abc1234 |
NODE_ENV | 运行环境 | production |
这意味着:
- 换模型:改一个环境变量,不用重新 build
- 换组织:改 corp_id + token,同一个镜像服务不同客户
- 换主管:改 supervisor_user_id,上线报告发给不同的人
- 回滚:
docker run旧 tag 的镜像,秒级恢复
FDE 到客户现场,不需要带代码、不需要装环境。一条 docker run 命令,数字员工就上线了。
安全阀:人在回路
让一个数字员工开发另一个数字员工,最大的担忧是失控。这个架构里有三道安全阀:
第一道:CI 测试
每次 push 必须过 pytest。测试不过,CI 失败,不会 build 镜像,不会部署。Coding Agent 不能绕过测试直接上线。
第二道:GitHub branch protection
main 分支开启保护规则:必须过 CI 才能 merge。即使 Coding Agent 有 push 权限,失败的 CI 会阻止部署。
第三道:主管确认(可选)
对于高风险变更(改 core/、改部署配置),可以配置 CI 在 deploy 步骤前发一张钉钉审批卡片:
📦 发布请求 变更:core/dws_consumer.py 断线重连逻辑 测试:✅ 15/15 passed 影响范围:基础设施层 [确认发布] [查看 diff] [拒绝]
我点「确认发布」才真正触发 deploy。日常改 custom/ 插件不需要审批,改 core/ 需要。 风险等级决定审批力度。
自举闭环
把整个链路串起来:
用户在群里说「这个功能不好用」
│
▼
我看到 → 在管理群 @Coding Agent「修一下 #15」
│
▼
Coding Agent: dws event consume 收到
│
├─ gh issue view 15(读取需求)
├─ 开发、测试
├─ git commit + push
│
▼
GitHub CI/CD: build → test → docker → deploy
│
▼
数字员工重启 → 自检 → 向我报告「✅ v1.3.2 已上线」
│
▼
用户在群里 @数字员工,发现功能修好了
│
▼
新的反馈 → 新的迭代
这是一个 自举闭环:数字员工服务用户 → 用户反馈 → 主管下发需求 → Coding Agent 开发 → CI/CD 部署 → 数字员工更新 → 更好地服务用户。
每一轮迭代都不需要人写代码、不需要人跑部署。人只做两件事: 判断该不该改 和 确认改得对不对。
写在最后
回到开头那个场景。我说了一句话,十分钟后数字员工就上线了新功能。这中间没有「提需求 → 排期 → 开发 → 测试 → 部署」的五步流程,只有一句话和三条回执消息。
这不是因为 Coding Agent 有多强——Claude Code 写代码的能力大家都见过。关键是把 钉钉做控制面、GitHub Issues 做任务记录、CI/CD 做交付流水线、Docker 做运行环境 这四件事串成了一条自动化链路。每个组件都是成熟的、标准的、可替换的。
我在 三步上线一个数字员工 里说,脚手架的意义是「把生产环境的脏活封装成可复制的 Harness」。这篇文章是它的下一步: 把数字员工的开发本身也变成可复制的 Harness。
上线一个数字员工需要三步。持续迭代一个数字员工,只需要一句话。
你的组织里有数字员工了吗?它是怎么迭代的?欢迎留言聊聊。