用 OpenClaw + 钉钉打造“群里一句话建会”助手(开箱可用实操教程)
一、我们要做成什么
在钉钉群里发一句:
@群里的钉钉机器人 创建会议 今天下午一点 邀请某同事 主题项目例会
效果:机器人真的调用钉钉日历,建出一场 13:00–14:00 的会议,参会人包含“某同事”,标题是“项目例会”。
这不是演示话术,是真实落地的闭环。下面把搭建过程拆成 8 步。
二、整体架构(先看清链路)
钉钉群消息
│ (钉钉连接器把消息接入 OpenClaw)
▼
OpenClaw Agent ── model: react-local/react-gemma4
│ baseUrl: http://react-proxy:11888/v1
▼
react-proxy(Python 增强层,本文主角)
│ ① 强制 ReAct 兜底 ② enrich 业务增强
▼ upstream: http://ollama:11434/v1
本地大模型(一张消费级显卡,GPU 推理)
│ 调工具
▼
钉钉日历 MCP → 真实创建日历会议
一个关键点先说在前:react-proxy 不是“第二个模型”,它和本地模型跑在同一张显卡上,只是本地模型前面套的一层 Python 壳。它干两件小模型自己干不稳的事:
- 强制调工具兜底:小模型容易“只回话不调工具”(假成功),代理发现没调工具就重问(最多 2 轮)。
- 业务增强(enrich):把正文里的同事姓名映射成钉钉 userId 注入参会人;规范标题(给了主题用你的,没给用「{发起人}的会议」)。
三、准备工作
开始前确认你已经有这些(平台侧前置,不在本文代码范围内):
- 一台能跑 Docker 的机器,带一张消费级显卡(用于本地模型 GPU 推理)。
- 已安装并运行
ollama,且拉好了你的本地模型(本文以某个 12B 级模型举例)。 - 一个钉钉开放平台应用(拿到
clientId/clientSecret),并开通了日历相关权限。 - 一个钉钉日历 MCP 的服务地址(形如
<你的钉钉日历 MCP 网关地址>,需带你的访问凭证)。
四、实操步骤
步骤 1:准备模型服务(ollama)
确保 ollama 在容器内网可达。我们把 OpenClaw 系列容器都挂到同一个自定义网络 ai-net 上,容器间用服务名互访。
# 创建专用网络(只需一次)
docker network create ai-net
启动 ollama(按你本机实际情况,注意把显卡透传给容器)
docker run -d --name ollama --network ai-net --gpus all
-v <宿主机模型目录>:/root/.ollama
ollama/ollama
验证模型能跑:
docker exec ollama ollama list
# 确认你要用的那个模型已经在列表里
步骤 2:启动 OpenClaw 核心
OpenClaw 用 Docker 跑,配置文件 openclaw.json 通过卷挂进容器。关键点是:模型的 baseUrl 不要直连 ollama,而是指向我们要加的 react-proxy:11888。
docker run -d --name openclaw --network ai-net --restart unless-stopped \
-p 18789:18789 \
-v D:/Docker/openclaw-data/openclaw.json:/home/node/.openclaw/openclaw.json \
ghcr.io/openclaw/openclaw:latest
openclaw.json 里和模型相关的核心片段(替换成你自己的):
{
"agents": {
"defaults": {
"model": "react-local/react-gemma4",
"compaction": {
"reserveTokensFloor": 4096,
"reserveTokens": 4096
}
}
},
"models": {
"react-gemma4": {
"contextWindow": 32768,
"maxTokens": 2048,
"baseUrl": "http://react-proxy:11888/v1",
"model": "gemma4:12b"
}
},
"mcp": {
"servers": {
"dingtalk-calendar": {
"url": "<你的钉钉日历 MCP 网关地址>"
}
}
},
"channels": {
"dingtalk": {
"enabled": true,
"clientId": "<你的钉钉 clientId>",
"clientSecret": "<你的钉钉 clientSecret>",
"groupPolicy": "open"
}
}
}
注意
contextWindow与compaction.reserveTokens*的关系:预留必须远小于窗口,否则长会话会触发压缩死循环(踩坑 5 详述)。
步骤 3:写“增强代理”react-proxy
新建 react_proxy.py,它做两件事:转发到 ollama + 在模型返回建会工具调用时做 enrich。下面是核心逻辑骨架(完整脚本里还包含强制调工具的重问循环,思路一致):
import json, os, httpx
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://ollama:11434/v1")
CONTACTS = json.load(open("/contacts.json", encoding="utf-8")).get("contacts", {})
INITIATOR = os.getenv("INITIATOR_NAME", "我") # 默认标题里的"发起人"
def enrich_create_event(tool_calls, messages):
"""服务端兜底:把正文里的同事姓名映射成 userId 注入参会人,并规范标题。"""
topic = extract_user_topic(messages) # 若用户给了主题则优先用
for tc in tool_calls:
fn = tc.get("function", {})
if "create_calendar_event" not in fn.get("name", ""):
continue
args = json.loads(fn.get("arguments", "{}"))
# 1) 参会人:姓名 -> userId
att = [str(a) for a in (args.get("attendees") or [])]
norm = []
for a in att:
uid = CONTACTS.get(a) # 已知姓名直接映射
norm.append(uid if uid else a)
# 2) 标题:给了主题用主题,没给且是空壳就用「{发起人}的会议」
s = (args.get("summary") or "").strip()
if topic:
args["summary"] = topic
elif is_shell_title(s):
args["summary"] = f"{INITIATOR}的会议"
args["attendees"] = norm
fn["arguments"] = json.dumps(args, ensure_ascii=False)
return tool_calls
为什么把“姓名→userId”“标题规范”写成代码而不是塞进提示词?因为这类“必须做对”的强逻辑,代码比 system prompt 稳十倍。
步骤 4:启动 react-proxy 容器并挂载
用 --env-file 传中文环境变量(避免命令行中文编码坑),把脚本和通讯录挂进容器:
# react-proxy.env 内容:
# INITIATOR_NAME=<你的名字,用作默认标题的发起人>
# OLLAMA_URL=http://ollama:11434/v1
# MAX_RETRIES=2
docker run -d --name react-proxy --network ai-net --restart unless-stopped
--env-file D:/Docker/openclaw-scripts/react-proxy.env
-v D:/Docker/openclaw-scripts/react_proxy.py:/react_proxy.py
-v D:/Docker/openclaw-scripts/contacts.json:/contacts.json
ghcr.io/openclaw/openclaw:latest
python3 /react_proxy.py
验证链路通了(从 openclaw 容器内访问代理,应返回你的模型名):
docker exec openclaw curl -s http://react-proxy:11888/v1/models
# 期望看到你的模型名,例如 gemma4:12b
步骤 5:登记通讯录 contacts.json
钉钉建会必须传 userId(不是姓名),所以维护一张「姓名→userId」静态表。新增常邀成员,加一行、重启代理即可。
{
"contacts": {
"某同事": "<钉钉 userId,一串数字>",
"另一同事": "<钉钉 userId,一串数字>"
}
}
怎么拿到同事的 userId?用钉钉通讯录 MCP 的“按姓名查用户”工具即可,不在本文展开。
步骤 6:把规则写进技能 SKILL.md
让 Agent 建会时自动遵守约定,新建一个技能文件 /home/node/.openclaw/skills/dingtalk-meeting/SKILL.md,核心要点:
# 钉钉会议创建技能
- 参会人必须用 userId;已知同事查 contacts.json 映射。
- 标题规则:用户给了主题用用户的;没给则用「{发起人}的会议」。
- 群消息里 @成员 不会被自动带入,必须在正文里写清"邀请某某"。
- 时间要带"今天/明天 + 几点",默认按本地时区。
把文件 docker cp 进容器对应目录,OpenClaw 重启后即生效。
步骤 7:验收——在群里发一句话
等 OpenClaw healthy(首次启动约 6–7 分钟,期间网页“gateway 断开”属正常),在钉钉群发:
@群里的钉钉机器人 创建会议 今天下午一点 邀请某同事 主题项目例会
验收标准:
- 日历里出现 13:00–14:00 的会议 ✅
- 参会人含“某同事” ✅
- 标题是“项目例会”(你给的主题被保留)✅
五、踩坑速查(附解法)
| 坑 | 现象 | 解法 |
|---|---|---|
| 假成功 | 机器人说建好了,日历没有 | 加 react-proxy 强制调工具兜底,没调工具就重问 |
| 参会人只有自己 | 邀请了却没带进去 | contacts.json 登记 userId,enrich 自动注入 |
| 标题乱填 | 变成“会议(邀请:XX)” | enrich 识别空壳标题,纠正为「{发起人}的会议」 |
| 群 @ 失效 | @同事没被邀请 | 连接器不透传 @提及,参会人写在正文里驱动注入 |
| 代理不回复 | 改小窗口后彻底卡死 | contextWindow 留足,reserveTokens 远小于窗口 |
关于“群 @ 失效”重点说一句:我们拉了会话原始日志 .trajectory.jsonl 实锤——钉钉连接器注入给 Agent 的上下文只有发送者,没有 @成员。所以“@能不能被 Agent 感知”是连接器级别的能力,不是 MCP 或模型的事。钉钉日历 MCP 的 attendees 参数确实支持 @写法,但前提是 @数据先到 Agent。上游丢了,下游再强也救不回。当前用“正文写姓名”规避即可。
六、给同行的建议
- 小模型落地企业工具,务必加“强制调工具”兜底。模型越小,函数调用越不可靠,“假成功”比报错更坑。
- 业务强逻辑下沉到增强层,别指望提示词。userId 映射、标题规范写成代码最稳。
- 选型连接器时,确认它是否透传 @提及 / atUsers。这是群协作的隐形前提,文档往往不写,要实测入站消息原文。
- 上下文窗口留足余量。Agent 多轮对话 token 累积很容易超你预期。
- 日志是第一真相源。会话
.trajectory.jsonl比docker logs权威,能直接看到 Agent 收到的原始消息。
七、结语
“自然语言建会”看似一个功能点,实则串联起模型可靠性、企业身份体系(userId)、连接器能力边界、上下文工程四条主线。难点不在“调一个 MCP”,而在把这些异构环节缝成一个稳的闭环。照着上面 8 步搭一遍,你也能在群里用一句话把会开了。
(文中架构与配置均来自真实生产环境复盘,具体鉴权密钥、真实姓名与用户 ID 均已脱敏。)
更多推荐

所有评论(0)