返回文章
Agent 与工具·约 2 分钟·Kain

用 Opencode + GLM4.7 快速打造 DingTalk 智能机器人(丐版 MoltBot/OpenClawd)

用 OpenCode 服务和钉钉消息,把终端里的 Agent 带到日常工作入口。

用 Opencode + GLM4.7 快速打造 DingTalk 智能机器人(丐版 MoltBot/OpenClawd)的封面
文章目录

最近 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 了。

继续阅读同主题文章Agent 与工具