Generated: 2026-03-28 Type: OpenClaw DingTalk Channel Plugin
DingTalk (钉钉) enterprise bot channel plugin using Stream mode (WebSocket, no public IP required). Part of OpenClaw ecosystem.
Current architecture is modularized by responsibility. src/channel.ts is now an assembly layer; heavy logic is split into dedicated modules.
Recent refactors unified short-lived message persistence into src/message-context-store.ts and split reply delivery selection into dedicated reply-strategy* modules.
Recent targeting work added a learned target directory under src/targeting/ and a displayNameResolution config gate (disabled by default, all to enable learned displayName resolution).
For new code and refactors, the canonical architecture guide is docs/contributor/architecture.en.md.
Chinese version: docs/contributor/architecture.zh-CN.md.
Use those documents as the source of truth for logical domain placement, incremental migration rules, and module boundaries.
For AI-agent generated design and execution docs, write specs to docs/spec/ and plans to docs/plans/. Do not create tool-specific doc roots such as docs/superpowers/.
Documentation updates must follow the repo docs structure: keep README.md as a concise project entry page, put user-facing details in docs/user/, contributor/process docs in docs/contributor/, and release notes in docs/releases/. Do not expand README with long-form feature/config/troubleshooting content that belongs in docs/.
This repository is licensed under MIT. If you reuse code, retain the copyright and license notice required by MIT.
If you substantially reuse this repository's documentation, prompts, AGENTS/CLAUDE conventions, architecture writeups, or agent-oriented implementation playbooks, please provide attribution to OpenClaw DingTalk Channel Plugin, YM Shen and contributors, and https://github.com/soimy/openclaw-channel-dingtalk.
See docs/contributor/citation-and-attribution.md and CITATION.cff for the preferred citation and attribution format. This request describes the project's preferred community norm and does not replace or modify the LICENSE file.
Issue convention for this repo: prefer the GitHub issue templates under .github/ISSUE_TEMPLATE/; keep issue communication primarily in Simplified Chinese; use 问题反馈 for bugs and 功能建议 for feature ideas; and encourage reporters to include background, reproduction or goals, environment, and desensitized evidence.
Pull request convention for this repo: use an English Conventional-style PR title such as fix(targeting): normalize learned display names; keep the title in English; write the PR description in Simplified Chinese; and include clearly labeled 背景, 目标, 实现, 实现 TODO, and 验证 TODO sections.
Planned domain summary:
gateway/: stream connection lifecycle, callback registration, inbound entry pointstargeting/: peer identity, session aliasing, target resolution, and learned displayName directorymessaging/: inbound extraction, reply strategies, outbound delivery, message contextcard/: AI card lifecycle, recovery, and cachescommand/: slash commands and related extensions including feedback learningplatform/: config, auth, runtime, logger, and core typesshared/: reusable persistence primitives, dedup, and generic helpers
./
├── index.ts # Plugin registration entry point
├── src/
│ ├── channel.ts # Channel definition + gateway wiring + public exports
│ ├── inbound-handler.ts # Inbound pipeline (authz, routing, quote/media restore, dispatch orchestration)
│ ├── send-service.ts # Outbound send (session/proactive/text/media/card fallback)
│ ├── card-service.ts # AI Card lifecycle + cache + recovery helpers
│ ├── card-callback-service.ts # Card callback handling and action processing
│ ├── card-draft-controller.ts # Card draft buffering / state transitions
│ ├── reply-strategy.ts # Reply strategy selection entry
│ ├── reply-strategy-card.ts # AI Card reply strategy
│ ├── reply-strategy-markdown.ts # Markdown/text reply strategy
│ ├── reply-strategy-with-reaction.ts # Reply wrapper for reaction lifecycle
│ ├── auth.ts # Access token cache + retry
│ ├── config.ts # Config/account/agent helpers
│ ├── config-schema.ts # Zod validation schema
│ ├── runtime.ts # Runtime getter/setter
│ ├── types.ts # Shared types/constants
│ ├── access-control.ts # DM/group allowlist checks
│ ├── message-utils.ts # Markdown/title detection + inbound content extraction
│ ├── message-context-store.ts # Unified short-TTL message context persistence
│ ├── media-utils.ts # Media type detect + upload/download helpers
│ ├── quoted-file-service.ts # Quote/file recovery helpers
│ ├── docs-service.ts # DingTalk docs gateway methods
│ ├── feedback-learning-service.ts # Learning signal handling
│ ├── feedback-learning-store.ts # Learning persistence
│ ├── learning-command-service.ts # /learn command handling
│ ├── session-command-service.ts # Session alias and related commands
│ ├── connection-manager.ts # Robust stream connection lifecycle
│ ├── dedup.ts # Inbound message dedup with TTL + lazy cleanup
│ ├── persistence-store.ts # Namespace-based persistence primitives
│ ├── session-routing.ts # Agent/session routing helpers
│ ├── session-peer-store.ts # Session peer persistence
│ ├── session-lock.ts # Per-session dispatch locking
│ ├── peer-id-registry.ts # Preserve case-sensitive conversationId mapping
│ ├── proactive-risk-registry.ts # Proactive send risk tracking
│ ├── logger-context.ts # Shared logger getter/setter
│ ├── onboarding.ts # Channel onboarding adapter
│ ├── ack-reaction/
│ │ ├── dynamic-ack-reaction-controller.ts # Tool-progress reaction orchestration
│ │ ├── dynamic-ack-reaction-events.ts # Reaction event definitions
│ │ └── dynamic-ack-reaction-progress.ts # Reaction progress mapping
│ ├── messaging/
│ │ ├── attachment-text-extractor.ts # Text extraction for supported attachments
│ │ ├── quoted-file-service.ts # Quote/file recovery helpers
│ │ ├── quoted-context.ts # Quoted context assembly
│ │ └── quoted-ref.ts # Structured quotedRef helpers
│ └── targeting/
│ ├── agent-name-matcher.ts # @agent name matching
│ ├── agent-routing.ts # Sub-agent routing helpers
│ ├── group-members-store.ts # Group member cache/persistence
│ ├── target-directory-adapter.ts # Learned directory bridge + displayNameResolution gate
│ ├── target-directory-store.ts # Learned group/user target persistence
│ └── target-input.ts # DingTalk target normalization + id heuristics
├── docs/
│ ├── index.md # Docs home
│ ├── .vitepress/ # VitePress site config and build output root
│ ├── user/ # User-facing install/config/features/troubleshooting docs
│ ├── contributor/ # Contributor/dev/test/release/architecture docs
│ ├── releases/ # Release notes index + version pages
│ ├── en/ # Partial English entry pages
│ ├── spec/ # AI-authored design/spec docs (not published)
│ ├── plans/ # AI-authored implementation plans (not published)
│ ├── archive/ # Archived/non-nav docs
│ └── assets/ # Non-published doc assets
├── scripts/
│ ├── dingtalk-connection-check.* # Connection diagnostics for Stream setup
│ ├── dingtalk-stream-monitor.mjs # Stream monitoring helper
│ └── feedback-learning-debug.mjs # Local feedback-learning inspection UI
├── tests/
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests with mocked external calls
├── .github/
│ └── workflows/ # CI, npm publish, docs pages deploy
└── [config files] # package.json, tsconfig.json, vitest.config.ts, lint/format configs
| Task | Location | Notes |
|---|---|---|
| Plugin registration | index.ts |
Exports default plugin object |
| Channel assembly | src/channel.ts |
Defines dingtalkPlugin; wires gateway/outbound/status |
| Inbound message handling | src/inbound-handler.ts |
handleDingTalkMessage, downloadMedia |
| Text/media sending | src/send-service.ts |
sendBySession, sendProactive*, sendMessage |
| Reply strategy selection | src/reply-strategy.ts |
createReplyStrategy |
| AI Card operations | src/card-service.ts |
createAICard, streamAICard, finishAICard |
| Message context persistence | src/message-context-store.ts |
upsertInboundMessageContext, upsertOutboundMessageContext, resolveByMsgId, resolveByAlias |
| Token management | src/auth.ts |
getAccessToken with clientId-scoped cache |
| Access control | src/access-control.ts |
DM/group allowlist helpers |
| Message parsing | src/message-utils.ts |
quote parsing + richText/media extraction |
| Config/path helpers | src/config.ts |
getConfig, resolveRelativePath, stripTargetPrefix |
| Target directory persistence | src/targeting/target-directory-store.ts |
learned group/user displayName directory |
| Target directory adapter | src/targeting/target-directory-adapter.ts |
directory bridge + displayNameResolution gate |
| Deduplication | src/dedup.ts |
message retry dedup keys |
| Type definitions | src/types.ts |
DingTalk and plugin types/constants |
| Symbol | Type | Location | Role |
|---|---|---|---|
dingtalkPlugin |
const | src/channel.ts |
Main channel plugin definition |
handleDingTalkMessage |
function | src/inbound-handler.ts |
Process inbound messages end-to-end |
downloadMedia |
function | src/inbound-handler.ts |
Download inbound media via runtime media service |
sendBySession |
function | src/send-service.ts |
Send replies via session webhook |
sendMessage |
function | src/send-service.ts |
Auto send (card/text/markdown fallback) |
sendProactiveMedia |
function | src/send-service.ts |
Proactive media send |
createReplyStrategy |
function | src/reply-strategy.ts |
Select reply implementation by mode/capability |
createAICard |
function | src/card-service.ts |
Create and cache AI Card |
streamAICard |
function | src/card-service.ts |
Stream updates to AI Card |
finishAICard |
function | src/card-service.ts |
Finalize AI Card |
upsertInboundMessageContext |
function | src/message-context-store.ts |
Persist inbound message context by canonical msgId |
upsertOutboundMessageContext |
function | src/message-context-store.ts |
Persist outbound message context + delivery aliases |
resolveByMsgId |
function | src/message-context-store.ts |
Resolve unified message record by canonical/inbound msgId |
resolveByAlias |
function | src/message-context-store.ts |
Resolve outbound record by messageId/processQueryKey/outTrackId/cardInstanceId |
upsertObservedGroupTarget |
function | src/targeting/target-directory-store.ts |
Persist observed group conversationId/displayName |
upsertObservedUserTarget |
function | src/targeting/target-directory-store.ts |
Persist observed user staffId/senderId/displayName |
listDingTalkDirectoryGroups |
function | src/targeting/target-directory-adapter.ts |
Expose learned group directory entries |
listDingTalkDirectoryUsers |
function | src/targeting/target-directory-adapter.ts |
Expose learned user directory entries |
getAccessToken |
function | src/auth.ts |
Get/cached DingTalk token |
extractMessageContent |
function | src/message-utils.ts |
Normalize inbound msg payload |
normalizeAllowFrom |
function | src/access-control.ts |
Normalize allowlist entries |
isMessageProcessed |
function | src/dedup.ts |
Message dedup check |
DingTalkConfigSchema |
const | src/config-schema.ts |
Zod validation schema |
AICardStatus |
const | src/types.ts |
AI Card state constants |
Code Style:
- TypeScript strict mode enabled
- ES2020 target, ESNext modules
- 4-space indentation (Prettier)
- Public low-level API exported from
src/channel.ts(re-exported from service modules)
Naming:
- Private functions: camelCase
- Exported functions: camelCase
- Type interfaces: PascalCase
- Constants: UPPER_SNAKE_CASE
Error Handling:
- Use
try/catchfor async API calls - Log with structured prefixes (e.g.
[DingTalk],[DingTalk][AICard],[DingTalk][Dispatch]) - For DingTalk API error payloads, use unified prefix format:
- Standard:
[DingTalk][ErrorPayload][<scope>] - AI Card:
[DingTalk][AICard][ErrorPayload][<scope>] - Include
code=<...> message=<...> payload=<...>for fast diagnosis
- Standard:
- Send APIs return
{ ok: boolean, error?: string }where applicable - Retry with exponential backoff for transient HTTP failures (401/429/5xx)
State Management:
- Access token cache in
src/auth.ts - AI Card caches in
src/card-service.ts(aiCardInstances,activeCardsByTarget) - Unified short-TTL message contexts in
src/message-context-store.tsunder namespacemessages.context - Learned target directory persistence in
src/targeting/target-directory-store.tsunder namespacetargets.directory - Card createdAt fallback keeps an in-memory-only bucket in
src/card-service.tswhen nostorePathis available - Message dedup state in
src/dedup.ts - Runtime stored via getter/setter in
src/runtime.ts
Test File Structure:
- Single test file should stay under 500 lines; files approaching 800+ lines require split planning
- Use
-suffix to split by feature domain:inbound-handler-quote.test.ts,send-service-media.test.ts - Share mock fixtures via
tests/unit/fixtures/for complex multi-file test suites - Each split file should focus on one feature domain with 10-25 tests
- Keep core end-to-end flow tests in the main file; extract sub-feature tests to split files
- Before splitting, analyze test chain for redundancy: merge tests validating same behavior ≥3 times
- Test file naming follows
source-module-{domain}.test.tspattern
Prohibited:
- Sending messages without token retrieval (
getAccessToken) - Creating multiple active AI Cards for same
accountId:conversationId - Hardcoding credentials (must read
channels.dingtalk) - Suppressing type errors with
@ts-ignore - Using
console.log(use logger) - Logging raw sensitive token data
- Re-introducing
quote-journal.ts/quoted-msg-cache.ts-style wrapper persistence layers instead of usingmessage-context-storedirectly
Security:
- Validate
dmPolicy/groupPolicybefore command dispatch - Respect allowlist (
allowFrom) in allowlist modes - Normalize sender IDs (strip
dingtalk:,dd:,ding:prefixes)
AI Card Flow:
- Create card and cache with
PROCESSING - Stream updates with full replacement (
isFull=true) - Transition state to
INPUTINGon first stream - Finalize with
isFinalize=trueandFINISHED - Fallback to markdown send when card stream fails
Reply Delivery Flow:
inbound-handler.tsbuilds reply context and selects a strategy viacreateReplyStrategyreply-strategy-card.tsowns AI Card creation/stream/finalize decisionsreply-strategy-markdown.tshandles markdown/text send fallbackreply-strategy-with-reaction.tswraps strategy execution with reaction lifecycle when enabled
Unified Message Context Flow:
- Inbound messages persist text/media into
messages.contextkeyed by canonicalmsgId - Outbound messages persist after send succeeds using
messageId > processQueryKey > outTrackIdas canonical fallback - Alias lookup supports
messageId,processQueryKey,outTrackId,cardInstanceId, and inboundmsgId - Quote recovery prefers alias lookup and only uses
createdAtwindow as fallback - Old persistence wrappers are removed; production code should call
message-context-storedirectly
Message Processing Pipeline:
- Dedup check by bot-scoped key (
robotKey:msgId) - Filter self-messages
- Extract text/media content
- Authorization check (
dmPolicy/groupPolicy) - Resolve route + session + workspace
- Download media into agent workspace if present
- Persist inbound quote/media context into
messages.context - Dispatch to runtime reply pipeline
- Deliver via selected reply strategy
Media Handling:
- Inbound media saved to
<agent-workspace>/media/inbound - Outbound media uploaded then sent by DingTalk media template messages
- Orphaned temp cleanup at gateway startup
# Type check
npm run type-check
# Lint
npm run lint
# Lint + fix
npm run lint:fix
# Runtime build
pnpm run build:runtime
# Unit + integration tests
pnpm test
# Coverage report (V8)
pnpm test:coverageImportant: OpenClaw real-device debugging loads the runtime extension from dist/index.js.
After applying code changes, always run pnpm run build:runtime before openclaw gateway restart
or any DingTalk real-device validation; otherwise the gateway may keep running stale built code.
OpenClaw Plugin Architecture:
index.tsregistersdingtalkPlugin- Runtime set once via
setDingTalkRuntime(api.runtime) - Multi-account config supported via
channels.dingtalk.accounts displayNameResolutiondefaults todisabled; onlyallenables learned group/user displayName resolution- Message quote/media recovery is unified through
messages.context; no backward-compatible read path exists for removed legacy namespaces
DingTalk API Endpoints Used:
- Token:
/v1.0/oauth2/accessToken - Media download:
/v1.0/robot/messageFiles/download - Proactive send:
/v1.0/robot/groupMessages/send,/v1.0/robot/oToMessages/batchSend - AI Card create+deliver:
/v1.0/card/instances/createAndDeliver - AI Card stream:
/v1.0/card/streaming
Testing:
- Vitest test suite is initialized with unit + integration coverage under
tests/ - Network calls are mocked in tests (
vi.mock), no real DingTalk API requests are made - CI should run
pnpm teston every push and pull request - Coverage can be generated with
pnpm test:coverage - Before applying code changes to a live DingTalk debugging session, run
pnpm run build:runtimeand then restart the gateway sodist/index.jsmatches the source. - When the task involves DingTalk real-device validation, PR-scoped test checklists,
验证 TODOdrafting, or contributor-workflow updates for that process, read and followskills/dingtalk-real-device-testing/SKILL.mdfirst.