Skip to content
Merged
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]

jobs:
test:
name: Java Test
runs-on: ubuntu-latest
strategy:
matrix:
java-version: ["8", "11", "17"]
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: "temurin"
cache: "maven"
- name: Build & Test
run: mvn -B clean test
195 changes: 98 additions & 97 deletions GUIDE.md

Large diffs are not rendered by default.

130 changes: 66 additions & 64 deletions OVERVIEW.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
# 钉钉 Channel SDK 家族 · 项目总览
**English** | [简体中文](./OVERVIEW.zh-CN.md)

> 起点议题:[DingTalk-Real-AI/dingtalk-workspace-cli#796](https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/issues/796)——「钉钉未来会出 Channel 这样的集成 SDK 吗」。
> 本项目给出答案:**四个语言、效果对齐、即拿即用**。
# DingTalk Channel SDK Family · Project Overview

## 1. 产出物
> Origin issue: [DingTalk-Real-AI/dingtalk-workspace-cli#796](https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/issues/796)—"Will DingTalk offer a Channel-like integration SDK in the future?"
> This project provides the answer: **Four languages, effect parity, ready to use**.

| 仓库 | 语言 | 依赖 | 测试 |
## 1. Deliverables

| Repository | Language | Dependencies | Tests |
|---|---|---|---|
| [typefield/dingtalk-channel-sdk-go](https://github.com/typefield/dingtalk-channel-sdk-go) | Go 1.22+ | gorilla/websocket | 19 tests(-race 干净) |
| [typefield/dingtalk-channel-sdk-nodejs](https://github.com/typefield/dingtalk-channel-sdk-nodejs) | Node 18+ | ws | 12 tests |
| [typefield/dingtalk-channel-sdk-python](https://github.com/typefield/dingtalk-channel-sdk-python) | Python 3.10+ | websockets | 12 tests |
| [typefield/dingtalk-channel-sdk-java](https://github.com/typefield/dingtalk-channel-sdk-java) | JDK 8+ | Java-WebSocket + Gson | 12 tests |
| [DingTalk-Real-AI/dingtalk-channel-sdk-go](https://github.com/DingTalk-Real-AI/dingtalk-channel-sdk-go) | Go 1.22+ | gorilla/websocket | 19 tests (-race clean) |
| [DingTalk-Real-AI/dingtalk-channel-sdk-nodejs](https://github.com/DingTalk-Real-AI/dingtalk-channel-sdk-nodejs) | Node 18+ | ws | 12 tests |
| [DingTalk-Real-AI/dingtalk-channel-sdk-python](https://github.com/DingTalk-Real-AI/dingtalk-channel-sdk-python) | Python 3.10+ | websockets | 12 tests |
| [DingTalk-Real-AI/dingtalk-channel-sdk-java](https://github.com/DingTalk-Real-AI/dingtalk-channel-sdk-java) | JDK 8+ | Java-WebSocket + Gson | 12 tests |

每仓含:完整源码、单测、`SPEC.md`(四语言统一契约)、流式 echo 示例、**livecheck 真实联调程序**、README、MIT LICENSE。
Each repository contains: complete source code, unit tests, `SPEC.md` (unified contract across four languages), streaming echo examples, **livecheck real integration program**, README, MIT LICENSE.

## 2. 定位与边界
## 2. Positioning and Boundaries

**与 Agent runtime 解耦的会话接入层**。SDK 负责"通道"的全部脏活,开发者只写「用户说了什么、机器人回什么」:
**A conversation access layer decoupled from Agent runtime**. The SDK handles all the dirty work of the "channel", developers only write "what the user said and what the bot should respond with":

- **负责**:Stream 长连接(建连/心跳/指数退避重连/服务端 disconnect 自愈)、事件双层去重 + 过期消息过滤、sessionWebhook 回复(文本/Markdown/图片,超长自动分片)、AI 卡片流式输出(打字机、帧间隔防竞态、看门狗孤儿保护)、卡片 API 全局限流与 QpsLimit 退避、媒体上传/下载与媒体消息(file/video/audio)、Markdown 归一化、主动发消息(单聊/群聊 + @)、🤔Thinking/🥳Done 状态章、显式中止 Abort、错误兜底冷却
- **不管**:Agent runtime(模型/prompt/工具编排)、会话上下文持久化、凭据存储、业务操作(文档/表格/日历——dws CLI 与 skills 的领域)
- **Responsible for**: Stream long connection (connect/heartbeat/exponential backoff reconnection/server disconnect recovery), dual-layer event deduplication + expired message filtering, sessionWebhook replies (text/Markdown/image, auto-chunking for oversized content), AI card streaming output (typewriter effect, inter-frame interval race protection, watchdog orphan protection), card API global rate limiting and QpsLimit backoff, media upload/download and media messages (file/video/audio), Markdown normalization, proactive messaging (DM/group + @), 🤔Thinking/🥳Done status badges, explicit abort, error fallback cooldown
- **Not responsible for**: Agent runtime (model/prompt/tool orchestration), conversation context persistence, credential storage, business operations (docs/spreadsheets/calendar—domain of dws CLI and skills)

## 3. 快速上手(四语言同构)
## 3. Quick Start (Four Languages Isomorphic)

```go
ch := channel.New(channel.Config{ClientID: "ding...", ClientSecret: "..."})
ch.OnMessage(func(ctx context.Context, msg *channel.IncomingMessage, reply channel.Reply) error {
s, _ := reply.Stream(ctx) // 秒级出"输入中"卡片
s, _ := reply.Stream(ctx) // "Typing" card appears in seconds
for _, tok := range myLLM(msg.Text) {
_ = s.Append(tok) // 打字机追加(800ms 节流+trailing flush)
_ = s.Append(tok) // Typewriter append (800ms throttle + trailing flush)
}
return s.Finish("") // 终帧定格
return s.Finish("") // Final frame finalization
})
ch.Start(ctx)
```
Expand All @@ -46,64 +48,64 @@ async def handle(msg, reply): s = await reply.stream(); await s.append(tok); awa
ch.onMessage((msg, reply) -> { CardStreamer s = reply.stream(); s.append(tok); s.finish(""); });
```

非流式:`reply.Text/Markdown/Image`、附件下载 `reply.DownloadURL`、媒体上传 `reply.UploadMedia`;
主动发消息(不依赖入站):`ch.SendText/SendMarkdown/SendImage`,群发支持 `AtUserIds/AtAll`;
卡片交互:`ch.OnCardAction`(注册即自动订阅 card topic)。
Non-streaming: `reply.Text/Markdown/Image`, attachment download `reply.DownloadURL`, media upload `reply.UploadMedia`;
Proactive messaging (independent of inbound): `ch.SendText/SendMarkdown/SendImage`, group messaging supports `AtUserIds/AtAll`;
Card interaction: `ch.OnCardAction` (registration automatically subscribes to card topic).

## 4. 效果对齐(验收清单 E1–E10,见 SPEC §0)
## 4. Effect Parity (Acceptance Checklist E1–E10, see SPEC §0)

| | 用户可见效果 | 实现 |
| | User-Visible Effect | Implementation |
|---|---|---|
| E1 | 发消息秒级出"输入中"卡片 | `stream()` 立即建卡+投递 INPUTING |
| E2 | 打字机平滑追加 | streaming 接口 + 800ms 节流 + **trailing flush**(窗口内不丢)+ 长间隔 300ms 攒批 |
| E3 | 完成后 loading 消失、Markdown 定格 | isFinalize 终帧 + FINISHED 状态 |
| E4 | 卡片失败/限流用户无感 | 静默降级 webhook 文本;QpsLimit 退避 2s 重试 |
| E5 | 群聊/单聊同一体验 | 同一 Reply API;投递目标自动选;群聊剥 @ 前缀 |
| E6 | 绝不重复回复 | messageId+msgId 双层去重(TTL 5min) |
| E7 | 卡片交互闭环 | OnCardAction + 自动订阅 |
| E8 | 永不掉线 | 指数退避重连 + disconnect 即时重连 + 120s/5s 心跳 + ACK 先行 |
| E9 | 媒体收发 | uploadMedia(OAPI multipart)/ downloadURL / image 回复 |
| E10 | Markdown 渲染质量 | normalizeForCard(代码块/表格/引用钉钉渲染规则) |

每条在四语言均有对应单测;E8 另有专门的断线重连 e2e 回归。

## 5. 协议保真(真源,非文档臆测)

| 能力 | 真源 |
| E1 | "Typing" card appears seconds after sending message | `stream()` immediately creates card + delivers INPUTING |
| E2 | Smooth typewriter append | streaming interface + 800ms throttle + **trailing flush** (no drops within window) + long interval 300ms batching |
| E3 | Loading disappears after completion, Markdown finalizes | isFinalize final frame + FINISHED status |
| E4 | Card failure/rate limiting transparent to user | Silent fallback to webhook text; QpsLimit backoff 2s retry |
| E5 | Same experience in group/DM | Same Reply API; delivery target auto-selected; group strips @ prefix |
| E6 | Never duplicate replies | messageId+msgId dual-layer deduplication (TTL 5min) |
| E7 | Card interaction closed loop | OnCardAction + auto-subscribe |
| E8 | Never disconnects | Exponential backoff reconnection + disconnect immediate reconnection + 120s/5s heartbeat + ACK first |
| E9 | Media send/receive | uploadMedia (OAPI multipart) / downloadURL / image reply |
| E10 | Markdown rendering quality | normalizeForCard (code blocks/tables/quotes DingTalk rendering rules) |

Each item has corresponding unit tests in all four languages; E8 has dedicated disconnect reconnection e2e regression.

## 5. Protocol Fidelity (Ground Truth, Not Documentation Speculation)

| Capability | Ground Truth Source |
|---|---|
| Stream 线协议(open/wss/帧/ACK/心跳/topic 常量) | 官方 dingtalk-stream-sdk-go 源码逐行对照 |
| AI 卡片五步协议 + 限流 + Markdown 归一化 | 官方 connector(dingtalk-openclaw-connector)card.ts |
| token(新版/OAPI 双轨)与 sessionWebhook 载荷 | 官方 connector token.ts / messaging.ts + 官方文档校验 |
| 主动发消息 API | dws(dingtalk-workspace-cli)源码 |
| ticket 编码 / localIp / UA 头 | 四门官方 stream SDK 交叉对照取齐 |
| Stream wire protocol (open/wss/frames/ACK/heartbeat/topic constants) | Official dingtalk-stream-sdk-go source code line-by-line comparison |
| AI card five-step protocol + rate limiting + Markdown normalization | Official connector (dingtalk-openclaw-connector) card.ts |
| Token (new version/OAPI dual-track) and sessionWebhook payload | Official connector token.ts / messaging.ts + official documentation validation |
| Proactive messaging API | dws (dingtalk-workspace-cli) source code |
| Ticket encoding / localIp / UA headers | Cross-referenced across all four official stream SDKs |

Review 轮次中修复的协议级问题:msgParam 字符串化 JSON(官方文档要求)、Go 版 disconnect 误停、ACK 先行语义、ticket URL 编码。
Protocol-level issues fixed during review rounds: msgParam JSON stringification (official documentation requirement), Go disconnect mis-stop, ACK first semantics, ticket URL encoding.

## 6. 关键架构决策:为什么自研传输层而非引用官方 stream-sdk
## 6. Key Architectural Decision: Why Build Custom Transport Layer Instead of Using Official stream-sdk

1. **官方 connector 自己都不信任**:钉钉官方 connector 源码 `autoReconnect:false, keepAlive:false` 全关重写(issue #571/#536/#573)
2. **四语言一致是本项目验收标准**,而官方四门 SDK 心跳/重连/编码行为互不一致,引用即继承分歧
3. **依赖重量**:官方 Java 版引 Netty 多模块、Python 版带 requests+aiohttp 双 HTTP 栈;自研每语言仅 1–2 个小依赖,传输层约 300 行/语言
4. 留有演进接缝(`Channel → StreamConn → onFrame` 单一边界),上游 SDK 成熟后可加官方适配后端
1. **Official connector doesn't trust itself**: DingTalk official connector source code has `autoReconnect:false, keepAlive:false` all disabled and rewritten (issues #571/#536/#573)
2. **Four-language consistency is this project's acceptance standard**, while official four SDKs have inconsistent heartbeat/reconnection/encoding behaviors; referencing them inherits divergence
3. **Dependency weight**: Official Java version pulls Netty multi-modules, Python version includes requests+aiohttp dual HTTP stack; custom implementation only 1–2 small dependencies per language, transport layer ~300 lines/language
4. Leaves evolution seam (`Channel → StreamConn → onFrame` single boundary), official SDK adapter backend can be added later when upstream matures

## 7. 质量证据
## 7. Quality Evidence

- **测试**:Go 14 / Node 12 / Python 12 / Java 12(BUILD SUCCESS),全部含 e2e(假网关+假 API)与断线重连回归
- **真实联调**:每语言 `example/livecheck` 一键验证(连接→收消息→文本回复→卡片流式全周期→媒体上传,逐步 PASS/FAIL):
`DD_CLIENT_ID=... DD_CLIENT_SECRET=... go run ./example/livecheck`(Node `npm run live`;Python `python example/livecheck.py`;Java `mvn exec:java`)
- **Code review**:三轮(协议一致性 / 效果对齐 / stream 层对官方 SDK),修复 8 处问题,全部有回归测试
- **Tests**: Go 14 / Node 12 / Python 12 / Java 12 (BUILD SUCCESS), all include e2e (mock gateway + mock API) and disconnect reconnection regression
- **Real integration**: Each language's `example/livecheck` one-command verification (connection→receive message→text reply→card streaming full cycle→media upload, progressive PASS/FAIL):
`DD_CLIENT_ID=... DD_CLIENT_SECRET=... go run ./example/livecheck` (Node `npm run live`; Python `python example/livecheck.py`; Java `mvn exec:java`)
- **Code review**: Three rounds (protocol consistency / effect parity / stream layer vs official SDK), fixed 8 issues, all have regression tests

## 8. 已知边界与路线图
## 8. Known Boundaries and Roadmap

- 真实凭据联跑待执行(livecheck 就绪,一条命令)
- >20MB 文件分块上传(v0.2)
- 卡片模板默认值为 connector 内置模板,对外发布建议配置化强调
- 平台差异项(reaction/评论/转发)随钉钉开放平台演进跟进
- 可选:官方 stream-sdk 适配后端(`WithTransport` 接缝已留)
- Real credential live testing to be executed (livecheck ready, one command)
- >20MB file chunked upload (v0.2)
- Card template default value is connector built-in template, recommended to make it configurable for public release
- Platform differences (reaction/comment/forward) will follow DingTalk Open Platform evolution
- Optional: Official stream-sdk adapter backend (seam already left via `WithTransport`)

## 9. 前置条件
## 9. Prerequisites

钉钉开发者后台创建**企业内部应用**并开启机器人,取得 ClientID/ClientSecret。Stream 模式无需公网 IP 与域名。
Create **Enterprise Internal Application** in DingTalk Developer Console and enable bot, obtain ClientID/ClientSecret. Stream mode requires no public IP or domain.

---
License:MIT | 契约:各仓 `SPEC.md` | 版本:v0.1.0(2026-08)
License: MIT | Contract: `SPEC.md` in each repo | Version: v0.1.0 (Aug 2026)
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Requires JDK 8+. Dependencies: Java-WebSocket and Gson only.

```xml
<dependency>
<groupId>io.github.typefield</groupId>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-channel-sdk</artifactId>
<version>0.1.0</version>
</dependency>
Expand Down Expand Up @@ -69,7 +69,7 @@ s.finish("");
Application code typically imports only the root package:

```java
import io.github.typefield.dingtalk.channel.DingTalkChannel;
import com.dingtalk.channel.DingTalkChannel;
```

Sub-packages (`normalize` / `safety` / `outbound`) are internal layering and carry no compatibility promise.
Expand Down Expand Up @@ -98,7 +98,7 @@ Live check:

```bash
DD_CLIENT_ID=... DD_CLIENT_SECRET=... \
mvn -q compile exec:java -Dexec.mainClass=io.github.typefield.dingtalk.channel.example.LiveCheck
mvn -q compile exec:java -Dexec.mainClass=com.dingtalk.channel.example.LiveCheck
```

## License
Expand Down
6 changes: 3 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

```xml
<dependency>
<groupId>io.github.typefield</groupId>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-channel-sdk</artifactId>
<version>0.1.0</version>
</dependency>
Expand Down Expand Up @@ -69,7 +69,7 @@ s.finish("");
业务代码通常只需导入根包:

```java
import io.github.typefield.dingtalk.channel.DingTalkChannel;
import com.dingtalk.channel.DingTalkChannel;
```

子包(`normalize` / `safety` / `outbound`)属内部分层,不在兼容性承诺范围内。
Expand Down Expand Up @@ -98,7 +98,7 @@ mvn test # 34 个测试

```bash
DD_CLIENT_ID=... DD_CLIENT_SECRET=... \
mvn -q compile exec:java -Dexec.mainClass=io.github.typefield.dingtalk.channel.example.LiveCheck
mvn -q compile exec:java -Dexec.mainClass=com.dingtalk.channel.example.LiveCheck
```

## License
Expand Down
Loading