@jeik/dingtalk-connector 0.8.21-fix46 → 0.8.21-fix48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.8.21-fix48] - 2026-07-21
9
+
10
+ ### 新增 / Added
11
+ - ✨ **`messageAnswerCard`(默认 `true`)** — message 工具正文默认走**答案静态卡**(`answerCardTemplateId` 或内置模板,`skipInputingWalk`);失败降级普通消息。显式 `false` 关闭。
12
+ 已注册:`schema.ts`、`openclaw.plugin.json`(channels + accounts + uiHints)。
13
+ 与会话流式 `answerCard` 独立;图/音/视/文件仍走普通通道。
14
+
15
+ ### 安装 / Install
16
+ ```bash
17
+ npx -y @jeik/dingtalk-connector install --force
18
+ openclaw gateway restart
19
+ ```
20
+
21
+ ## [0.8.21-fix47] - 2026-07-20
22
+
23
+ ### 修复 / Fixed
24
+ - 🐛 **`file://` 本地图被误判为远程** — `looksLikeRemoteUrl` 对含 `://` 一律当远程,导致 `![x](file:///mnt/...)` / `file:///root/...` 走 `fetch` → `fetch failed`,原样发出灰图。现:`file://` / `MEDIA:` / `attachment://` **优先按本地上传**(`toLocalPath` 后 oapi)。
25
+ 日志特征(fix46):`kind=http path=file://...` + `远程图下载异常 | fetch failed` + `localTried=0`。
26
+
27
+ ### 安装 / Install
28
+ ```bash
29
+ npx -y @jeik/dingtalk-connector install --force
30
+ openclaw gateway restart
31
+ ```
32
+
8
33
  ## [0.8.21-fix46] - 2026-07-20
9
34
 
10
35
  ### 诊断 / Diagnostics
package/README.en.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <p>Community maintained fork of the official <strong>v0.8.20</strong> release, tracking and fixing bugs the official team hasn't addressed.<br/>
5
5
  Identical to the official release in functionality — only community-critical fixes applied.</p>
6
6
 
7
- <p><strong>Current published release: <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-fix46</strong> (production-stable; install: <code>npx -y @jeik/dingtalk-connector install</code>).</p>
7
+ <p><strong>Current published release: <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-fix48</strong> (production-stable; install: <code>npx -y @jeik/dingtalk-connector install</code>).</p>
8
8
 
9
9
  <p>
10
10
  <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector"><img src="https://img.shields.io/npm/v/@jeik/dingtalk-connector.svg?style=flat&colorA=18181B&colorB=28CF8D" alt="npm version" /></a>
@@ -23,19 +23,28 @@
23
23
 
24
24
  ## 🔧 Recent Updates
25
25
 
26
- ### 🚀 v0.8.21-fix46 · 2026-07-20 (current)
26
+ ### 🚀 v0.8.21-fix48 · 2026-07-21 (current)
27
27
 
28
- **Theme: always-on local MD image diagnostics (behavior same as fix45)**
28
+ **Theme: message tool answer card by default (toggle)**
29
29
 
30
30
  | | Change |
31
31
  |--|--------|
32
- | 🔍 **LocalImage logs** | message path: scan / exists / upload / residual |
33
- | 🔍 **MediaIdTrace** | `mdImgs`, `residualLocal`, API-before preview |
32
+ | ✨ **`messageAnswerCard`** | **Default `true`**: message body uses static answer card; set `false` for plain text/markdown |
33
+ | 🔧 **Registered** | `schema` + `openclaw.plugin.json` (channels / accounts / uiHints) |
34
+ | 📎 **Independent** | Separate from session-stream `answerCard`; media still uses normal APIs |
34
35
 
35
36
  ```bash
36
37
  npx -y @jeik/dingtalk-connector install --force && openclaw gateway restart
37
38
  ```
38
39
 
40
+ ### 📦 v0.8.21-fix47 · 2026-07-20
41
+
42
+ `file://` no longer misclassified as remote (grey-image fix).
43
+
44
+ ### 📦 v0.8.21-fix46 · 2026-07-20
45
+
46
+ Always-on local MD image diagnostics.
47
+
39
48
  ### 📦 v0.8.21-fix45 · 2026-07-20
40
49
 
41
50
  Image + download link one bubble · only `![]` · quote cache · residual false-positive fix.
@@ -46,14 +55,7 @@ Remote `media` download-then-upload.
46
55
 
47
56
  ### 📦 v0.8.21-fix37 · 2026-07-20
48
57
 
49
- **Theme: local images (incl. `/mnt`) · message media layout · diagnostics**
50
-
51
- | | Change |
52
- |--|--------|
53
- | 📷 **Local MD images** | Any absolute path (incl. **`/mnt`**, Chinese paths); upload retries via `/tmp` copy |
54
- | 📦 **Code-block safe** | Paths inside fenced/inline code are **not** uploaded (param samples stay literal) |
55
- | ⚙️ **`messageImageMd`** | Default `false`: message tool sends **text then image** separately; `true` merges only multi-image + text into one markdown |
56
- | 🔍 **Diagnostics** | Prefix `[DingTalk][LocalImage]` — **no `debug` flag required** |
58
+ Local images (incl. `/mnt`) · `messageImageMd` · LocalImage diagnostics.
57
59
 
58
60
  ---
59
61
 
@@ -66,7 +68,7 @@ Remote `media` download-then-upload.
66
68
  | 2026-06-28 | npm `@jeik/dingtalk-connector`; premature finalization fix |
67
69
  | 2026-05 | MD images; multi-turn spam; 4.29 empty reply; WS phantom reconnect |
68
70
 
69
- Full log: [CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release fix38](docs/RELEASE_NOTES_V0.8.21-fix38.md)
71
+ Full log: [CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release fix48](docs/RELEASE_NOTES_V0.8.21-fix48.md)
70
72
 
71
73
  ---
72
74
 
@@ -90,20 +92,96 @@ Full log: [CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release fix38
90
92
  "clientId": "your-clientId",
91
93
  "clientSecret": "your-clientSecret",
92
94
  "cardTemplateId": "your-card-template-id.schema",
93
- "cardContentVar": "content"
95
+ "cardContentVar": "content",
96
+ "messageAnswerCard": true
94
97
  }
95
98
  }
