SuperAPI AI Directory

openclaw-dingtalk-channel

Agents & models

View repository

Updated 10-16 02:08

About this skill

OpenClaw DingTalk Channel Skill

Skill by ara.so — Hermes Skills collection.

This skill provides expertise in configuring and using the @soimy/dingtalk channel plugin for OpenClaw, which enables DingTalk enterprise bot integration with Stream mode (no public IP required).

What It Does

The DingTalk channel plugin connects OpenClaw to DingTalk enterprise bots, supporting:

  • Stream mode: No webhook or public IP needed
  • Message types: Text, images, voice, video, files, DingTalk docs/cards
  • Reply modes: Markdown and AI Card (v2 structured blocks with streaming)
  • Multi-agent routing: Bind multiple agents/bots, use @agent-name routing
  • Bypass mode: /btw for quick answers outside main session lock
  • Real-time abort: Stop AI generation mid-stream
  • Device Flow auth: Scan QR code to auto-register credentials

Installation

Prerequisites

Requires OpenClaw >= 2026.3.24.

Install from ClawHub

openclaw plugins install @soimy/dingtalk

Install from Local Source

git clone https://github.com/soimy/openclaw-channel-dingtalk.git
cd openclaw-channel-dingtalk
npm install
openclaw plugins install -l .

Enable Plugin

Add to OpenClaw configuration:

{
  "plugins": {
    "enabled": true,
    "allow": ["dingtalk"]
  }
}

Configuration

Interactive Setup (Recommended)

openclaw onboard

Or configure channels section:

openclaw configure --section channels

Minimal Manual Configuration

{
  "channels": {
    "dingtalk": {
      "enabled": true,
      "clientId": "dingxxxxxx",
      "clientSecret": "your-app-secret",
      "dmPolicy": "open",
      "groupPolicy": "open",
      "messageType": "markdown"
    }
  }
}

Full Configuration Example

{
  "channels": {
    "dingtalk": {
      "enabled": true,
      "clientId": process.env.DINGTALK_CLIENT_ID,
      "clientSecret": process.env.DINGTALK_CLIENT_SECRET,
      
      // Access policies
      "dmPolicy": "open",        // "open" | "whitelist" | "blacklist" | "closed"
      "groupPolicy": "open",     // Same options
      "dmWhitelist": [],         // User IDs if dmPolicy is "whitelist"
      "groupWhitelist": [],      // Conversation IDs if groupPolicy is "whitelist"
      
      // Message type: "markdown" or "aiCard"
      "messageType": "aiCard",
      
      // AI Card streaming options
      "aiCardStreamingMode": true,
      "aiCardStreamingInterval": 800,  // ms between chunks
      
      // Multi-agent bindings
      "agentBindings": [
        {
          "agentName": "default",
          "robotCode": "dingxxxxxx",
          "isDefault": true
        },
        {
          "agentName": "analyst",
          "robotCode": "dingyyyyyyy",
          "isDefault": false
        }
      ],
      
      // Routing
      "enableAtAgentRouting": true,
      "atAgentRoutingPrefix": "@",
      
      // Features
      "enableBtwBypass": true,
      "enableRealTimeAbort": true,
      "abortKeywords": ["停止", "stop", "/stop", "esc"],
      
      // Security
      "maxMessageLength": 10000,
      "rateLimitPerUser": 20,
      "rateLimitWindow": 60000  // 1 minute
    }
  }
}

Environment Variables

export DINGTALK_CLIENT_ID="dingxxxxxx"
export DINGTALK_CLIENT_SECRET="your-app-secret"

DingTalk App Setup

Required Permissions

In DingTalk Developer Console, configure:

  1. Application Credentials: Client ID and Client Secret
  2. Stream Mode: Enable and configure
  3. API Permissions:
    • qyapi_robot_sendmsg - Send messages
    • qyapi_chat_manage - Group chat management
    • contact.User.Read - Read user info
    • im.Chat.Read - Read conversation info
  4. Event Subscriptions:
    • chat_update_title - Group name changes
    • chat_update_owner - Owner changes
    • im_robot_at_message - @ mentions
    • im_robot_message - Direct messages

Key Features & Usage

Reply Modes

Markdown Mode

Simple text responses with Markdown formatting:

