Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/src/channels/dingtalk.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ client_id = "your-app-key"
client_secret = "your-app-secret"
bot_title = "Klaw"
show_reasoning = false
stream_output = false
stream_template_id = ""
stream_content_key = "content"
stream_reasoning_key = "reasoning"
allowlist = ["USER123", "*"]

[channels.dingtalk.proxy]
Expand All @@ -51,14 +55,21 @@ url = "http://proxy.example.com:8080"
| `client_secret` | `string` | 是 | 钉钉应用 AppSecret | - |
| `bot_title` | `string` | 否 | 机器人显示名称 | `"Klaw"` |
| `show_reasoning` | `bool` | 否 | 响应中是否展示推理过程 | `false` |
| `stream_output` | `bool` | 否 | 是否使用钉钉 AI 卡片模板流式刷新回复 | `false` |
| `stream_template_id` | `string` | 条件必填 | `stream_output=true` 时使用的钉钉 AI 卡片模板 ID | `""` |
| `stream_content_key` | `string` | 条件必填 | AI 卡片模板中承载正文的变量名 | `"content"` |
| `stream_reasoning_key` | `string` | 条件必填 | AI 卡片模板中承载 reasoning 的变量名 | `"reasoning"` |
| `allowlist` | `string[]` | 否 | 发送者白名单(`*` 表示允许所有) | `[]`(允许所有) |
| `proxy.enabled` | `bool` | 否 | 是否启用代理 | `false` |
| `proxy.url` | `string` | 条件必填 | 代理地址 | - |

配置校验:
- `client_id` 和 `client_secret` 不能为空
- `stream_output=true` 时 `stream_template_id`、`stream_content_key`、`stream_reasoning_key` 均不能为空,且正文变量名不能与 reasoning 变量名相同
- `proxy.enabled=true` 时 `proxy.url` 必填

流式 AI 卡片模板需要在钉钉侧声明 `stream_content_key` 与 `stream_reasoning_key` 对应的变量。开启 `show_reasoning` 时,正文变量只刷新模型正文,reasoning 变量按全量覆盖方式刷新模型返回的 reasoning,避免正文与独立 reasoning 区块重复展示。

## 连接建立流程

DingTalk 渠道使用 WebSocket 长连接协议,连接建立流程如下:
Expand Down
54 changes: 54 additions & 0 deletions docs/src/configration/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ client_id = "your-app-key"
client_secret = "your-app-secret"
bot_title = "Klaw"
show_reasoning = false
stream_output = false
stream_template_id = ""
stream_content_key = "content"
stream_reasoning_key = "reasoning"
allowlist = ["USER123", "*"]
```

Expand Down Expand Up @@ -442,6 +446,56 @@ channels.dingtalk.bot_title = "我的助手"
channels.dingtalk.show_reasoning = true
```

当 `stream_output=true` 且钉钉 AI 卡片模板包含独立 reasoning 变量时,开启该项会把模型流式返回的 reasoning 写入 `stream_reasoning_key` 指定的模板变量;正文变量不会再重复拼接 reasoning 块。

#### `channels.dingtalk[].stream_output`

**类型**: `boolean`
**默认值**: `false`
**必填**: 否

是否允许 DingTalk 渠道使用 AI 卡片模板流式刷新回复。

```toml
channels.dingtalk.stream_output = true
```

#### `channels.dingtalk[].stream_template_id`

**类型**: `string`
**默认值**: `""`
**必填**: `stream_output=true` 时必填

钉钉 AI 卡片模板 ID。

```toml
channels.dingtalk.stream_template_id = "template-1.schema"
```

#### `channels.dingtalk[].stream_content_key`

**类型**: `string`
**默认值**: `"content"`
**必填**: `stream_output=true` 时必填

钉钉 AI 卡片模板中承载正文内容的变量名。

```toml
channels.dingtalk.stream_content_key = "content"
```

#### `channels.dingtalk[].stream_reasoning_key`

**类型**: `string`
**默认值**: `"reasoning"`
**必填**: `stream_output=true` 时必填

钉钉 AI 卡片模板中承载 reasoning 内容的变量名。该变量会用全量覆盖方式刷新,适合每轮 reasoning 不同的展示块;不能与 `stream_content_key` 相同。

```toml
channels.dingtalk.stream_reasoning_key = "reasoning"
```

#### `channels.dingtalk[].allowlist`

**类型**: `array<string>`
Expand Down
14 changes: 14 additions & 0 deletions klaw-channel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# CHANGELOG

## 2026-04-27

### Changed

- `dingtalk` AI 卡片流式输出在开启 `show_reasoning` 时会把正文与 reasoning 拆分到独立模板变量,并对 reasoning 变量执行全量刷新,避免正文重复拼接 reasoning 块

### Fixed

- `dingtalk` AI 卡片流式输出不再对初始空正文或空 reasoning 调用 streaming update,避免触发钉钉接口的空 `content` 错误;清空已有 reasoning 时改用非空占位内容刷新模板变量
- `dingtalk` AI 卡片流式输出在单次更新内容超过钉钉 streaming 接口限制时会提前停止流式卡片并回退最终 Markdown 回复,避免接口返回 `unknownError`
- `dingtalk` AI 卡片创建 payload 恢复为只初始化正文变量,reasoning 变量仅通过后续 streaming update 写入,避免额外初始变量影响既有 `content` 流式通道
- `dingtalk` AI 卡片流式更新现在会在发送前补齐未闭合的 Markdown fenced code block,避免中间帧 Markdown 语法不完整时钉钉服务端转换失败
- `dingtalk` AI 卡片收尾时若同时刷新正文与 reasoning,现在只让最后一个 streaming key 携带 `isFinalize=true`,避免先 finalize 正文导致后续 reasoning 流仍未收尾时卡片状态异常

## 2026-04-22

### Changed
Expand Down
14 changes: 11 additions & 3 deletions klaw-channel/src/dingtalk/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,18 @@ pub(super) fn build_proactive_markdown_payload(
}

pub(super) fn build_ai_card_card_data(content_key: &str, content: &str) -> Value {
build_ai_card_card_data_from_pairs([(content_key, content)])
}

pub(super) fn build_ai_card_card_data_from_pairs<'a>(
pairs: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Value {
let card_param_map = pairs
.into_iter()
.map(|(key, content)| (key.trim().to_string(), Value::String(content.to_string())))
.collect::<serde_json::Map<_, _>>();
serde_json::json!({
"cardParamMap": {
content_key.trim(): content,
}
"cardParamMap": card_param_map
})
}

Expand Down
2 changes: 2 additions & 0 deletions klaw-channel/src/dingtalk/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct DingtalkChannelConfig {
pub stream_output: bool,
pub stream_template_id: String,
pub stream_content_key: String,
pub stream_reasoning_key: String,
pub allowlist: Vec<String>,
pub local_attachments: LocalAttachmentConfig,
pub proxy: DingtalkProxyConfig,
Expand All @@ -31,6 +32,7 @@ impl Default for DingtalkChannelConfig {
stream_output: false,
stream_template_id: String::new(),
stream_content_key: "content".to_string(),
stream_reasoning_key: "reasoning".to_string(),
allowlist: Vec::new(),
local_attachments: LocalAttachmentConfig::default(),
proxy: DingtalkProxyConfig::default(),
Expand Down
Loading