96
99
  ```
97
100
 
98
- | Parameter | Description |
99
- |-----------|-------------|
100
- | `cardTemplateId` | AI Card template ID, uses official default if not set |
101
- | `cardContentVar` | Card content variable (process / tool line / final all write here), defaults to `msgContent` |
102
- | `answerCard` | Answer-card mode switch, **on by default**; set `false` to disable |
103
- | `answerActToken` | Answer-card trigger threshold (tokens), default `500`; final answer ≤ this stays on the original card, > this opens a separate answer card |
104
- | `answerCardTemplateId` | Answer-card template ID, uses the built-in default if not set (must contain a `content` variable) |
105
-
106
- > Card template must be created in [DingTalk Open Platform](https://open.dingtalk.com/) with matching variable fields.
101
+ ### Full config reference (`channels.dingtalk-connector`)
102
+
103
+ Source of truth: `src/config/schema.ts` + `openclaw.plugin.json`.
104
+ Top-level single-bot fields and `accounts.<id>` share the same **shared shape**; missing account fields inherit top-level.
105
+
106
+ #### 1. Enable / credentials / multi-account
107
+
108
+ | Field | Type | Default | Description |
109
+ |-------|------|---------|-------------|
110
+ | `enabled` | boolean | — | Enable this channel |
111
+ | `defaultAccount` | string | — | Default account key (must exist under `accounts`) |
112
+ | `clientId` | string \| number | — | AppKey / Client ID (single-bot top-level) |
113
+ | `clientSecret` | string \| SecretRef | — | AppSecret; plain string or `{ source, provider, id }` |
114
+ | `accounts` | object | — | Multi-bot map; keys are account IDs |
115
+ | `accounts.*.enabled` | boolean | — | Enable this account |
116
+ | `accounts.*.name` | string | — | Display name |
117
+ | `accounts.*.clientId` | string \| number | — | Account Client ID |
118
+ | `accounts.*.clientSecret` | string \| SecretRef | — | Account secret |
119
+ | `accounts.*.chatbotUserId` | string | — | Encrypted bot id for @-mentioning bots in groups (see `[BotIdentity]` logs) |
120
+ | `accounts.*.chatbotCorpId` | string | — | Bot corp id |
121
+
122
+ #### 2. Access policy
123
+
124
+ | Field | Type | Default | Description |
125
+ |-------|------|---------|-------------|
126
+ | `dmPolicy` | `"open"` \| `"pairing"` \| `"allowlist"` | `"open"` | DM policy |
127
+ | `allowFrom` | (string\|number)[] | — | DM allowlist; **required (≥1)** when `dmPolicy=allowlist` |
128
+ | `groupPolicy` | `"open"` \| `"allowlist"` \| `"disabled"` | `"open"` | Group policy |
129
+ | `groupAllowFrom` | (string\|number)[] | — | Group allowlist; **required (≥1)** when `groupPolicy=allowlist` |
130
+ | `requireMention` | boolean | `true` | Require @bot in groups |
131
+ | `groups` | object | — | Per-group overrides (below) |
132
+
133
+ **`groups.<openConversationId>`:**
134
+
135
+ | Field | Type | Description |
136
+ |-------|------|-------------|
137
+ | `enabled` | boolean | Enable this group |
138
+ | `requireMention` | boolean | Override @ requirement |
139
+ | `allowFrom` | (string\|number)[] | Per-group sender allowlist |
140
+ | `systemPrompt` | string | Extra system prompt for this group |
141
+ | `groupSessionScope` | `"group"` \| `"group_sender"` | Session scope for this group |
142
+ | `tools.allow` / `tools.deny` | string[] | Per-group tool policy |
143
+
144
+ #### 3. Session / behavior
145
+
146
+ | Field | Type | Default | Description |
147
+ |-------|------|---------|-------------|
148
+ | `groupSessionScope` | `"group"` \| `"group_sender"` | `"group"` | One session per group vs per group+sender |
149
+ | `separateSessionByConversation` | boolean | `true` | Isolate sessions by conversation |
150
+ | `sharedMemoryAcrossConversations` | boolean | `false` | Share memory across conversations |
151
+ | `historyLimit` | int ≥0 | — | History length cap |
152
+ | `textChunkLimit` | int >0 | — | Text chunk size cap |
153
+ | `mediaMaxMb` | number >0 | — | Media size cap (MB) |
154
+ | `typingIndicator` | boolean | — | Typing indicator |
155
+ | `resolveSenderNames` | boolean | — | Resolve sender display names |
156
+ | `asyncMode` | boolean | — | Async mode |
157
+ | `ackText` | string | — | Inbound ACK text |
158
+ | `systemPrompt` | string | — | Channel-level extra system prompt |
159
+ | `enableMediaUpload` | boolean | — | Enable media upload features |
160
+ | `endpoint` | string | — | Custom DWClient gateway (advanced) |
161
+ | `debug` | boolean | — | Debug logging |
162
+
163
+ #### 4. Tools
164
+
165
+ | Field | Type | Default | Description |
166
+ |-------|------|---------|-------------|
167
+ | `tools.docs` | boolean | true (semantic) | Document tools |
168
+ | `tools.media` | boolean | true (semantic) | Media upload tools |
169
+
170
+ #### 5. Group reply / AI card / answer card / message tool
171
+
172
+ | Field | Type | Default | Description |
173
+ |-------|------|---------|-------------|
174
+ | `groupReplyMode` | `"aicard"` \| `"text"` \| `"markdown"` | `"aicard"` | Group reply style; `text`/`markdown` disables streaming AI card in groups (multi-bot @) |
175
+ | `cardTemplateId` | string | official stream template | Streaming AI Card template id |
176
+ | `cardContentVar` | string | `"msgContent"` | Stream card content variable (process/tool/final) |
177
+ | `answerCard` | boolean | **true** (unless `false`) | **Session-stream** answer card: long answers on a static card; original card freezes at “thinking done” |
178
+ | `answerActToken` | int >0 | **500** | Token threshold: ≤ stay on stream card; > spawn answer card |
179
+ | `answerCardTemplateId` | string | built-in answer template | Static answer card template id |
180
+ | `messageAnswerCard` | boolean | **true** (unless `false`) | **message tool** body via static answer card; `false` = plain text/markdown. **Independent** of `answerCard`; media still normal APIs |
181
+ | `messageImageMd` | boolean | **false** | message tool layout: `false` text then image; `true` merge multi-image+text markdown |
182
+
183
+ > Create/publish card templates in [DingTalk Open Platform](https://open.dingtalk.com/).
184
+ > `answerCard` = conversational stream finalize; `messageAnswerCard` = Agent `message` tool outbound. Do not conflate them.
107
185
 
108
186
  ---
109
187
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <p>基于官方 <strong>v0.8.20</strong> 的社区维护版本,由社区持续跟进修复官方无暇处理的 Bug。<br/>
5
5
  功能与官方完全一致,拥有最快的修复速度,及时合并官方pr和个人发现的bug和社区急需的 Bug。</p>
6
6
 
7
- <p><strong>当前发布版:<a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-fix46</strong>(稳定生产可用;一键安装:`npx -y @jeik/dingtalk-connector install`;本地 tgz:`openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix46.tgz --force`)</p>
7
+ <p><strong>当前发布版:<a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-fix48</strong>(稳定生产可用;一键安装:`npx -y @jeik/dingtalk-connector install`;本地 tgz:`openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix48.tgz --force`)</p>
8
8
 
9
9
  <p>
10
10
  <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector"><img src="https://img.shields.io/npm/v/@jeik/dingtalk-connector.svg?style=flat&colorA=18181B&colorB=28CF8D" alt="npm version" /></a>
@@ -23,19 +23,28 @@
23
23
 
24
24
  ## 🔧 最近更新
25
25
 
26
- ### 🚀 v0.8.21-fix46 · 2026-07-20(当前)
26
+ ### 🚀 v0.8.21-fix48 · 2026-07-21(当前)
27
27
 
28
- **主题:本地 MD 图灰图诊断日志(逻辑同 fix45)**
28
+ **主题:message 工具默认答案卡(可关)**
29
29
 
30
30
  | | 改动 |
31
31
  |--|------|
32
- | 🔍 **LocalImage 日志** | message 路径 always-on:扫描 / exists / 上传成败 / residual |
33
- | 🔍 **MediaIdTrace** | `mdImgs`、`residualLocal`、API前 preview,方便对灰图 |
32
+ | ✨ **`messageAnswerCard`** | **默认 `true`**:message 正文走答案静态卡;`false` 恢复普通 text/markdown |
33
+ | 🔧 **注册** | `schema` + `openclaw.plugin.json`(channels / accounts / uiHints) |
34
+ | 📎 **独立** | 与会话流式 `answerCard` 分开;媒体消息仍普通通道 |
34
35
 
35
36
  ```bash
36
37
  npx -y @jeik/dingtalk-connector install --force && openclaw gateway restart
37
38
  ```
38
39
 
40
+ ### 📦 v0.8.21-fix47 · 2026-07-20
41
+
42
+ `file://` 本地图误判远程修复(不再 `fetch failed` 灰图)。
43
+
44
+ ### 📦 v0.8.21-fix46 · 2026-07-20
45
+
46
+ 本地 MD 图灰图诊断日志(LocalImage / MediaIdTrace)。
47
+
39
48
  ### 📦 v0.8.21-fix45 · 2026-07-20
40
49
 
41
50
  图 + 下载链接单气泡 · 只认 `![]` · 引用卡片缓存 · residual 误报修复。
@@ -46,14 +55,7 @@ message 远程 `media` 下载上传。
46
55
 
47
56
  ### 📦 v0.8.21-fix37 · 2026-07-20
48
57
 
49
- **主题:本地图(含 `/mnt`)· message 文图策略 · 可观测日志**
50
-
51
- | | 改动 |
52
- |--|------|
53
- | 📷 **MD 本地图** | 支持任意绝对路径(含 **`/mnt` 共享盘**、中文目录);上传失败会拷到 `/tmp` 再试 |
54
- | 📦 **代码块保护** | 围栏 / 行内 code 里的路径**不会上传**,参数示例不会被改成 `mediaId` |
55
- | ⚙️ **`messageImageMd`** | 默认 `false`:message 工具**文图分开**;`true` 时仅「多图 + 文字」合并为一条 markdown |
56
- | 🔍 **诊断日志** | 前缀 `[DingTalk][LocalImage]`,**无需开 debug** 也能看到匹配 / 上传 / 失败原因 |
58
+ 本地图(含 `/mnt`)· `messageImageMd` · LocalImage 诊断日志。
57
59
 
58
60
  ---
59
61
 
@@ -66,7 +68,7 @@ message 远程 `media` 下载上传。
66
68
  | 2026-06-28 | 上线 npm `@jeik/dingtalk-connector`;过程消息提前定稿修复 |
67
69
  | 2026-05 | MD 直链/本地图;多轮刷屏;4.29+ 无文本输出;WebSocket 幻影重连 |
68
70
 
69
- 完整说明:[CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release fix46](docs/RELEASE_NOTES_V0.8.21-fix46.md)
71
+ 完整说明:[CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release fix48](docs/RELEASE_NOTES_V0.8.21-fix48.md)
70
72
 
71
73
  ---
72
74
 
@@ -96,6 +98,7 @@ message 远程 `media` 下载上传。
96
98
  "cardTemplateId": "你的卡片模板ID.schema",
97
99
  "cardContentVar": "content",
98
100
  "messageImageMd": false,
101
+ "messageAnswerCard": true,
99
102
  "debug": false