{
  "messageType": "markdown"
}

User sees plain Markdown-formatted text.

AI Card Mode

Rich interactive cards with streaming support:

{
  "messageType": "aiCard",
  "aiCardStreamingMode": true,
  "aiCardStreamingInterval": 800
}

Features:

  • Structured block rendering
  • Inline images
  • Task metadata display
  • Real-time streaming updates

/btw Bypass Mode

Quick answers outside the main session lock:

User: /btw What's the weather?

The message bypasses conversation locking and gets immediate response in a separate session. Prefix is configurable:

{
  "enableBtwBypass": true,
  "btwPrefix": "/btw"  // Optional, defaults to "/btw"
}

Multi-Agent & Bot Bindings

Bind multiple OpenClaw agents to different DingTalk bots:

{
  "agentBindings": [
    {
      "agentName": "default",
      "robotCode": "dingbot001",
      "isDefault": true
    },
    {
      "agentName": "analyst",
      "robotCode": "dingbot002",
      "isDefault": false
    },
    {
      "agentName": "writer",
      "robotCode": "dingbot003",
      "isDefault": false
    }
  ]
}

@Agent Routing

Route to specific agents by mentioning them:

User: @analyst analyze Q4 revenue

{
  "enableAtAgentRouting": true,
  "atAgentRoutingPrefix": "@"
}

The message routes to the "analyst" agent if bound.

Real-Time Abort

Stop AI generation mid-stream:

User: stop or 停止 or esc

{
  "enableRealTimeAbort": true,
  "abortKeywords": ["停止", "stop", "/stop", "esc"]
}

Message Type Handling

Plugin automatically handles:

  • Text: Direct processing
  • Images: Extracted and passed to vision-capable models
  • Voice: Transcribed if supported
  • Files: Content extracted when possible
  • DingTalk Docs: Content fetched via API
  • Reply/Quote: Original message context included

Code Examples

TypeScript Plugin Development

import type { ChannelPlugin, InboundMessage, OutboundMessage } from '@openclaw/types';

export class DingTalkChannel implements ChannelPlugin {
  name = 'dingtalk';
  
  async processInbound(rawMessage: any): Promise<InboundMessage> {
    const { msgtype, text, senderStaffId, conversationId } = rawMessage;
    
    return {
      channelId: 'dingtalk',
      userId: senderStaffId,
      conversationId,
      content: {
        type: 'text',
        text: text?.content || ''
      },
      metadata: {
        msgId: rawMessage.msgId,
        timestamp: rawMessage.createAt
      }
    };
  }
  
  async processOutbound(message: OutboundMessage): Promise<void> {
    const { conversationId, content, metadata } = message;
    
    if (this.config.messageType === 'aiCard') {
      await this.sendAICard(conversationId, content, metadata);
    } else {
      await this.sendMarkdown(conversationId, content);
    }
  }
  
  private async sendAICard(
    conversationId: string,
    content: any,
    metadata?: any
  ): Promise<void> {
    const cardData = {
      cardTemplateId: 'standard',
      outTrackId: metadata?.trackId,
      cardData: {
        cardParamMap: {
          content: content.text,
          blocks: this.formatBlocks(content)
        }
      }
    };
    
    await this.client.sendInteractiveCard({
      conversationId,
      ...cardData
    });
  }
}

Handling Streaming Updates

async streamAIResponse(
  conversationId: string,
  generator: AsyncGenerator<string>
): Promise<void> {
  let buffer = '';
  let lastUpdate = 0;
  const interval = this.config.aiCardStreamingInterval || 800;
  
  for await (const chunk of generator) {
    buffer += chunk;
    const now = Date.now();
    
    if (now - lastUpdate >= interval) {
      await this.updateAICard(conversationId, buffer);
      lastUpdate = now;
    }
  }
  
  // Final update
  await this.updateAICard(conversationId, buffer, { finished: true });
}

Custom Agent Routing Logic

function resolveAgent(message: InboundMessage): string {
  const { content, metadata } = message;
  
  // Check for @agent mention
  const atMatch = content.text.match(/@(\w+)\s/);
  if (atMatch) {
    const agentName = atMatch[1];
    const binding = this.config.agentBindings.find(
      b => b.agentName === agentName
    );
    if (binding) return agentName;
  }
  
  // Check robot code mapping
  const robotCode = metadata.robotCode;
  const binding = this.config.agentBindings.find(
    b => b.robotCode === robotCode
  );
  
  return binding?.agentName || 'default';
}

