用 Opencode + GLM4.7 快速打造 DingTalk 智能机器人(丐版 MoltBot/OpenClawd)
用 OpenCode 服务和钉钉消息,把终端里的 Agent 带到日常工作入口。
文章目录
最近 ClawdBot 很火,其出圈也是技术积累到一定程度的产物。其实大家都在往这方面思考,技术行业越来越卷了。ClawdBot 借助沙箱+IM 工具打通了终端和工作端的便利性,这种思路在前面思考 Jarvis 的实现时其实就有了,没想到这么快就做出来了。虽然 ClawdBot 已经比较成熟,但自己折腾一下,也能很好地整理思路,深入理解整个过程。授人以鱼不如授人以渔。
Opencode 作为 Agent 提供了一套完整的 API 接口,同时也有对应的 NodeJs SDK。有了 SDK,打通 IM 也就简单了。整体思路是在 Docker 中运行 Opencode,前端部署一个 NodeJS 服务作为网关,转发 DingTalk 消息到 Opencode。
OpenCode 服务
开启 OpenCode 服务很简单:
opencode serve --hostname 0.0.0.0
为了测试方便,这里放开了所有 IP。如果你跑在容器里,控制好权限,安全性会好很多。细节参数可参考官方文档。
Opencode 的接口验证稍微有点麻烦,踩了不少坑。为简单起见,第一次可以先照抄。服务起来后,先看一下 provider,这很重要。
查看 provider 信息,记录所有 opencode 支持的 provider 及可用的 provider:
curl http://127.0.0.1:4096/provider
第一次执行会比较慢,如果超时可再执行一次。返回内容是一个巨大的 json,不需要关心拉到最后,确保存在:"zhipuai-coding-plan":"glm-4.7"。如果没有,就找对应你在 OpenCode 中用的 Model。特别注意一个模型会有很多提供商,千万别搞错。
创建一个 Session,你也可以用已存在的 session:
curl -i 'http://127.0.0.1:4096/session' \
-X 'POST' \
-H 'Content-Type: application/json' \
-d '{ "title": "My session2"}'
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Origin
Date: Sat, 31 Jan 2026 02:31:15 GMT
Content-Length: 209
{"id":"ses_3ee1c119fffezwh6nQUsFKpHbz","slug":"crisp-moon","version":"1.1.43","projectID":"global","directory":"C:\\Users\\kainh","title":"My session2","time":{"created":1769826676320,"updated":1769826676320}}
把 session id 记下来,到这里我们就可以测试发送消息了。
POST http://127.0.0.1:4096/session/ses_3ee1c119fffezwh6nQUsFKpHbz/message \
-X 'POST' \
-H 'Content-Type: application/json' \
# 为显示方便这里未调整格式
{
"model" : {
"providerID": "zhipuai-coding-plan", # 前面的 Provider ID
"modelID": "glm-4.7" # 前面的 Model ID,注意大小写
},
"parts":[
{ "type": "text", "text": "Hi" } # 消息内容,type 支持多种格式,详见官方文档代码(文档未写清楚)
]
}
如果消息一直不返回,那就是 model 设置错了,要重点检查 provider 信息是否匹配。
DingTalk
钉钉需要用机器人,机器人的相关配置在钉钉官方文档讲得很详细,这里不赘述,直接上关键代码。
相关依赖
"@opencode-ai/sdk": "^1.1.43",
"axios": "^1.13.4",
"dingtalk-stream": "^2.1.4",
"dotenv": "^16.3.1"
import { DWClient, DWClientDownStream, EventAck,TOPIC_ROBOT } from 'dingtalk-stream';
import { config, validateConfig } from './config.js';
import { MessageHandler } from './bot/handler.js';
import { DingTalkCallbackMessage, BotResponse } from './types/index.js';
import axios from 'axios';
import { opencodeService } from '../opencode/opencode.js';
async start(): Promise<void> {
console.log('🤖 钉钉机器人服务启动中...');
console.log(`📡 使用 Client ID: ${config.clientId.slice(0, 8)}...`);
// 注册消息处理器
this.client.registerCallbackListener(
TOPIC_ROBOT,
async (req: DWClientDownStream) => {
// 解析消息
const msg: DingTalkCallbackMessage = JSON.parse(req.data);
// 建议:将 DingTalk 的 conversationId 与 opencode sessionId 做映射,保证同一会话上下文一致
// 例如:const sessionId = getOrCreateSessionId(msg.conversationId);
const resp = await this.handleIncomingMessage(msg);
const body:any = {
at: {
atUserIds: [msg.senderStaffId],
isAtAll: false,
},
msgtype: resp.message?.msgtype,
};
if(resp.message?.msgtype === "markdown"){
body.markdown = {
text: resp.message?.content,
title: resp.message?.title
};
}
else{
body.text = { content: resp.message?.content };
}
const accessToken = await this.client.getAccessToken();
const result = await axios({
url: msg.sessionWebhook,
method: "POST",
responseType: "json",
data: body,
headers: {
"x-acs-dingtalk-access-token": accessToken,
},
});
// 发送响应回钉钉服务器
if(result?.data && result.data.errcode === 0 ){
this.client.socketCallBackResponse(req.headers.messageId, result.data);
}
else{
console.log("发送消息失败:" + result.data.errmsg);
}
}
);
// 注册卡片回调处理器(可选)
this.client.registerCallbackListener(
'/v1.0/card/instances/callback',
async (message: DWClientDownStream) => {
console.log('收到卡片回调:', message);
this.client.socketCallBackResponse(message.headers.messageId, { status: EventAck.SUCCESS });
}
);
// 启动连接
await this.client.connect();
console.log('✅ 钉钉机器人服务已启动');
console.log('📋 提示: 在钉钉中 @机器人并发送"帮助"查看可用命令');
}
/**
* 处理收到的消息
*/
private async handleIncomingMessage(
msg: DingTalkCallbackMessage
): Promise<{ status: EventAck; message?: BotResponse }> {
try {
// 建议:此处应根据 conversationId 获取或创建 sessionId
// const sessionId = getOrCreateSessionId(msg.conversationId);
opencodeService.connect(null);
// 获取消息内容
const text = msg.text?.content?.trim() || '';
const response = await opencodeService.sendMessage(text);
if (response) {
return {
status: EventAck.SUCCESS,
message: response,
};
}
return { status: EventAck.SUCCESS };
} catch (error) {
console.error('处理消息失败:', error);
return {
status: EventAck.SUCCESS,
message: {
msgtype: 'text',
content: '处理消息时出错了,请稍后重试。',
},
};
}
}
}
消息处理
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk"
export class OpencodeService {
private client:OpencodeClient | undefined;
private sessionId?: string;
private model = {
modelID:"glm-4.7",
providerID:"zhipuai-coding-plan"
}
connect(sessionId:string|null): OpencodeService {
const baseUrl = "http://127.0.0.1:4096";
this.client = createOpencodeClient({
baseUrl,
headers: {
"Authorization": "Basic " + Buffer.from("abc:123456").toString("base64") //如无身份验证可去掉
}
});
this.sessionId = sessionId || this.sessionId;
return this;
};
async openSession(): Promise<string>{
if(this.sessionId){
return this.sessionId;
}
const session = await this.client?.session.create({
body: { title: "DingTalk-" + new Date().toISOString() },
})
this.sessionId = session?.data?.id;
return this.sessionId!;
}
async sendMessage(message: string): Promise<string> {
if(!this.client){
throw new Error("Opencode client is not connected.");
}
// 发送 prompt 消息
const result = await this.client.session.prompt({
path: { id: this.sessionId! },
body: {
model:this.model,
parts: [{ type: "text", text: message }],
},
});
const text = result.data?.parts.find(part => part.type === "text")?.text;
return text || "No response";
}
}
export const opencodeService = new OpencodeService();
这里关键核心逻辑都已具备,通过钉钉机器人即可遥控 opencode,至于 opencode 能做什么,就看你的 agent 和 skill 了。