100
103
  }
101
104
  }
@@ -129,21 +132,90 @@ message 远程 `media` 下载上传。
129
132
  }
130
133
  ```
131
134
 
132
- | 参数 | 说明 |
133
- |------|------|
134
- | `clientId` / `clientSecret` | 单机器人模式直接填在顶层 |
135
- | `accounts` | 多机器人模式,key 为账号标识名(可任意命名) |
136
- | `accounts.*.enabled` | 是否启用该账号 |
137
- | `accounts.*.name` | 账号显示名称(仅用于标识) |
138
- | `accounts.*.clientId` | 钉钉应用 ClientId |
139
- | `accounts.*.clientSecret` | 钉钉应用 ClientSecret |
140
- | `cardTemplateId` | AI Card 模板 ID,不填则使用官方默认模板 |
141
- | `cardContentVar` | 卡片内容变量名,不填默认 `msgContent`(过程/工具/终稿统一写入此字段) |
142
- | `answerCard` | 答案卡模式开关,**默认开启**;显式设 `false` 关闭 |
143
- | `answerActToken` | 答案卡触发阈值(token),默认 `500`;最终答案 ≤ 此值直接在原卡定稿,> 此值才另开答案卡 |
144
- | `answerCardTemplateId` | 答案卡模板 ID,不填用内置默认模板(需含 `content` 变量) |
145
-
146
- > 卡片模板需在[钉钉开放平台](https://open.dingtalk.com/)创建,并添加对应的变量字段。
135
+ ### 完整配置字段(`channels.dingtalk-connector`)
136
+
137
+ 源码权威:`src/config/schema.ts` + `openclaw.plugin.json`。
138
+ **顶层单账号**与 **`accounts.<id>`** 共用同一批「共享字段」;账号级缺省时继承顶层。
139
+
140
+ #### 1. 启用 / 凭证 / 多账号
141
+
142
+ | 字段 | 类型 | 默认 | 说明 |
143
+ |------|------|------|------|
144
+ | `enabled` | boolean | — | 是否启用本渠道 |
145
+ | `defaultAccount` | string | — | 默认账号 key;须存在于 `accounts` |
146
+ | `clientId` | string \| number | — | AppKey / Client ID(单账号顶层) |
147
+ | `clientSecret` | string \| SecretRef | — | AppSecret;支持明文或 `{ source, provider, id }` |
148
+ | `accounts` | object | — | 多账号;key 为账号 ID(如 `main-bot`) |
149
+ | `accounts.*.enabled` | boolean | — | 是否启用该账号 |
150
+ | `accounts.*.name` | string | — | 显示名 |
151
+ | `accounts.*.clientId` | string \| number | — | 该账号 Client ID |
152
+ | `accounts.*.clientSecret` | string \| SecretRef | — | 该账号 Secret |
153
+ | `accounts.*.chatbotUserId` | string | — | 机器人加密 ID(群内 @ 其它 bot 用);见日志 `[BotIdentity] chatbotUserId=...` |
154
+ | `accounts.*.chatbotCorpId` | string | — | 机器人所属 corpId(身份日志) |
155
+
156
+ #### 2. 访问策略
157
+
158
+ | 字段 | 类型 | 默认 | 说明 |
159
+ |------|------|------|------|
160
+ | `dmPolicy` | `"open"` \| `"pairing"` \| `"allowlist"` | `"open"` | 单聊策略 |
161
+ | `allowFrom` | (string\|number)[] | — | 单聊白名单;`dmPolicy=allowlist` 时**必填至少 1 项** |
162
+ | `groupPolicy` | `"open"` \| `"allowlist"` \| `"disabled"` | `"open"` | 群聊策略 |
163
+ | `groupAllowFrom` | (string\|number)[] | — | 群白名单;`groupPolicy=allowlist` 时**必填至少 1 项** |
164
+ | `requireMention` | boolean | `true` | 群聊是否需 @机器人 才响应 |
165
+ | `groups` | object | — | 按群 ID 覆盖策略(见下表) |
166
+
167
+ **`groups.<openConversationId>`:**
168
+
169
+ | 字段 | 类型 | 说明 |
170
+ |------|------|------|
171
+ | `enabled` | boolean | 该群是否启用 |
172
+ | `requireMention` | boolean | 覆盖全局 @ 要求 |
173
+ | `allowFrom` | (string\|number)[] | 该群发送者白名单 |
174
+ | `systemPrompt` | string | 该群额外 system 提示 |
175
+ | `groupSessionScope` | `"group"` \| `"group_sender"` | 该群会话维度 |
176
+ | `tools.allow` / `tools.deny` | string[] | 该群工具策略 |
177
+
178
+ #### 3. 会话 / 行为
179
+
180
+ | 字段 | 类型 | 默认 | 说明 |
181
+ |------|------|------|------|
182
+ | `groupSessionScope` | `"group"` \| `"group_sender"` | `"group"` | 群会话:整群一份 / 群+发送者一份 |
183
+ | `separateSessionByConversation` | boolean | `true` | 按会话隔离 |
184
+ | `sharedMemoryAcrossConversations` | boolean | `false` | 跨会话共享记忆 |
185
+ | `historyLimit` | int ≥0 | — | 历史条数上限 |
186
+ | `textChunkLimit` | int >0 | — | 文本分片上限 |
187
+ | `mediaMaxMb` | number >0 | — | 媒体大小上限(MB) |
188
+ | `typingIndicator` | boolean | — | 输入中指示 |
189
+ | `resolveSenderNames` | boolean | — | 解析发送者昵称 |
190
+ | `asyncMode` | boolean | — | 异步模式 |
191
+ | `ackText` | string | — | 收到消息时的 ACK 文案 |
192
+ | `systemPrompt` | string | — | 渠道级附加 system 提示 |
193
+ | `enableMediaUpload` | boolean | — | 是否启用媒体上传能力 |
194
+ | `endpoint` | string | — | 自定义 DWClient 网关(高级) |
195
+ | `debug` | boolean | — | 调试日志 |
196
+
197
+ #### 4. 工具
198
+
199
+ | 字段 | 类型 | 默认 | 说明 |
200
+ |------|------|------|------|
201
+ | `tools.docs` | boolean | true(语义) | 文档类工具 |
202
+ | `tools.media` | boolean | true(语义) | 媒体上传类工具 |
203
+
204
+ #### 5. 群回复形态 / AI 卡 / 答案卡 / message 工具
205
+
206
+ | 字段 | 类型 | 默认 | 说明 |
207
+ |------|------|------|------|
208
+ | `groupReplyMode` | `"aicard"` \| `"text"` \| `"markdown"` | `"aicard"` | 群回复形态;`text`/`markdown` 时群聊不用 AI 流式卡(便于多 bot @) |
209
+ | `cardTemplateId` | string | 官方默认流式模板 | 会话流式 AI Card 模板 ID |
210
+ | `cardContentVar` | string | `"msgContent"` | 流式卡内容变量名(过程/工具/终稿写入此字段) |
211
+ | `answerCard` | boolean | **true**(未配或非 false) | **会话流式**答案卡:长答案另开静态卡,原卡定格「思考完成」 |
212
+ | `answerActToken` | int >0 | **500** | 会话答案卡阈值:估算 token ≤ 此值原卡定稿;> 此值才新建答案卡 |
213
+ | `answerCardTemplateId` | string | 内置答案模板 | 答案静态卡模板 ID(需含内容变量) |
214
+ | `messageAnswerCard` | boolean | **true**(未配或非 false) | **message 工具**正文是否走答案静态卡;`false`=普通 text/markdown。与 `answerCard` **独立**;图/音/视/文件仍普通通道 |
215
+ | `messageImageMd` | boolean | **false** | message 工具图文:`false` 文图分开;`true` 仅多图+文字时合并一条 markdown |
216
+
217
+ > 卡片模板需在[钉钉开放平台](https://open.dingtalk.com/)创建并发布。
218
+ > `answerCard` = 对话流式收尾;`messageAnswerCard` = Agent 调 message 工具外发。不要混为一谈。
147
219
 
148
220
  ---
149
221
 
@@ -23,7 +23,7 @@ var entry_bundled_default = defineBundledChannelEntry({
23
23
  exportName: "setDingtalkRuntime"
24
24
  },
25
25
  async registerFull(api) {
26
- const { registerGatewayMethods } = await import("./gateway-methods-BTByJFxo.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-DtzGBBiy.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });
@@ -1,7 +1,7 @@
1
1
  import { a as resolveDingtalkAccount, t as listDingtalkAccountIds } from "./accounts-BAzdqkAV.mjs";
2
2
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
3
3
  import { r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
4
- import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-ngF20yqM.mjs";
4
+ import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-DOEBZgXQ.mjs";
5
5
  import { c as getUnionId, d as recallEmotionReply } from "./utils-legacy-CALCPP1t.mjs";
6
6
  //#region src/docs.ts
7
7
  var DingtalkDocsClient = class {
@@ -1,2 +1,2 @@
1
- import { t as registerGatewayMethods } from "./gateway-methods-CkBI4__L.mjs";
1
+ import { t as registerGatewayMethods } from "./gateway-methods-CuqlMAXE.mjs";
2
2
  export { registerGatewayMethods };
package/dist/index.d.mts CHANGED
@@ -70,6 +70,7 @@ declare const DingtalkConfigSchema: z$1.ZodObject<{
70
70
  enableMediaUpload: z$1.ZodOptional<z$1.ZodBoolean>;
71
71
  systemPrompt: z$1.ZodOptional<z$1.ZodString>;
72
72
  messageImageMd: z$1.ZodOptional<z$1.ZodBoolean>;
73
+ messageAnswerCard: z$1.ZodOptional<z$1.ZodBoolean>;
73
74
  groupReplyMode: z$1.ZodOptional<z$1.ZodEnum<{
74
75
  aicard: "aicard";
75
76
  text: "text";
@@ -127,6 +128,7 @@ declare const DingtalkConfigSchema: z$1.ZodObject<{
127
128
  enableMediaUpload: z$1.ZodOptional<z$1.ZodBoolean>;
128
129
  systemPrompt: z$1.ZodOptional<z$1.ZodString>;
129
130
  messageImageMd: z$1.ZodOptional<z$1.ZodBoolean>;
131
+ messageAnswerCard: z$1.ZodOptional<z$1.ZodBoolean>;
130
132
  groupReplyMode: z$1.ZodOptional<z$1.ZodEnum<{
131
133
  aicard: "aicard";
132
134
  text: "text";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-I36vhOzx.mjs";
2
- import { t as registerGatewayMethods } from "./gateway-methods-CkBI4__L.mjs";
1
+ import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-hVrm4xYI.mjs";
2
+ import { t as registerGatewayMethods } from "./gateway-methods-CuqlMAXE.mjs";
3
3
  //#region index.ts
4
4
  /**
5
5
  * 检测同一 plugin id 在多个路径被加载的情况。
@@ -1,2 +1,2 @@
1
- import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-BMNRjseJ.mjs";
1
+ import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-DnQB2BrL.mjs";
2
2
  export { hasResidualLocalMdImagesOutsideCode, processLocalImages, processRawMediaPaths, processVideoMarkers, sendFileProactive, toLocalPath };
@@ -30,10 +30,18 @@ const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
30
30
  * 注意:旧版只匹配 /tmp|/home|/root 等前缀,**漏掉 /mnt 共享盘**,导致 message 工具 MD 灰图。
31
31
  */
32
32
  const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
33
- /** 是否像远程 URL 或其截断片段(禁止当本地路径上传) */
33
+ /** 本地文件 URI / OpenClaw 媒体前缀(不是 http 远程) */
34
+ function isLocalFileScheme(p) {
35
+ return p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://");
36
+ }
37
+ /**
38
+ * 是否像远程 URL(禁止当本地路径上传)。
39
+ * **不含** file:// / MEDIA: / attachment://(那些是本地引用)。
40
+ */
34
41
  function looksLikeRemoteUrl(raw) {
35
42
  const p = (raw || "").trim();
36
43
  if (!p) return false;
44
+ if (isLocalFileScheme(p)) return false;
37
45
  if (/^https?:\/\//i.test(p)) return true;
38
46
  if (/^[a-z]:\/\//i.test(p) && !/^[a-z]:[\\/][^/\\]/i.test(p)) return true;
39
47
  if (p.includes("://")) return true;
@@ -43,9 +51,9 @@ function looksLikeRemoteUrl(raw) {
43
51
  function isLocalImageRef(rawPath) {
44
52
  const p = (rawPath || "").trim();
45
53
  if (!p) return false;
54
+ if (isLocalFileScheme(p)) return true;
46
55
  if (looksLikeRemoteUrl(p) || /^https?:\/\//i.test(p)) return false;
47
56
  if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
48
- if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) return true;
49
57
  if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
50
58
  if (IMAGE_EXTENSIONS.test(p.split("?")[0] || p)) return true;
51
59
  return !p.includes("://");
@@ -1,9 +1,9 @@
1
1
  import { a as resolveDingtalkAccount } from "./accounts-BAzdqkAV.mjs";
2
- import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-I36vhOzx.mjs";
2
+ import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-hVrm4xYI.mjs";
3
3
  import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
4
4
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
5
5
  import { i as getOapiAccessToken } from "./utils-CIfI_3Jh.mjs";
6
- import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-ngF20yqM.mjs";
6
+ import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-DOEBZgXQ.mjs";
7
7
  import { a as QUEUE_BUSY_ACK_PHRASES, n as normalizeSlashCommand, t as buildSessionContext } from "./session-DJ4jYqPv.mjs";
8
8
  import { d as recallEmotionReply, o as getAccessToken, r as addEmotionReply, s as getOapiAccessToken$1, t as DINGTALK_API } from "./utils-legacy-CALCPP1t.mjs";
9
9
  import "./chunk-upload-BBQgGtcZ.mjs";
@@ -62,7 +62,7 @@ function resolveAgentWorkspaceDir(cfg, agentId) {
62
62
  * 直接复用 media.ts 权威实现,避免 reply 路径与 message 路径两套逻辑分叉。
63
63
  */
64
64
  async function processLocalImages(content, oapiToken, log) {
65
- const { processLocalImages: impl } = await import("./media-9LvaywGZ.mjs");
65
+ const { processLocalImages: impl } = await import("./media-BqpUGCni.mjs");
66
66
  return impl(content, oapiToken, log);
67
67
  }
68
68
  //#endregion
@@ -581,14 +581,14 @@ function createDingtalkReplyDispatcher(params) {
581
581
  finalText = await processLocalImages(finalText, oapiToken, log);
582
582
  if (beforeImg !== finalText) console.log(`[DingTalk][LocalImage] closeStreaming 图片处理 | ${beforeImg.length}→${finalText.length}`);
583
583
  try {
584
- const { hasResidualLocalMdImagesOutsideCode } = await import("./media-9LvaywGZ.mjs");
584
+ const { hasResidualLocalMdImagesOutsideCode } = await import("./media-BqpUGCni.mjs");
585
585
  if (hasResidualLocalMdImagesOutsideCode(finalText)) console.warn(`[DingTalk][LocalImage] closeStreaming 定稿后仍有代码块外的本地 MD 图未上传,钉钉可能灰图`);
586
586
  } catch {}
587
587
  finalText = await processVideoMarkers(finalText, "", account.config, oapiToken, log, true, target);
588
588
  finalText = await processAudioMarkers(finalText, "", account.config, oapiToken, log, true, target);
589
589
  finalText = await uploadAndReplaceFileMarkers(finalText, "", account.config, oapiToken, log, true, target);
590
590
  log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
591
- const { processRawMediaPaths } = await import("./media-9LvaywGZ.mjs");
591
+ const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
592
592
  finalText = await processRawMediaPaths(finalText, account.config, oapiToken, log, target);
593
593
  log.info(`[DingTalk][closeStreaming] processRawMediaPaths 处理完成`);
594
594
  } else log.warn(`[DingTalk][closeStreaming] oapiToken 为空,跳过媒体处理`);
@@ -744,7 +744,7 @@ function createDingtalkReplyDispatcher(params) {
744
744
  const oapiToken = await getOapiAccessToken(account.config);
745
745
  if (oapiToken) {
746
746
  log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
747
- const { processRawMediaPaths } = await import("./media-9LvaywGZ.mjs");
747
+ const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
748
748
  text = await processRawMediaPaths(text, account.config, oapiToken, log, target);
749
749
  log.info(`[DingTalk][deliver] 裸露文件路径处理完成`);
750
750
  }
@@ -2184,7 +2184,7 @@ async function handleDingTalkMessageInternal(params) {
2184
2184
  finalText = await processVideoMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2185
2185
  finalText = await processAudioMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2186
2186
  finalText = await uploadAndReplaceFileMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2187
- const { processRawMediaPaths } = await import("./media-9LvaywGZ.mjs");
2187
+ const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
2188
2188
  finalText = await processRawMediaPaths(finalText, config, oapiToken, log, mediaTarget);
2189
2189
  }
2190
2190
  const textToSend = finalText.trim() || "✅ 任务执行完成(无文本输出)";
@@ -1,4 +1,4 @@
1
- import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-BMNRjseJ.mjs";
1
+ import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-DnQB2BrL.mjs";
2
2
  import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
3
3
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
4
4
  import { i as getOapiAccessToken, r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
@@ -582,6 +582,28 @@ function prepareMultiBotMentions(params) {
582
582
  //#endregion
583
583
  //#region src/services/messaging.ts
584
584
  /**
585
+ * message 工具是否用答案卡(默认开;显式 false 才关)。
586
+ * 与会话流式 answerCard 独立。
587
+ */
588
+ function isMessageAnswerCardEnabled(config) {
589
+ return config?.messageAnswerCard !== false;
590
+ }
591
+ /**
592
+ * message 工具正文发送的卡片选项:答案卡模板 + 静态 FINISHED,失败降级普通消息。
593
+ */
594
+ function messageToolProactiveCardOptions(config) {
595
+ if (!isMessageAnswerCardEnabled(config)) return {
596
+ useAICard: false,
597
+ fallbackToNormal: true
598
+ };
599
+ return {
600
+ useAICard: true,
601
+ fallbackToNormal: true,
602
+ cardTemplateId: typeof config.answerCardTemplateId === "string" && config.answerCardTemplateId.trim() || "d246b7f5-1783-4e9b-bb46-bef52d63050e.schema",
603
+ skipInputingWalk: true
604
+ };
605
+ }
606
+ /**
585
607
  * 发送 Markdown 消息
586
608
  * 支持 @用户(atUserId)和 @机器人(atDingtalkIds)
587
609
  */
@@ -848,7 +870,7 @@ async function resolveMediaSourceToLocalFile(mediaUrl, mediaLocalRoots, log) {
848
870
  cleanup: tmp
849
871
  };
850
872
  }
851
- const { toLocalPath } = await import("./media-9LvaywGZ.mjs");
873
+ const { toLocalPath } = await import("./media-BqpUGCni.mjs");
852
874
  const _fs = await import("fs");
853
875
  const _path = await import("path");
854
876
  let resolved = toLocalPath(mediaUrl);
@@ -898,12 +920,13 @@ async function sendTextToDingTalk(params) {
898
920
  console.warn(`[DingTalk][LocalImage] sendTextToDingTalk 图片处理失败: ${err?.message || err}`);
899
921
  }
900
922
  const msgType = shouldSendAsMarkdown(text) ? "markdown" : "text";
901
- logMediaIdTrace("sendTextToDingTalk:API前", text, `msgType=${msgType}`, true);
923
+ const cardOpts = messageToolProactiveCardOptions(config);
924
+ logMediaIdTrace("sendTextToDingTalk:API前", text, `msgType=${msgType} messageAnswerCard=${isMessageAnswerCardEnabled(config)} useAICard=${cardOpts.useAICard}`, true);
925
+ console.log(`[DingTalk][MessageCard] sendTextToDingTalk | messageAnswerCard=${isMessageAnswerCardEnabled(config)} useAICard=${cardOpts.useAICard} tpl=${cardOpts.cardTemplateId || "-"}`);
902
926
  return sendProactive(config, targetParam, text, {
903
927
  msgType,
904
928
  replyToId,
905
- useAICard: false,
906
- fallbackToNormal: true,
929
+ ...cardOpts,
907
930
  skipProcessLocalImages: true
908
931
  });
909
932
  }
@@ -1000,8 +1023,7 @@ async function sendMediaToDingTalk(params) {
1000
1023
  if (text?.trim()) await sendProactive(config, targetParam, text.trim(), {
1001
1024
  msgType: shouldSendAsMarkdown(text) ? "markdown" : "text",
1002
1025
  replyToId,
1003
- useAICard: false,
1004
- fallbackToNormal: true
1026
+ ...messageToolProactiveCardOptions(config)
1005
1027
  });
1006
1028
  if (isRemoteHttpUrl(mediaUrl) && mediaType === "image") {
1007
1029
  console.log(`[DingTalk][RemoteMedia] 下载失败,尝试 photoURL 直链发送 | ${mediaUrl}`);
@@ -1041,8 +1063,7 @@ async function sendMediaToDingTalk(params) {
1041
1063
  if (caption) await sendProactive(config, targetParam, caption, {
1042
1064
  msgType: shouldSendAsMarkdown(caption) ? "markdown" : "text",
1043
1065
  replyToId,
1044
- useAICard: false,
1045
- fallbackToNormal: true
1066
+ ...messageToolProactiveCardOptions(config)
1046
1067
  });
1047
1068
  if (isRemoteHttpUrl(mediaUrl)) {
1048
1069
  console.log(`[DingTalk][RemoteMedia] 再上传失败,photoURL 直链兜底 | ${mediaUrl}`);
@@ -1068,8 +1089,7 @@ async function sendMediaToDingTalk(params) {
1068
1089
  const result = await sendProactive(config, targetParam, `${caption}\n\n![](${mediaId})`, {
1069
1090
  msgType: "markdown",
1070
1091
  replyToId,
1071
- useAICard: false,
1072
- fallbackToNormal: true
1092
+ ...messageToolProactiveCardOptions(config)
1073
1093
  });
1074
1094
  return {
1075
1095
  ...result,
@@ -1079,8 +1099,7 @@ async function sendMediaToDingTalk(params) {
1079
1099
  if (caption) await sendProactive(config, targetParam, caption, {
1080
1100
  msgType: shouldSendAsMarkdown(caption) ? "markdown" : "text",
1081
1101
  replyToId,
1082
- useAICard: false,
1083
- fallbackToNormal: true
1102
+ ...messageToolProactiveCardOptions(config)
1084
1103
  });
1085
1104
  const result = await sendProactive(config, targetParam, mediaId, {
1086
1105
  msgType: "image",
@@ -1102,8 +1121,7 @@ async function sendMediaToDingTalk(params) {
1102
1121
  await sendProactive(config, targetParam, caption, {
1103
1122
  msgType: shouldSendAsMarkdown(caption) ? "markdown" : "text",
1104
1123
  replyToId,
1105
- useAICard: false,
1106
- fallbackToNormal: true
1124
+ ...messageToolProactiveCardOptions(config)
1107
1125
  });
1108
1126
  }
1109
1127
  const uploadResult = await uploadMediaToDingTalk(resolvedMediaUrl, mediaType, oapiToken, maxSize, log);
@@ -1121,7 +1139,7 @@ async function sendMediaToDingTalk(params) {
1121
1139
  const fileName = mediaUrl.split("/").filter(Boolean).pop()?.split("?")[0] || "file";
1122
1140
  if (mediaType === "video") {
1123
1141
  const videoMarker = `[DINGTALK_VIDEO]{"path":"${resolvedMediaUrl}"}[/DINGTALK_VIDEO]`;
1124
- const { processVideoMarkers } = await import("./media-9LvaywGZ.mjs");
1142
+ const { processVideoMarkers } = await import("./media-BqpUGCni.mjs");
1125
1143
  await processVideoMarkers(videoMarker, "", config, oapiToken, console, true, targetParam);
1126
1144
  return {
1127
1145
  ok: true,
@@ -1140,7 +1158,7 @@ async function sendMediaToDingTalk(params) {
1140
1158
  fileName,
1141
1159
  fileType: ext && ext.length < 12 ? ext : "file"
1142
1160
  };
1143
- const { sendFileProactive } = await import("./media-9LvaywGZ.mjs");
1161
+ const { sendFileProactive } = await import("./media-BqpUGCni.mjs");
1144
1162
  await sendFileProactive(config, targetParam, fileInfo, uploadResult.mediaId, log);
1145
1163
  return {
1146
1164
  ok: true,
@@ -1219,12 +1237,13 @@ async function sendProactiveInternal(config, target, content, options) {
1219
1237
  usedAICard: false
1220
1238
  };
1221
1239
  }
1222
- const { msgType = "text", useAICard = true, fallbackToNormal = true, log: externalLog } = options;
1240
+ const { msgType = "text", useAICard = true, fallbackToNormal = true, cardTemplateId, skipInputingWalk = false, log: externalLog } = options;
1223
1241
  const isMediaMessage = MEDIA_MSG_TYPES.has(msgType);
1224
1242
  if (useAICard && !isMediaMessage) try {
1225
- const card = await createAICardForTarget(config, target, externalLog);
1243
+ const card = await createAICardForTarget(config, target, externalLog, cardTemplateId);
1226
1244
  if (card) {
1227
- await finishAICard(card, content, config, externalLog);
1245
+ await finishAICard(card, content, config, externalLog, void 0, skipInputingWalk);
1246
+ console.log(`[DingTalk][MessageCard] AI Card 发送成功 | outTrack=${card.cardInstanceId} tpl=${cardTemplateId || "default"} skipInputing=${skipInputingWalk}`);
1228
1247
  return {
1229
1248
  ok: true,
1230
1249
  cardInstanceId: card.cardInstanceId,
@@ -1236,8 +1255,10 @@ async function sendProactiveInternal(config, target, content, options) {
1236
1255
  error: "Failed to create AI Card",
1237
1256
  usedAICard: false
1238
1257
  };
1258
+ console.warn(`[DingTalk][MessageCard] AI Card 创建失败,fallbackToNormal=${fallbackToNormal}`);
1239
1259
  } catch (err) {
1240
1260
  externalLog?.error?.(`AI Card 发送失败: ${err.message}`);
1261
+ console.warn(`[DingTalk][MessageCard] AI Card 异常: ${err?.message || err},fallbackToNormal=${fallbackToNormal}`);
1241
1262
  if (!fallbackToNormal) return {
1242
1263
  ok: false,
1243
1264
  error: err.message,
@@ -1,9 +1,9 @@
1
- import { b as __exportAll } from "./media-BMNRjseJ.mjs";
1
+ import { b as __exportAll } from "./media-DnQB2BrL.mjs";
2
2
  import { a as resolveDingtalkAccount, c as addWildcardAllowFrom, d as hasConfiguredSecretInput, f as normalizeAccountId, l as createDefaultChannelRuntimeState, m as resolveDefaultGroupPolicy, o as resolveDingtalkCredentials, p as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultDingtalkAccountId, s as DEFAULT_ACCOUNT_ID, t as listDingtalkAccountIds, u as formatDocsLink } from "./accounts-BAzdqkAV.mjs";
3
3
  import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
4
4
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
5
5
  import "./utils-CIfI_3Jh.mjs";
6
- import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-ngF20yqM.mjs";
6
+ import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-DOEBZgXQ.mjs";
7
7
  import { createRequire } from "node:module";
8
8
  import { z, z as z$1 } from "zod";
9
9
  //#region src/secret-input.ts
@@ -102,6 +102,13 @@ const DingtalkSharedConfigShape = {
102
102
  * 单张图文、纯媒体文件仍分开发送
103
103
  */
104
104
  messageImageMd: z$1.boolean().optional(),
105
+ /**
106
+ * message 工具是否用答案卡发送正文(默认 true,显式 false 才关):
107
+ * - true / 未配:走答案专用静态卡(answerCardTemplateId 或内置模板),失败降级普通消息
108
+ * - false:直接普通 text/markdown(旧行为)
109
+ * 与会话流式的 answerCard 独立;媒体消息(图/音/视/文件)仍走普通通道。
110
+ */
111
+ messageAnswerCard: z$1.boolean().optional(),
105
112
  groupReplyMode: GroupReplyModeSchema,
106
113
  /** AI Card 模板 ID,不填则使用官方默认模板 */
107
114
  cardTemplateId: z$1.string().optional(),
@@ -111,7 +118,7 @@ const DingtalkSharedConfigShape = {
111
118
  * 规避钉钉流式卡 FINISHED 后仍抖动的官方 bug。默认开启(显式设 false 才关)。 */
112
119
  answerCard: z$1.boolean().optional(),
113
120
  /** 答案卡触发阈值(token):最终答案估算 token ≤ 此值时直接在原卡定稿(不另建卡),
114
- * > 此值才新建答案卡。避免简单任务也多一张卡。默认 600。 */
121
+ * > 此值才新建答案卡。避免简单任务也多一张卡。默认 500(见 reply-dispatcher)。 */
115
122
  answerActToken: z$1.number().int().positive().optional(),
116
123
  /** 答案专用卡模板 ID:不填用内置默认模板。需是钉钉后台已发布、含 content 变量的卡片模板。 */
117
124
  answerCardTemplateId: z$1.string().optional()
@@ -930,7 +937,7 @@ async function monitorDingtalkProvider(opts = {}) {
930
937
  const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
931
938
  const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
932
939
  import("./accounts-BQptOmgB.mjs"),
933
- import("./message-handler-CytB34ZJ.mjs"),
940
+ import("./message-handler-DKPtnK0x.mjs"),
934
941
  import("./connection-DHHFFNQJ.mjs")
935
942
  ]);
936
943
  const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
@@ -0,0 +1,32 @@
1
+ # Release Notes — v0.8.21-fix47(2026-07-20)
2
+
3
+ ## 安装 / Install
4
+
5
+ ```bash
6
+ npx -y @jeik/dingtalk-connector install --force
7
+ openclaw gateway restart
8
+ ```
9
+
10
+ ## 修复 / Fixed
11
+
12
+ ### `file://` MD 本地图灰图
13
+
14
+ **现象(fix46 日志):**
15
+
16
+ ```text
17
+ 扫描 ![]() | #2 kind=http path=file:///mnt/smb/...
18
+ processImagesForOutbound 远程图 | file:///mnt/...
19
+ 远程图下载异常 | fetch failed
20
+ localTried=0 remoteTried=3
21
+ API前 mdImgs 仍含 http:file:///...
22
+ ```
23
+
24
+ **原因:** `looksLikeRemoteUrl` 把任意含 `://` 的字符串当远程;`isLocalImageRef` 又先调用它,`file://` 永远进不了本地上传。
25
+
26
+ **修复:** `file://` / `MEDIA:` / `attachment://` 优先判本地 → `toLocalPath` → 上传 mediaId。
27
+
28
+ 裸绝对路径 `/mnt/...`、`/tmp/...` 本来就正常;本版修的是 **带 `file://` 前缀** 的写法。
29
+
30
+ ## 升级
31
+
32
+ 从 fix46 直接 `--force` 安装。
@@ -0,0 +1,73 @@
1
+ # Release Notes — v0.8.21-fix48(2026-07-21)
2
+
3
+ 生产稳定版 / Production-stable community build of `@jeik/dingtalk-connector`。
4
+
5
+ ## 安装 / Install
6
+
7
+ ```bash
8
+ npx -y @jeik/dingtalk-connector install --force
9
+ openclaw gateway restart
10
+ ```
11
+
12
+ 本地 tgz:
13
+
14
+ ```bash
15
+ openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix48.tgz --force
16
+ openclaw gateway restart
17
+ ```
18
+
19
+ ## 本版要点 / Highlights
20
+
21
+ | 领域 | 说明 |
22
+ |------|------|
23
+ | **message 答案卡** | 新增配置 **`messageAnswerCard`(默认 `true`)**:message 工具正文默认走**答案静态卡** |
24
+ | 模板 | 使用 `answerCardTemplateId` 或内置答案卡模板;`skipInputingWalk` 直接 FINISHED |
25
+ | 降级 | 建卡失败 → 自动普通 text/markdown |
26
+ | 关闭 | `"messageAnswerCard": false` 恢复旧行为(普通消息) |
27
+ | 注册 | `schema.ts` + `openclaw.plugin.json`(channels / accounts / uiHints) |
28
+ | 独立 | 与会话流式 **`answerCard`** 互不影响;图/音/视/文件仍走普通通道 |
29
+
30
+ ## 配置示例
31
+
32
+ ```json
33
+ "channels": {
34
+ "dingtalk-connector": {
35
+ "messageAnswerCard": true,
36
+ "answerCardTemplateId": "可选-自定义答案卡模板.schema"
37
+ }
38
+ }
39
+ ```
40
+
41
+ 关闭 message 答案卡:
42
+
43
+ ```json
44
+ "messageAnswerCard": false
45
+ ```
46
+
47
+ ### 完整字段表
48
+
49
+ 见仓库 README「完整配置字段」章节(中文 `README.md` / 英文 `README.en.md`),与 `src/config/schema.ts`、`openclaw.plugin.json` 对齐,含:
50
+
51
+ - 凭证 / `accounts` / `chatbotUserId`
52
+ - `dmPolicy` / `groupPolicy` / `requireMention` / `groups.*`
53
+ - 会话:`groupSessionScope` / `separateSessionByConversation` / …
54
+ - 卡片:`groupReplyMode` / `cardTemplateId` / `cardContentVar`
55
+ - 答案卡:`answerCard` / `answerActToken`(默认 500)/ `answerCardTemplateId`
56
+ - message:`messageAnswerCard`(默认 true)/ `messageImageMd`(默认 false)
57
+
58
+ ## 日志
59
+
60
+ ```text
61
+ [DingTalk][MessageCard] sendTextToDingTalk | messageAnswerCard=true useAICard=true tpl=...
62
+ [DingTalk][MessageCard] AI Card 发送成功 | outTrack=card_... skipInputing=true
63
+ ```
64
+
65
+ ## 含此前修复
66
+
67
+ - fix47:`file://` 本地图不再误判远程
68
+ - fix46:LocalImage / MediaIdTrace 诊断日志
69
+ - fix45:图+下载同泡、只认 `![]`、CardCache、residual 误报
70
+
71
+ ## 升级
72
+
73
+ 从任意 fix37+ 直接 `--force` 安装即可。
@@ -247,6 +247,11 @@
247
247
  "default": false,