Implementing Bypass Mode

function isBtwMessage(text: string): boolean {
  const prefix = this.config.btwPrefix || '/btw';
  return text.trim().startsWith(prefix);
}

async processBtwMessage(message: InboundMessage): Promise<void> {
  const prefix = this.config.btwPrefix || '/btw';
  const cleanText = message.content.text.replace(prefix, '').trim();
  
  // Create isolated session
  const btwSession = await this.createBypassSession(message.userId);
  
  // Process independently
  const response = await this.agent.chat(cleanText, {
    sessionId: btwSession.id,
    bypass: true
  });
  
  await this.send(message.conversationId, response);
}

Common Patterns

Policy-Based Access Control

function checkAccess(
  policy: 'open' | 'whitelist' | 'blacklist' | 'closed',
  identifier: string,
  whitelist: string[],
  blacklist: string[]
): boolean {
  switch (policy) {
    case 'closed':
      return false;
    case 'whitelist':
      return whitelist.includes(identifier);
    case 'blacklist':
      return !blacklist.includes(identifier);
    case 'open':
    default:
      return true;
  }
}

// Usage
const canDM = checkAccess(
  config.dmPolicy,
  message.userId,
  config.dmWhitelist,
  config.dmBlacklist
);

const canGroup = checkAccess(
  config.groupPolicy,
  message.conversationId,
  config.groupWhitelist,
  config.groupBlacklist
);

Rate Limiting

class RateLimiter {
  private counts = new Map<string, number[]>();
  
  check(userId: string, limit: number, window: number): boolean {
    const now = Date.now();
    const timestamps = this.counts.get(userId) || [];
    
    // Remove old timestamps
    const recent = timestamps.filter(t => now - t < window);
    
    if (recent.length >= limit) {
      return false;
    }
    
    recent.push(now);
    this.counts.set(userId, recent);
    return true;
  }
}

// Usage
if (!this.rateLimiter.check(
  message.userId,
  config.rateLimitPerUser,
  config.rateLimitWindow
)) {
  throw new Error('Rate limit exceeded');
}

Error Recovery

async sendWithRetry(
  conversationId: string,
  content: any,
  maxRetries = 3
): Promise<void> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await this.send(conversationId, content);
      return;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      
      // Exponential backoff
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
    }
  }
}

Troubleshooting

Plugin Not Loading

Check plugin allowlist:

openclaw config get plugins.allow

Should include "dingtalk".

Verify installation:

openclaw plugins list

No Messages Received

Check Stream connection:

openclaw gateway status

Verify credentials:

openclaw config get channels.dingtalk.clientId
openclaw config get channels.dingtalk.clientSecret

Test DingTalk app permissions in Developer Console → Application Details → Permissions.

AI Card Not Streaming

Enable streaming mode:

{
  "messageType": "aiCard",
  "aiCardStreamingMode": true
}

Check interval (minimum 500ms recommended):

{
  "aiCardStreamingInterval": 800
}

Rate Limit Errors

Increase limits (if quota allows):

{
  "rateLimitPerUser": 30,
  "rateLimitWindow": 60000
}

Check DingTalk quota in Developer Console → Resource Management.

Agent Routing Fails

Verify bindings:

openclaw config get channels.dingtalk.agentBindings

Check agent names match:

openclaw agents list

Enable routing:

{
  "enableAtAgentRouting": true
}

Device Flow Registration

If auto-registration fails:

  1. Check network connectivity
  2. Verify clientId and clientSecret
  3. Ensure DingTalk app has Device Flow enabled
  4. Check logs: openclaw logs --filter dingtalk

Updates

Update from ClawHub

openclaw plugins update dingtalk

Update from Local Source

cd openclaw-channel-dingtalk
git pull
openclaw gateway restart

Resources

Version Compatibility

  • Minimum OpenClaw version: 2026.3.24
  • Recommended: Latest stable release
  • DingTalk API quota: Free unlimited until 2026-03-31 (or 2026-04-30 if approved)