248
248
  "description": "message 工具图文策略:false=文图分开(默认);true=多图+文字时才合并为 markdown"
249
249
  },
250
+ "messageAnswerCard": {
251
+ "type": "boolean",
252
+ "default": true,
253
+ "description": "message 工具是否用答案卡发送正文:true(默认)= 答案静态卡;false= 普通 text/markdown。媒体消息仍走普通通道。"
254
+ },
250
255
  "groupReplyMode": {
251
256
  "type": "string",
252
257
  "enum": [
@@ -493,6 +498,11 @@
493
498
  "default": false,
494
499
  "description": "message 工具图文策略:false=文图分开(默认);true=多图+文字时才合并为 markdown"
495
500
  },
501
+ "messageAnswerCard": {
502
+ "type": "boolean",
503
+ "default": true,
504
+ "description": "message 工具是否用答案卡发送正文:true(默认)= 答案静态卡;false= 普通 text/markdown。媒体消息仍走普通通道。"
505
+ },
496
506
  "groupReplyMode": {
497
507
  "type": "string",
498
508
  "enum": [
@@ -557,6 +567,41 @@
557
567
  "help": "message 工具图文策略。false(默认):文字与图片分开发送,阅读体验更好。true:仅当多图与文字结合时合并为一条 markdown;单张图文、纯媒体仍分开。",
558
568
  "advanced": true
559
569
  },
570
+ "channels.dingtalk-connector.messageAnswerCard": {
571
+ "label": "Message Answer Card",
572
+ "help": "message 工具是否用答案卡发正文。默认开启(答案静态卡,失败降级普通消息);设为 false 则直接发 text/markdown。与会话流式的 answerCard 独立。",
573
+ "advanced": true
574
+ },
575
+ "channels.dingtalk-connector.groupReplyMode": {
576
+ "label": "Group Reply Mode",
577
+ "help": "群回复形态:aicard(默认,流式 AI 卡)/ text / markdown。text、markdown 时群聊不用 AI 卡,便于多机器人互相 @。",
578
+ "advanced": true
579
+ },
580
+ "channels.dingtalk-connector.cardTemplateId": {
581
+ "label": "AI Card Template ID",
582
+ "help": "会话流式 AI Card 模板 ID;不填用官方默认流式模板。",
583
+ "advanced": true
584
+ },
585
+ "channels.dingtalk-connector.cardContentVar": {
586
+ "label": "Card Content Variable",
587
+ "help": "流式卡内容变量名,默认 msgContent(过程/工具/终稿写入此字段)。",
588
+ "advanced": true
589
+ },
590
+ "channels.dingtalk-connector.answerCard": {
591
+ "label": "Session Answer Card",
592
+ "help": "会话流式答案卡开关,默认开启。长答案另开静态卡,原卡定格思考完成;显式 false 关闭。",
593
+ "advanced": true
594
+ },
595
+ "channels.dingtalk-connector.answerActToken": {
596
+ "label": "Answer Card Token Threshold",
597
+ "help": "会话答案卡触发阈值(token),默认 500。≤ 阈值在原卡定稿;> 阈值才新建答案卡。",
598
+ "advanced": true
599
+ },
600
+ "channels.dingtalk-connector.answerCardTemplateId": {
601
+ "label": "Answer Card Template ID",
602
+ "help": "答案静态卡模板 ID;message 答案卡与会话答案卡共用。不填用内置默认模板。",
603
+ "advanced": true
604
+ },
560
605
  "channels.dingtalk-connector.endpoint": {
561
606
  "label": "Gateway Endpoint",
562
607
  "help": "自定义 DWClient 网关地址(高级)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeik/dingtalk-connector",
3
- "version": "0.8.21-fix46",
3
+ "version": "0.8.21-fix48",
4
4
  "description": "OpenClaw DingTalk channel plugin (fork) | 钉钉 OpenClaw 渠道插件(fork 自官方,含标记/卡片渲染修复)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -91,6 +91,13 @@ const DingtalkSharedConfigShape = {
91
91
  * 单张图文、纯媒体文件仍分开发送
92
92
  */
93
93
  messageImageMd: z.boolean().optional(),
94
+ /**
95
+ * message 工具是否用答案卡发送正文(默认 true,显式 false 才关):
96
+ * - true / 未配:走答案专用静态卡(answerCardTemplateId 或内置模板),失败降级普通消息
97
+ * - false:直接普通 text/markdown(旧行为)
98
+ * 与会话流式的 answerCard 独立;媒体消息(图/音/视/文件)仍走普通通道。
99
+ */
100
+ messageAnswerCard: z.boolean().optional(),
94
101
  groupReplyMode: GroupReplyModeSchema,
95
102
  /** AI Card 模板 ID,不填则使用官方默认模板 */
96
103
  cardTemplateId: z.string().optional(),
@@ -100,7 +107,7 @@ const DingtalkSharedConfigShape = {
100
107
  * 规避钉钉流式卡 FINISHED 后仍抖动的官方 bug。默认开启(显式设 false 才关)。 */
101
108
  answerCard: z.boolean().optional(),
102
109
  /** 答案卡触发阈值(token):最终答案估算 token ≤ 此值时直接在原卡定稿(不另建卡),
103
- * > 此值才新建答案卡。避免简单任务也多一张卡。默认 600。 */
110
+ * > 此值才新建答案卡。避免简单任务也多一张卡。默认 500(见 reply-dispatcher)。 */
104
111
  answerActToken: z.number().int().positive().optional(),
105
112
  /** 答案专用卡模板 ID:不填用内置默认模板。需是钉钉后台已发布、含 content 变量的卡片模板。 */
106
113
  answerCardTemplateId: z.string().optional(),
@@ -28,18 +28,17 @@ export const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
28
28
  */
29
29
  export const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
30
30
 
31
- /** 是否为需上传的本地图引用 */
31
+ /** 是否为需上传的本地图引用(与 media.ts 权威实现一致:file:// 必须算本地) */
32
32
  export function isLocalImageRef(rawPath: string): boolean {
33
33
  const p = (rawPath || "").trim();
34
34
  if (!p) return false;
35
- if (/^https?:\/\//i.test(p)) return false;
36
- if (p.includes("://") && !p.startsWith("file://") && !p.startsWith("MEDIA:") && !p.startsWith("attachment://")) {
37
- return false;
38
- }
39
- if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
35
+ // file:// 等本地 scheme 优先(勿被 includes("://") 挡掉)
40
36
  if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) {
41
37
  return true;
42
38
  }
39
+ if (/^https?:\/\//i.test(p)) return false;
40
+ if (p.includes("://")) return false;
41
+ if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
43
42
  if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
44
43
  if (IMAGE_EXTENSIONS.test(p.split("?")[0] || p)) return true;
45
44
  return !p.includes("://");
@@ -45,11 +45,26 @@ export const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
45
45
  */
46
46
  export const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
47
47
 
48
- /** 是否像远程 URL 或其截断片段(禁止当本地路径上传) */
48
+ /** 本地文件 URI / OpenClaw 媒体前缀(不是 http 远程) */
49
+ function isLocalFileScheme(p: string): boolean {
50
+ return (
51
+ p.startsWith("file://") ||
52
+ p.startsWith("MEDIA:") ||
53
+ p.startsWith("attachment://")
54
+ );
55
+ }
56
+
57
+ /**
58
+ * 是否像远程 URL(禁止当本地路径上传)。
59
+ * **不含** file:// / MEDIA: / attachment://(那些是本地引用)。
60
+ */
49
61
  export function looksLikeRemoteUrl(raw: string): boolean {
50
62
  const p = (raw || "").trim();
51
63
  if (!p) return false;
64
+ // 本地 scheme 优先:否则 file:// 会被 includes("://") 误判成远程 → fetch 失败 → 灰图
65
+ if (isLocalFileScheme(p)) return false;
52
66
  if (/^https?:\/\//i.test(p)) return true;
67
+ // 伪协议截断如 p://(Windows 路径被当 URL 的历史坑)
53
68
  if (/^[a-z]:\/\//i.test(p) && !/^[a-z]:[\\/][^/\\]/i.test(p)) return true;
54
69
  if (p.includes("://")) return true;
55
70
  return false;
@@ -156,12 +171,11 @@ export function protectNonImageOccurrencesOfTargets(
156
171
  export function isLocalImageRef(rawPath: string): boolean {
157
172
  const p = (rawPath || "").trim();
158
173
  if (!p) return false;
174
+ // file:// / MEDIA: / attachment:// 一律本地(必须先于 looksLikeRemoteUrl)
175
+ if (isLocalFileScheme(p)) return true;
159
176
  // 已是远程 URL 或钉钉 mediaId(@ 开头)→ 不上传
160
177
  if (looksLikeRemoteUrl(p) || /^https?:\/\//i.test(p)) return false;
161
178
  if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
162
- if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) {
163
- return true;
164
- }
165
179
  // Unix 绝对路径 / Windows 盘符
166
180
  if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
167
181
  // 有图片后缀,或路径中明显是文件(含扩展名)
@@ -15,4 +15,6 @@ export {
15
15
  sendToGroup,
16
16
  sendTextToDingTalk,
17
17
  sendMediaToDingTalk,
18
+ isMessageAnswerCardEnabled,
19
+ messageToolProactiveCardOptions,
18
20
  } from '../messaging.ts';
@@ -22,11 +22,43 @@ import {
22
22
  createAICardForTarget,
23
23
  streamAICard,
24
24
  finishAICard,
25
+ ANSWER_CARD_TEMPLATE_ID,
25
26
  type AICardInstance,
26
27
  type AICardTarget,
27
28
  } from "./messaging/card.ts";
28
29
  import { substituteBotMentions } from "./messaging/mentions.ts";
29
30
 
31
+ /**
32
+ * message 工具是否用答案卡(默认开;显式 false 才关)。
33
+ * 与会话流式 answerCard 独立。
34
+ */
35
+ export function isMessageAnswerCardEnabled(config: DingtalkConfig | null | undefined): boolean {
36
+ return (config as any)?.messageAnswerCard !== false;
37
+ }
38
+
39
+ /**
40
+ * message 工具正文发送的卡片选项:答案卡模板 + 静态 FINISHED,失败降级普通消息。
41
+ */
42
+ export function messageToolProactiveCardOptions(
43
+ config: DingtalkConfig,
44
+ ): Pick<
45
+ ProactiveSendOptions,
46
+ "useAICard" | "fallbackToNormal" | "cardTemplateId" | "skipInputingWalk"
47
+ > {
48
+ if (!isMessageAnswerCardEnabled(config)) {
49
+ return { useAICard: false, fallbackToNormal: true };
50
+ }
51
+ const tpl =
52
+ (typeof config.answerCardTemplateId === "string" && config.answerCardTemplateId.trim()) ||
53
+ ANSWER_CARD_TEMPLATE_ID;
54
+ return {
55
+ useAICard: true,
56
+ fallbackToNormal: true,
57
+ cardTemplateId: tpl,
58
+ skipInputingWalk: true,
59
+ };
60
+ }
61
+
30
62
  // ============ 常量 ============
31
63
  // 注意:AI Card 相关的类型和函数已移至 ./messaging/card.ts,通过上方 import 引入
32
64
 
@@ -55,6 +87,15 @@ export interface ProactiveSendOptions {
55
87
  log?: any;
56
88
  useAICard?: boolean;
57
89
  fallbackToNormal?: boolean;
90
+ /**
91
+ * 覆盖 AI Card / 答案卡模板 ID(如 message 工具答案卡)。
92
+ * 不传则 createAICardForTarget 用 config.cardTemplateId / 默认流式模板。
93
+ */
94
+ cardTemplateId?: string;
95
+ /**
96
+ * 答案静态卡:finish 时跳过 INPUTING 过渡,直接 FINISHED(与 closeStreaming 答案卡一致)。
97
+ */
98
+ skipInputingWalk?: boolean;
58
99
  /**
59
100
  * 已在上层 processLocalImages 过则跳过,避免二次处理干扰 MediaIdTrace。
60
101
  */
@@ -873,13 +914,21 @@ export async function sendTextToDingTalk(params: {
873
914
  }
874
915
 
875
916
  const msgType: DingTalkMsgType = shouldSendAsMarkdown(text) ? "markdown" : "text";
876
- logMediaIdTrace("sendTextToDingTalk:API前", text, `msgType=${msgType}`, true);
917
+ const cardOpts = messageToolProactiveCardOptions(config);
918
+ logMediaIdTrace(
919
+ "sendTextToDingTalk:API前",
920
+ text,
921
+ `msgType=${msgType} messageAnswerCard=${isMessageAnswerCardEnabled(config)} useAICard=${cardOpts.useAICard}`,
922
+ true,
923
+ );
924
+ console.log(
925
+ `[DingTalk][MessageCard] sendTextToDingTalk | messageAnswerCard=${isMessageAnswerCardEnabled(config)} useAICard=${cardOpts.useAICard} tpl=${cardOpts.cardTemplateId || "-"}`,
926
+ );
877
927
 
878
928
  return sendProactive(config, targetParam, text, {
879
929
  msgType,
880
930
  replyToId,
881
- useAICard: false,
882
- fallbackToNormal: true,
931
+ ...cardOpts,
883
932
  skipProcessLocalImages: true,
884
933
  });
885
934
  }
@@ -992,8 +1041,7 @@ export async function sendMediaToDingTalk(params: {
992
1041
  await sendProactive(config, targetParam, text.trim(), {
993
1042
  msgType: shouldSendAsMarkdown(text) ? "markdown" : "text",
994
1043
  replyToId,
995
- useAICard: false,
996
- fallbackToNormal: true,
1044
+ ...messageToolProactiveCardOptions(config),
997
1045
  });
998
1046
  }
999
1047
  // 远程图可再尝试 photoURL 直链(部分环境可用)
@@ -1052,8 +1100,7 @@ export async function sendMediaToDingTalk(params: {
1052
1100
  await sendProactive(config, targetParam, caption, {
1053
1101
  msgType: shouldSendAsMarkdown(caption) ? "markdown" : "text",
1054
1102
  replyToId,
1055
- useAICard: false,
1056
- fallbackToNormal: true,
1103
+ ...messageToolProactiveCardOptions(config),
1057
1104
  });
1058
1105
  }
1059
1106
  if (isRemoteHttpUrl(mediaUrl)) {
@@ -1089,8 +1136,7 @@ export async function sendMediaToDingTalk(params: {
1089
1136
  const result = await sendProactive(config, targetParam, combined, {
1090
1137
  msgType: "markdown",
1091
1138
  replyToId,
1092
- useAICard: false,
1093
- fallbackToNormal: true,
1139
+ ...messageToolProactiveCardOptions(config),
1094
1140
  });
1095
1141
  return {
1096
1142
  ...result,
@@ -1102,8 +1148,7 @@ export async function sendMediaToDingTalk(params: {
1102
1148
  await sendProactive(config, targetParam, caption, {
1103
1149
  msgType: shouldSendAsMarkdown(caption) ? "markdown" : "text",
1104
1150
  replyToId,
1105
- useAICard: false,
1106
- fallbackToNormal: true,
1151
+ ...messageToolProactiveCardOptions(config),
1107
1152
  });
1108
1153
  }
1109
1154
  const result = await sendProactive(config, targetParam, mediaId, {
@@ -1130,8 +1175,7 @@ export async function sendMediaToDingTalk(params: {
1130
1175
  await sendProactive(config, targetParam, caption, {
1131
1176
  msgType: shouldSendAsMarkdown(caption) ? "markdown" : "text",
1132
1177
  replyToId,
1133
- useAICard: false,
1134
- fallbackToNormal: true,
1178
+ ...messageToolProactiveCardOptions(config),
1135
1179
  });
1136
1180
  }
1137
1181
 
@@ -1321,6 +1365,8 @@ async function sendProactiveInternal(
1321
1365
  msgType = "text",
1322
1366
  useAICard = true, // 默认启用 AI Card,让主动发送消息优先使用卡片形式
1323
1367
  fallbackToNormal = true, // 默认降级,AI Card 失败时自动回退到普通消息
1368
+ cardTemplateId,
1369
+ skipInputingWalk = false,
1324
1370
  log: externalLog,
1325
1371
  } = options;
1326
1372
 
@@ -1330,9 +1376,25 @@ async function sendProactiveInternal(
1330
1376
  // 如果启用 AI Card(媒体消息强制跳过)
1331
1377
  if (useAICard && !isMediaMessage) {
1332
1378
  try {
1333
- const card = await createAICardForTarget(config, target, externalLog);
1379
+ const card = await createAICardForTarget(
1380
+ config,
1381
+ target,
1382
+ externalLog,
1383
+ cardTemplateId,
1384
+ );
1334
1385
  if (card) {
1335
- await finishAICard(card, content, config, externalLog);
1386
+ // 答案静态卡:skipInputingWalk=true 直接 FINISHED;普通卡仍走 INPUTING 再定稿
1387
+ await finishAICard(
1388
+ card,
1389
+ content,
1390
+ config,
1391
+ externalLog,
1392
+ undefined,
1393
+ skipInputingWalk,
1394
+ );
1395
+ console.log(
1396
+ `[DingTalk][MessageCard] AI Card 发送成功 | outTrack=${card.cardInstanceId} tpl=${cardTemplateId || "default"} skipInputing=${skipInputingWalk}`,
1397
+ );
1336
1398
  return {
1337
1399
  ok: true,
1338
1400
  cardInstanceId: card.cardInstanceId,
@@ -1346,8 +1408,14 @@ async function sendProactiveInternal(
1346
1408
  usedAICard: false,
1347
1409
  };
1348
1410
  }
1411
+ console.warn(
1412
+ `[DingTalk][MessageCard] AI Card 创建失败,fallbackToNormal=${fallbackToNormal}`,
1413
+ );
1349
1414
  } catch (err: any) {
1350
1415
  externalLog?.error?.(`AI Card 发送失败: ${err.message}`);
1416
+ console.warn(
1417
+ `[DingTalk][MessageCard] AI Card 异常: ${err?.message || err},fallbackToNormal=${fallbackToNormal}`,
1418
+ );
1351
1419
  if (!fallbackToNormal) {
1352
1420
  return { ok: false, error: err.message, usedAICard: false };
1353
1421
  }