@jeik/dingtalk-connector 0.8.21-fix2 → 0.8.21-fix22
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/README.en.md +27 -3
- package/README.md +31 -5
- package/bin/dingtalk-connector.js +83 -16
- package/dist/entry-bundled.mjs +1 -1
- package/dist/{gateway-methods-C4tcgI7P.mjs → gateway-methods-CZlqdypF.mjs} +1 -1
- package/dist/{gateway-methods-Ci31A3vg.mjs → gateway-methods-Cq_QWaX1.mjs} +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +2 -2
- package/dist/{message-handler-DESzFFDc.mjs → message-handler-D9opHiWI.mjs} +107 -67
- package/dist/{messaging-B6l1sRvX.mjs → messaging-C5esCpZS.mjs} +59 -15
- package/dist/{runtime-DUgpo5zC.mjs → runtime-Bm8q6lDq.mjs} +12 -6
- package/openclaw.plugin.json +18 -0
- package/package.json +1 -1
- package/src/channel.ts +3 -4
- package/src/config/schema.ts +8 -0
- package/src/reply-dispatcher.ts +188 -52
- package/src/services/messaging/card.ts +50 -12
- package/src/services/reply-markers.ts +32 -31
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.
|
|
7
|
+
<p><strong>Current published release: <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.23</strong> (on npm; `latest` points to this version — includes every historical fix plus the community's exclusive enhancements. See "Installation" below).</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>
|
|
@@ -25,8 +25,13 @@
|
|
|
25
25
|
|
|
26
26
|
| Date | Tag | Update |
|
|
27
27
|
|------|------|--------|
|
|
28
|
-
| 2026-06-
|
|
29
|
-
| 2026-06-
|
|
28
|
+
| 2026-06-29 | 🐛 | **Fixed incomplete DingTalk AI Card streaming replies (e.g. "你...")**: switched to **deferred card creation** — accumulate streaming text first, only create the AI Card once the real reply tokens actually start arriving, eliminating the half-empty "你..." card |
|
|
29
|
+
| 2026-06-29 | 🐛 | **Fixed message-tool cards rendering empty content**: `finishAICard` was simplified to PUT FINISHED directly. That works for the reply-dispatcher path (card already streamed, `inputingStarted=true`) but breaks the message-tool path (`createAICardForTarget` → `finishAICard` on a fresh card, `inputingStarted=false`) — skipping the INPUTING transition made DingTalk not render the `content` field (blank card). `finishAICard` now only triggers an extra `streamAICard(..., /*finished*/ false)` walk through INPUTING + content write when `!inputingStarted`; `finished=false` avoids "fake-stream replay" and the already-streamed path (`inputingStarted=true`) is unchanged. **Upgrade:** `npm install -g @jeik/dingtalk-connector` |
|
|
30
|
+
| 2026-06-29 | ✨ | **Fixed webchat long-text never finishing due to DingTalk's bug-bounded streaming-card render speed**: introduced **answer-card mode (on by default)** — DingTalk replies now bind a streaming card plus an answer card (built-in). When the final answer exceeds `answerActToken` (default 600) tokens, the streaming card shows just "✅ Done thinking", and an independent **static answer card** carries the full reply, sidestepping DingTalk's fixed-speed streaming-card render bug. Short answers still render on the streaming card; long answers get a fast static reply without hurting UX on short replies. Template id and threshold are configurable (`answerCardTemplateId` / `answerActToken`) |
|
|
31
|
+
| 2026-06-29 | ✨ | **Streamed tool-call progress on the AI Card**: while a tool runs, the card streams `🔧 Calling tool: <name>` and then seamlessly switches to the reply once done. Closes the gap the official connector never handled for tool callbacks |
|
|
32
|
+
| 2026-06-29 | 🐛 | **Fixed premature AI Card render-stop when multiple answers / tool-call errors land in one turn**: dws and other tool-failure results (carrying `isError`/`isStatusNotice`) were occasionally taken as the final answer and stopped rendering early. Now excluded per OpenClaw's official rule — the upstream official `dingtalk-connector` never handled this; this community fork does, showing them transiently without counting as the answer |
|
|
33
|
+
| 2026-06-29 | 🔧 | **Major install wizard upgrade vs upstream**: supports **enhanced AI Card toggle, skip-when-config-exists, QR scan config, manual clientId/clientSecret entry, dws now updated to the latest version, detects and can disable a local plugin copy shadowing the npm version**. Wizard is now more robust, more discoverable, and won't overwrite existing setups |
|
|
34
|
+
| 2026-06-28 | 📦 | **Now published to the official npm registry**: package `@jeik/dingtalk-connector`, added one-command scan-to-install; `--force` overwrites for updates, no uninstall needed |
|
|
30
35
|
| 2026-06-28 | 🐛 | Fixed the connector mistaking an intermediate progress message for the final answer and ending AI Card rendering too early when the model emits multiple progress messages in one turn (card now finalized only at turn end) |
|
|
31
36
|
| 2026-05-14 | ✨ | Markdown image support for direct URLs and local paths, no download required |
|
|
32
37
|
| 2026-05-11 | 🔧 | AI Card flashing and repeated re-rendering caused by duplicate intermediate messages after Agent multi-round loop completes |
|
|
@@ -68,11 +73,30 @@ Full update log: [FIXES.md](FIXES.md)([🇨🇳 中文](FIXES.en.md))
|
|
|
68
73
|
| `cardContentVar` | Final response content variable, defaults to `msgContent` |
|
|
69
74
|
| `cardProcessVar` | Intermediate process (block status) variable, defaults to `cardContentVar` if not set |
|
|
70
75
|
| `cardToolVar` | Tool call output variable, not written to card if not set |
|
|
76
|
+
| `answerCard` | Answer-card mode switch, **on by default**; set `false` to disable |
|
|
77
|
+
| `answerActToken` | Answer-card trigger threshold (tokens), default `600`; final answer ≤ this stays on the original card, > this opens a separate answer card |
|
|
78
|
+
| `answerCardTemplateId` | Answer-card template ID, uses the built-in default if not set (must contain a `content` variable) |
|
|
71
79
|
|
|
72
80
|
> Card template must be created in [DingTalk Open Platform](https://open.dingtalk.com/) with matching variable fields.
|
|
73
81
|
|
|
74
82
|
---
|
|
75
83
|
|
|
84
|
+
## 🎯 Reply markers + Answer card + Tool progress (core enhancements)
|
|
85
|
+
|
|
86
|
+
These make the "process → final answer" rendering on DingTalk cleaner and more stable, working around DingTalk's official streaming AI Card bug:
|
|
87
|
+
|
|
88
|
+
- **Reply markers**: work with the `[-process-]` (process) / `[-final-]` (final) markers injected by [prompt-rewriter](https://www.npmjs.com/package/@jeik/prompt-rewriter).
|
|
89
|
+
- Process segments stream token-by-token; once `[-final-]` appears, streaming stops and the card is **finalized in one shot** (no DingTalk "fake-stream replay").
|
|
90
|
+
- Markers are **never visible to the user** (stripped before writing to the card) and take **priority over** OpenClaw's default fallback — preventing intermediate process text from being mistaken for the final answer and stopping rendering early.
|
|
91
|
+
- With no markers, behavior follows OpenClaw's default entirely.
|
|
92
|
+
- **Answer-card mode** (on by default): when the final answer exceeds `answerActToken` (default 600) tokens, the **streaming card finalizes to "✅ Done thinking"** and a separate **static answer card** carries the full reply — avoiding DingTalk's bug where a FINISHED streaming card keeps flickering/re-rendering. Short answers stay on the original card, no extra card.
|
|
93
|
+
- **Tool-call progress**: while the Agent calls a tool, the card streams `🔧 Calling tool: <name>`, then updates to the reply when the tool finishes.
|
|
94
|
+
- **Tool failures no longer mis-rendered**: failed tool results (carrying `isError`/`isStatusNotice`) are shown transiently but **never treated as the final answer**.
|
|
95
|
+
|
|
96
|
+
> Markers require the prompt-rewriter plugin; answer card / tool progress are built into the connector and work out of the box.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
76
100
|
## Why Fork?
|
|
77
101
|
|
|
78
102
|
Due to the sluggish pace of the official DingTalk connector's updates and bug fixes, this repository was forked to keep up with critical fixes.
|
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</strong>(已发布到 npm
|
|
7
|
+
<p><strong>当前发布版:<a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-fix22</strong>(已发布到 npm,`fix` 标签指向此版本,`latest` 仍为 0.8.21-fix20;修复版用 `@fix` 或显式版本)</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>
|
|
@@ -25,11 +25,16 @@
|
|
|
25
25
|
|
|
26
26
|
| 日期 | 标识 | 更新内容 |
|
|
27
27
|
|------|------|---------|
|
|
28
|
-
| 2026-06-
|
|
29
|
-
| 2026-06-
|
|
28
|
+
| 2026-06-29 | 🐛 | **修复钉钉 AI 卡片流式回复不完整 例 “你...” 的问题** :采用**延迟建卡**模式,让流式文本积累更多文本,把建卡时机放到真正的文本到来之后,再进行建卡流式卡片展示 |
|
|
29
|
+
| 2026-06-29 | 🐛 | **修复 message 工具发卡 content 为空**:`finishAICard` 简化后直接 PUT FINISHED,对 reply-dispatcher 路径(已流式过)无影响,但 message 工具走的「新建卡立刻 finish」路径(`createAICardForTarget` → `finishAICard`,`inputingStarted=false`)会跳过 INPUTING 状态过渡,导致钉钉不渲染 content(卡片空白)。`finishAICard` 现仅在 `!inputingStarted` 时先调一次 `streamAICard(..., /*finished*/ false)` 走完 INPUTING + 内容写入再 FINISHED(`finished=false` 避免触发"假流式回放",已流式过的路径 `inputingStarted=true` 完全不受影响)。**升级:** `npm install -g @jeik/dingtalk-connector@fix` |
|
|
30
|
+
| 2026-06-29 | ✨ | **修复 webchat 中最终答案已生成,但由于钉钉上游流式卡片的原因导致长文本一直渲染的问题**:新增**答案卡模式(默认开启)**,钉钉回复默认会绑定一张流式卡片和答案卡片(默认内置),最终答案如果 token > `answerActToken`(默认 600)时,原流式卡片直接显示"✅ 思考完成"、另投一张独立的**静态答案卡**,规避钉钉流式卡的官方固定速度渲染的 bug;短答案仍在原流式卡渲染。长文本新建答案卡快速回复,短文本回答不影响用户体验。模板id/阈值可配(`answerCardTemplateId` / `answerActToken`) |
|
|
31
|
+
| 2026-06-29 | ✨ | **增加 AI 卡片工具流式展示工具调用进度**:增加调用工具时原卡流式显示 `🔧 正在调用工具:<工具名>`,结束后正常更新为回复 本功能补齐官方连接器未处理 tool 回调的短板|
|
|
32
|
+
| 2026-06-29 | 🐛 | **修复多条文本回答、工具调用错误等异常情况导致 AI流式卡片提前停止渲染的问题,**:dws 等工具失败结果(带 `isError`/`isStatusNotice`)以前偶发被当最终答案、提前停渲染;现按 OpenClaw 官方标准排除,但原版官方 dingtalk-connector 并没有考虑这方面,本社区版已完美修复支持,仅展示不计入答案 |
|
|
33
|
+
| 2026-06-29 | 🔧 | **安装向导对比官方超级增强**:安装向导支持**增强版AI card、已有配置跳过、扫码配置、填入clientID和clientSecret配置、dws 改为更新最新的 latest 版本、检测并可禁用遮蔽 npm 版的本地插件副本等**,安装向导更健壮 更易懂 更不会覆盖|
|
|
34
|
+
| 2026-06-28 | 📦 | **本仓库已上线npm官方仓库包**:`@jeik/dingtalk-connector`,新增一键扫码安装命令;`--force` 覆盖更新无需卸载 |
|
|
30
35
|
| 2026-06-28 | 🐛 | 修复模型一轮内发送多条过程消息时,连接器把中间过程消息当成最终答案、提前结束 AI Card 渲染的问题(改为整轮结束才定稿卡片) |
|
|
31
|
-
| 2026-05-14 | ✨ | Markdown
|
|
32
|
-
| 2026-05-11 | 🔧 | Agent 多轮循环完成后,中间过程消息重复发送到钉钉对话,造成刷屏和 AI Card
|
|
36
|
+
| 2026-05-14 | ✨ | **Markdown 图片发送支持直链和本地路径,无需下载到本地,请参考下列提示词** |
|
|
37
|
+
| 2026-05-11 | 🔧 | **Agent 多轮循环完成后,中间过程消息重复发送到钉钉对话,造成刷屏和 AI Card 倒放重渲染** |
|
|
33
38
|
| 2026-05-11 | 🐛 | OpenClaw 4.29+ 版本导致钉钉插件失效,群聊 @Agent 回复显示"✅ 任务执行完成(无文本输出)" |
|
|
34
39
|
| 2026-05-08 | 🌐 | 未注册的 Pong 监听器导致的 WebSocket 幻影重连,来源于 [PR #566](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/pull/566)([Majorshi](https://github.com/Majorshi) 提交) |
|
|
35
40
|
|
|
@@ -104,9 +109,28 @@
|
|
|
104
109
|
| `cardContentVar` | 最终回复内容变量名,不填默认 `msgContent` |
|
|
105
110
|
| `cardProcessVar` | 中间过程(block 状态)变量名,不填默认使用 `cardContentVar` |
|
|
106
111
|
| `cardToolVar` | 工具调用输出变量名,不填则不写入卡片 |
|
|
112
|
+
| `answerCard` | 答案卡模式开关,**默认开启**;显式设 `false` 关闭 |
|
|
113
|
+
| `answerActToken` | 答案卡触发阈值(token),默认 `600`;最终答案 ≤ 此值直接在原卡定稿,> 此值才另开答案卡 |
|
|
114
|
+
| `answerCardTemplateId` | 答案卡模板 ID,不填用内置默认模板(需含 `content` 变量) |
|
|
107
115
|
|
|
108
116
|
> 卡片模板需在[钉钉开放平台](https://open.dingtalk.com/)创建,并添加对应的变量字段。
|
|
109
117
|
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 🎯 回复标记 + 答案卡 + 工具进度(核心增强)
|
|
121
|
+
|
|
122
|
+
这套机制让钉钉侧的「过程 → 最终答案」渲染更干净、更稳定,规避钉钉流式 AI Card 的官方渲染 bug:
|
|
123
|
+
|
|
124
|
+
- **回复标记**:配合 [prompt-rewriter](https://www.npmjs.com/package/@jeik/prompt-rewriter) 注入的 `[-process-]`(过程段)/`[-final-]`(最终答案)标记。
|
|
125
|
+
- 过程段逐字流式滚动;出现 `[-final-]` 后**停止流式、一次性定稿**(去掉钉钉"假流式回放")。
|
|
126
|
+
- 标记对用户**完全不可见**(进卡前统一剥离),且**优先级高于** OpenClaw 默认兜底——避免中间过程被误判成最终答案、提前停渲染。
|
|
127
|
+
- 无标记时完全走 OpenClaw 默认逻辑。
|
|
128
|
+
- **答案卡模式**(默认开启):最终答案 token 超过 `answerActToken`(默认 600)时,**原流式卡定格"✅ 思考完成"**,另投一张**静态答案卡**承载完整回复——规避钉钉流式卡 FINISHED 后仍抖动/重渲染的 bug。短答案仍在原卡定稿,不多开卡。
|
|
129
|
+
- **工具调用进度**:Agent 调用工具时,原卡流式显示 `🔧 正在调用工具:<工具名>`,工具结束后正常更新为回复。
|
|
130
|
+
- **工具失败不再误判**:工具调用失败的结果(带 `isError`/`isStatusNotice`)只在卡片短暂展示,**不会被当成最终答案**。
|
|
131
|
+
|
|
132
|
+
> 标记功能需安装并启用 prompt-rewriter 插件;答案卡/工具进度为连接器内置,开箱即用。
|
|
133
|
+
|
|
110
134
|
**效果预览:**
|
|
111
135
|
|
|
112
136
|

|
|
@@ -156,6 +180,8 @@
|
|
|
156
180
|
|
|
157
181
|
```bash
|
|
158
182
|
npx -y @jeik/dingtalk-connector install
|
|
183
|
+
|
|
184
|
+
npx -y @jeik/dingtalk-connector --force ##强制安装 适用于本地已有 dingtalk-connector 的情况 第一条命令安装不了就用这个强制安装
|
|
159
185
|
```
|
|
160
186
|
|
|
161
187
|
**或仅安装插件**(自行配置凭证,见下方进阶文档):
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* node bin/dingtalk-connector.js install --local # local dev
|
|
8
8
|
*/
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync, renameSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import {
|
|
@@ -294,16 +294,68 @@ function saveCredentials(clientId, clientSecret, { isLocal = false, pluginInstal
|
|
|
294
294
|
|
|
295
295
|
// ── plugin install ─────────────────────────────────────────────
|
|
296
296
|
function getInstallSpec() {
|
|
297
|
-
//
|
|
297
|
+
// 永远钉住"本包自己的精确版本",让 openclaw 内部装的版本 == 你 npx 实际下到的版本。
|
|
298
|
+
// npx @fix / @0.8.21-fix7 / @latest / 不带 → 各自下到对应包 → 这里读到的就是那个版本。
|
|
299
|
+
// 不带版本号会让 openclaw 装 latest,导致"跑 fix 向导却装了正式版"那个坑。
|
|
298
300
|
try {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
if (version && /-(alpha|beta|rc|canary)/.test(version)) {
|
|
302
|
-
// prerelease → use exact version so openclaw accepts it
|
|
303
|
-
return `${PKG_NAME}@${version}`;
|
|
304
|
-
}
|
|
301
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
302
|
+
if (version) return `${PKG_NAME}@${version}`;
|
|
305
303
|
} catch {}
|
|
306
|
-
return PKG_NAME;
|
|
304
|
+
return PKG_NAME; // 兜底:读不到版本才退回不带版本(极少见)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 在 plugins.load.paths 扫描范围内找会"遮蔽"npm 版的本地 dingtalk 插件副本。
|
|
308
|
+
// 原理:load.paths 扫到的插件 origin=config,优先级(rank 0)高于 npm 安装的 global(rank 2),
|
|
309
|
+
// 所以一份本地克隆会永久压过 npm 版,--force 重装也没用。这里浅扫一层定位它。
|
|
310
|
+
// ponytail: 只扫 load.paths 下一层目录;克隆嵌套更深的极少见,需要再加深度。
|
|
311
|
+
function findShadowingLocalPlugin(paths = readConfig()?.plugins?.load?.paths) {
|
|
312
|
+
if (!Array.isArray(paths)) return null;
|
|
313
|
+
const npmRoot = join(homedir(), '.openclaw', 'npm');
|
|
314
|
+
for (const p of paths) {
|
|
315
|
+
if (typeof p !== 'string') continue;
|
|
316
|
+
const base = p.startsWith('~') ? join(homedir(), p.slice(1)) : p;
|
|
317
|
+
let entries;
|
|
318
|
+
try { entries = readdirSync(base, { withFileTypes: true }); } catch { continue; }
|
|
319
|
+
for (const e of entries) {
|
|
320
|
+
if (!e.isDirectory()) continue;
|
|
321
|
+
// OpenClaw 扫描会跳过这些名字(discovery shouldIgnoreScannedDirectory),它们不会遮蔽,
|
|
322
|
+
// 已禁用的 .disabled 目录也在此跳过,避免重复禁用成 .disabled.disabled。
|
|
323
|
+
const lower = e.name.toLowerCase();
|
|
324
|
+
if (lower.endsWith('.bak') || lower.includes('.backup-') || lower.includes('.disabled')) continue;
|
|
325
|
+
const dir = join(base, e.name);
|
|
326
|
+
if (dir.startsWith(npmRoot)) continue; // npm 安装目录本身不算遮蔽
|
|
327
|
+
try {
|
|
328
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
|
|
329
|
+
const channels = pkg?.openclaw?.channels;
|
|
330
|
+
if (pkg?.name === PKG_NAME || (Array.isArray(channels) && channels.includes(CHANNEL_ID))) {
|
|
331
|
+
return dir;
|
|
332
|
+
}
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 检测并(经确认后)禁用遮蔽 npm 版的本地副本:把目录重命名为 .disabled(可随时改回)。
|
|
340
|
+
async function resolvePluginShadow() {
|
|
341
|
+
const shadow = findShadowingLocalPlugin();
|
|
342
|
+
if (!shadow) return;
|
|
343
|
+
console.log('\n' + orange('⚠ 检测到本地插件副本会覆盖刚装的 npm 版:'));
|
|
344
|
+
console.log(dim(` ${shadow}`));
|
|
345
|
+
console.log(dim(' 它在 plugins.load.paths 扫描范围内,优先级高于 npm 版,网关会一直用这份旧本地代码。') + '\n');
|
|
346
|
+
const ans = await askUserConfirmation(' 是否禁用它(重命名为 .disabled,随时可改回)?[Y/n] ');
|
|
347
|
+
if (ans === 'n' || ans === 'no') {
|
|
348
|
+
console.log(dim(' 已跳过。网关仍会用本地版;如需用 npm 版,请手动移走该目录或从 plugins.load.paths 移除其父路径。') + '\n');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const disabled = shadow + '.disabled';
|
|
352
|
+
try {
|
|
353
|
+
if (existsSync(disabled)) rmSync(disabled, { recursive: true, force: true });
|
|
354
|
+
renameSync(shadow, disabled);
|
|
355
|
+
console.log(green(` ✔ 已禁用:${shadow} → ${disabled}`) + '\n');
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.log(red(` ⚠ 重命名失败:${err.message}。请手动处理该目录。`) + '\n');
|
|
358
|
+
}
|
|
307
359
|
}
|
|
308
360
|
|
|
309
361
|
function installPlugin(force = false) {
|
|
@@ -611,14 +663,21 @@ async function ensureDwsCli() {
|
|
|
611
663
|
const versionDisplay = installedVersion ? `v${installedVersion}` : 'unknown version';
|
|
612
664
|
|
|
613
665
|
if (!targetVersion) {
|
|
614
|
-
// @latest
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
console.log(
|
|
666
|
+
// @latest 模式:已安装则询问是否跳过更新(默认跳过,保留当前版本)
|
|
667
|
+
const ans = await askUserConfirmation(
|
|
668
|
+
` 检测到 dws CLI 已安装 (${versionDisplay}),是否跳过更新?(skip dws update?) [Y/n] `,
|
|
669
|
+
);
|
|
670
|
+
if (ans === 'n' || ans === 'no') {
|
|
671
|
+
console.log(dim(` 正在更新 dws CLI 到最新版...`) + '\n');
|
|
672
|
+
const ok = installDwsCli();
|
|
673
|
+
if (ok) {
|
|
674
|
+
const nv = getInstalledDwsVersion();
|
|
675
|
+
console.log(green(` ✔ dws CLI is now ${nv ? 'v' + nv : 'latest'}`) + '\n');
|
|
676
|
+
} else {
|
|
677
|
+
console.log(red(' ⚠ Update failed. Continuing with current version.') + '\n');
|
|
678
|
+
}
|
|
620
679
|
} else {
|
|
621
|
-
console.log(
|
|
680
|
+
console.log(dim(` 已跳过更新,保留当前版本 ${versionDisplay}。`) + '\n');
|
|
622
681
|
}
|
|
623
682
|
// 鉴权状态检查(与下方同逻辑)
|
|
624
683
|
if (isDwsAuthenticated()) {
|
|
@@ -731,10 +790,18 @@ Options:
|
|
|
731
790
|
globalThis['proc' + 'ess'].exit(1);
|
|
732
791
|
}
|
|
733
792
|
|
|
793
|
+
// 打印当前安装向导版本(= 本包版本),方便确认装的是哪一版
|
|
794
|
+
try {
|
|
795
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
796
|
+
console.log('\n' + cyan(`${PKG_NAME} v${version}`) + dim(' (install wizard)'));
|
|
797
|
+
} catch {}
|
|
798
|
+
|
|
734
799
|
// Step 1: Install connector plugin (unless --local)
|
|
735
800
|
let pluginInstalled = true;
|
|
736
801
|
if (!isLocal) {
|
|
737
802
|
pluginInstalled = installPlugin(force);
|
|
803
|
+
// 安装成功后检查是否有本地副本会遮蔽 npm 版(load.paths 优先级更高),有则确认禁用
|
|
804
|
+
if (pluginInstalled) await resolvePluginShadow();
|
|
738
805
|
} else {
|
|
739
806
|
console.log('\n' + dim('📦 --local mode: skipping plugin install') + '\n');
|
|
740
807
|
}
|
package/dist/entry-bundled.mjs
CHANGED
|
@@ -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-
|
|
26
|
+
const { registerGatewayMethods } = await import("./gateway-methods-Cq_QWaX1.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,
|
|
4
|
+
import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-C5esCpZS.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-
|
|
1
|
+
import { t as registerGatewayMethods } from "./gateway-methods-CZlqdypF.mjs";
|
|
2
2
|
export { registerGatewayMethods };
|
package/dist/index.d.mts
CHANGED
|
@@ -78,6 +78,9 @@ declare const DingtalkConfigSchema: z$1.ZodObject<{
|
|
|
78
78
|
cardContentVar: z$1.ZodDefault<z$1.ZodOptional<z$1.ZodString>>;
|
|
79
79
|
cardProcessVar: z$1.ZodOptional<z$1.ZodString>;
|
|
80
80
|
cardToolVar: z$1.ZodOptional<z$1.ZodString>;
|
|
81
|
+
answerCard: z$1.ZodOptional<z$1.ZodBoolean>;
|
|
82
|
+
answerActToken: z$1.ZodOptional<z$1.ZodNumber>;
|
|
83
|
+
answerCardTemplateId: z$1.ZodOptional<z$1.ZodString>;
|
|
81
84
|
enabled: z$1.ZodOptional<z$1.ZodBoolean>;
|
|
82
85
|
name: z$1.ZodOptional<z$1.ZodString>;
|
|
83
86
|
clientId: z$1.ZodOptional<z$1.ZodUnion<readonly [z$1.ZodString, z$1.ZodNumber]>>;
|
|
@@ -133,6 +136,9 @@ declare const DingtalkConfigSchema: z$1.ZodObject<{
|
|
|
133
136
|
cardContentVar: z$1.ZodDefault<z$1.ZodOptional<z$1.ZodString>>;
|
|
134
137
|
cardProcessVar: z$1.ZodOptional<z$1.ZodString>;
|
|
135
138
|
cardToolVar: z$1.ZodOptional<z$1.ZodString>;
|
|
139
|
+
answerCard: z$1.ZodOptional<z$1.ZodBoolean>;
|
|
140
|
+
answerActToken: z$1.ZodOptional<z$1.ZodNumber>;
|
|
141
|
+
answerCardTemplateId: z$1.ZodOptional<z$1.ZodString>;
|
|
136
142
|
enabled: z$1.ZodOptional<z$1.ZodBoolean>;
|
|
137
143
|
defaultAccount: z$1.ZodOptional<z$1.ZodString>;
|
|
138
144
|
clientId: z$1.ZodOptional<z$1.ZodUnion<readonly [z$1.ZodString, z$1.ZodNumber]>>;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-
|
|
2
|
-
import { t as registerGatewayMethods } from "./gateway-methods-
|
|
1
|
+
import { a as initDingtalkPluginConfigSchema, i as dingtalkPlugin, n as setDingtalkRuntime } from "./runtime-Bm8q6lDq.mjs";
|
|
2
|
+
import { t as registerGatewayMethods } from "./gateway-methods-CZlqdypF.mjs";
|
|
3
3
|
//#region index.ts
|
|
4
4
|
/**
|
|
5
5
|
* 检测同一 plugin id 在多个路径被加载的情况。
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { u as uploadMediaToDingTalk } from "./media-cz72EVS3.mjs";
|
|
2
2
|
import { a as resolveDingtalkAccount } from "./accounts-BAzdqkAV.mjs";
|
|
3
|
-
import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-
|
|
3
|
+
import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-Bm8q6lDq.mjs";
|
|
4
4
|
import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
|
|
5
5
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
6
6
|
import { i as getOapiAccessToken } from "./utils-CIfI_3Jh.mjs";
|
|
7
|
-
import { a as sendTextMessage,
|
|
7
|
+
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-C5esCpZS.mjs";
|
|
8
8
|
import { a as QUEUE_BUSY_ACK_PHRASES, n as normalizeSlashCommand, t as buildSessionContext } from "./session-DJ4jYqPv.mjs";
|
|
9
9
|
import { d as recallEmotionReply, o as getAccessToken, r as addEmotionReply, s as getOapiAccessToken$1, t as DINGTALK_API } from "./utils-legacy-CALCPP1t.mjs";
|
|
10
10
|
import "./chunk-upload-BBQgGtcZ.mjs";
|
|
@@ -204,42 +204,6 @@ async function uploadAndReplaceFileMarkers(content, sessionWebhook, config, oapi
|
|
|
204
204
|
return result;
|
|
205
205
|
}
|
|
206
206
|
//#endregion
|
|
207
|
-
//#region src/services/reply-markers.ts
|
|
208
|
-
const PROCESS_TAG = "[-process-]";
|
|
209
|
-
const FINAL_TAG = "[-final-]";
|
|
210
|
-
const EDGE_WS = /^[ \t\r\n]+|[ \t\r\n]+$/g;
|
|
211
|
-
/** 取最终答案:去掉那一个 [-final-](开头/中间/结尾都行),其余原样,首尾空白清掉。
|
|
212
|
-
* 没有 [-final-] 返回 null。多处出现取最后一个当信号。 */
|
|
213
|
-
function extractFinal(text) {
|
|
214
|
-
if (!text) return null;
|
|
215
|
-
const i = text.lastIndexOf(FINAL_TAG);
|
|
216
|
-
if (i < 0) return null;
|
|
217
|
-
return (text.slice(0, i) + text.slice(i + 9)).replace(EDGE_WS, "");
|
|
218
|
-
}
|
|
219
|
-
/** 剥掉一个标记 token([-process-] 或 [-final-],位置无关,取最后一个),其余原样,首尾空白清掉。 */
|
|
220
|
-
function stripOneMarker(text) {
|
|
221
|
-
if (!text) return "";
|
|
222
|
-
const pi = text.lastIndexOf(PROCESS_TAG);
|
|
223
|
-
const fi = text.lastIndexOf(FINAL_TAG);
|
|
224
|
-
if (pi < 0 && fi < 0) return text.replace(EDGE_WS, "");
|
|
225
|
-
const [i, len] = fi >= pi ? [fi, 9] : [pi, 11];
|
|
226
|
-
return (text.slice(0, i) + text.slice(i + len)).replace(EDGE_WS, "");
|
|
227
|
-
}
|
|
228
|
-
/** 去掉流式途中末尾未闭合的半截标记(如 "[-fin",缺右 "]")。完整标记交给上面两个函数。 */
|
|
229
|
-
function stripPartialTail(text) {
|
|
230
|
-
if (!text) return "";
|
|
231
|
-
return text.replace(/\[-[a-z]*-?$/i, "");
|
|
232
|
-
}
|
|
233
|
-
/** 定稿用:有 [-final-] 去掉它;否则剥一个标记。正文不动。 */
|
|
234
|
-
function finalClean(text) {
|
|
235
|
-
return extractFinal(text) ?? stripOneMarker(text);
|
|
236
|
-
}
|
|
237
|
-
/** 流式展示用:有 [-final-] 直接显示最终答案;否则剥一个标记 + 半截标记,正文不动。 */
|
|
238
|
-
function displayClean(text) {
|
|
239
|
-
const fin = extractFinal(text);
|
|
240
|
-
return fin !== null ? fin : stripPartialTail(stripOneMarker(text));
|
|
241
|
-
}
|
|
242
|
-
//#endregion
|
|
243
207
|
//#region src/reply-dispatcher.ts
|
|
244
208
|
const { createReplyPrefixOptions, createTypingCallbacks, logTypingFailure } = await import("openclaw/plugin-sdk/channel-runtime");
|
|
245
209
|
function createDingtalkReplyDispatcher(params) {
|
|
@@ -265,24 +229,34 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
265
229
|
let lastAnswerText = "";
|
|
266
230
|
let processMarkerLogged = false;
|
|
267
231
|
let finalMarkerLogged = false;
|
|
268
|
-
|
|
232
|
+
let markerSystemActive = false;
|
|
233
|
+
const isNonAnswerPayload = (p) => Boolean(p) && Boolean(p.isError || p.isStatusNotice || p.isCompactionNotice || p.isFallbackNotice);
|
|
234
|
+
const observeReply = (raw, payload) => {
|
|
269
235
|
const text = raw ?? "";
|
|
270
236
|
if (!text) return;
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
log.info(`[DingTalk][marker] 检测到过程标记 ${PROCESS_TAG}(已剥离,不展示给用户)`);
|
|
274
|
-
}
|
|
237
|
+
if (isNonAnswerPayload(payload)) return;
|
|
238
|
+
const isReasoning = payload?.isReasoning;
|
|
275
239
|
const fin = extractFinal(text);
|
|
276
240
|
if (fin !== null) {
|
|
277
241
|
if (!finalMarkerLogged) {
|
|
278
242
|
finalMarkerLogged = true;
|
|
279
|
-
log.info(`[DingTalk][marker]
|
|
243
|
+
log.info(`[DingTalk][marker] 检测到 ${FINAL_TAG},最终答案=剥光标记的完整正文,跳过 OpenClaw 兜底`);
|
|
280
244
|
}
|
|
281
245
|
finalMarkedText = fin;
|
|
246
|
+
markerSystemActive = true;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (text.includes("[-process-]")) {
|
|
250
|
+
if (!processMarkerLogged) {
|
|
251
|
+
processMarkerLogged = true;
|
|
252
|
+
log.info(`[DingTalk][marker] 检测到 ${PROCESS_TAG}(过程段),抑制 OpenClaw 默认兜底`);
|
|
253
|
+
}
|
|
254
|
+
markerSystemActive = true;
|
|
255
|
+
return;
|
|
282
256
|
}
|
|
283
|
-
if (!isReasoning && text.trim()) lastAnswerText = text;
|
|
257
|
+
if (!markerSystemActive && !isReasoning && text.trim()) lastAnswerText = text;
|
|
284
258
|
};
|
|
285
|
-
const pickFinalText = () => finalMarkedText ??
|
|
259
|
+
const pickFinalText = () => markerSystemActive ? finalMarkedText ?? accumulatedText : lastAnswerText || accumulatedText;
|
|
286
260
|
const applyReplyTemplate = async (text) => {
|
|
287
261
|
try {
|
|
288
262
|
const runner = getGlobalHookRunner?.();
|
|
@@ -428,6 +402,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
428
402
|
log.info(`[DingTalk][closeStreaming] 开始关闭 AI Card...`);
|
|
429
403
|
try {
|
|
430
404
|
let finalText = finalClean(pickFinalText());
|
|
405
|
+
const hadRealAnswer = finalText.trim().length > 0;
|
|
431
406
|
log.info(`[DingTalk][closeStreaming] 最终答案来源=${finalMarkedText !== null ? "marker[-final-]" : lastAnswerText ? "非reasoning答案" : "accumulatedText兜底"},长度=${finalText.length}`);
|
|
432
407
|
if (!finalText.trim()) {
|
|
433
408
|
finalText = "✅ 任务执行完成(无文本输出)";
|
|
@@ -479,9 +454,45 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
479
454
|
log.warn(`[DingTalk][closeStreaming] 养成系统处理失败(不影响主流程): ${gamErr?.message || gamErr}`);
|
|
480
455
|
}
|
|
481
456
|
finalText = await applyReplyTemplate(finalText);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
457
|
+
if (account.config?.answerCard !== false) {
|
|
458
|
+
const finalizeOriginalToDone = async () => {
|
|
459
|
+
try {
|
|
460
|
+
await streamAICard(cardSnapshot, "✅ 思考完成", true, account.config, log);
|
|
461
|
+
await finishAICard(cardSnapshot, "✅ 思考完成", account.config, log);
|
|
462
|
+
} catch (e) {
|
|
463
|
+
log.warn(`[DingTalk][closeStreaming] 原卡定格思考完成失败(忽略):${e?.message || e}`);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const answerTplId = (account.config?.answerCardTemplateId)?.trim() || "d246b7f5-1783-4e9b-bb46-bef52d63050e.schema";
|
|
467
|
+
const answerActToken = Number(account.config?.answerActToken) || 600;
|
|
468
|
+
const answerTokens = estimateTokens(finalText);
|
|
469
|
+
if (!hadRealAnswer) {
|
|
470
|
+
log.info(`[DingTalk][closeStreaming] answerCard 模式:无真实答案,仅定格原卡思考完成(不建答案卡)`);
|
|
471
|
+
await finalizeOriginalToDone();
|
|
472
|
+
} else if (answerTokens <= answerActToken) {
|
|
473
|
+
log.info(`[DingTalk][closeStreaming] answerCard 模式:答案约 ${answerTokens} token ≤ ${answerActToken},原卡直接定稿(不建答案卡)`);
|
|
474
|
+
try {
|
|
475
|
+
await streamAICard(cardSnapshot, finalText, true, account.config, log);
|
|
476
|
+
} catch (e) {
|
|
477
|
+
if (!isQpsLimitError(e)) log.warn(`[DingTalk][closeStreaming] 原卡流式覆盖最终答案失败(忽略,继续 FINISHED):${e?.message || e}`);
|
|
478
|
+
}
|
|
479
|
+
await finishAICard(cardSnapshot, finalText, account.config, log);
|
|
480
|
+
} else {
|
|
481
|
+
log.info(`[DingTalk][closeStreaming] answerCard 模式:答案约 ${answerTokens} token > ${answerActToken},原卡思考完成 + 新建答案卡(模板=${answerTplId})`);
|
|
482
|
+
await finalizeOriginalToDone();
|
|
483
|
+
const answerCard = await createAICardForTarget(account.config, target, log, answerTplId);
|
|
484
|
+
if (answerCard) {
|
|
485
|
+
await finishAICard(answerCard, finalText, account.config, log, void 0, true);
|
|
486
|
+
log.info(`[DingTalk][closeStreaming] ✅ 答案卡投放成功`);
|
|
487
|
+
} else {
|
|
488
|
+
log.warn(`[DingTalk][closeStreaming] 答案卡创建失败,降级定稿原卡`);
|
|
489
|
+
await finishAICard(cardSnapshot, finalText, account.config, log);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
log.info(`[DingTalk][closeStreaming] 准备调用 finishAICard,文本长度=${finalText.length}`);
|
|
494
|
+
await finishAICard(cardSnapshot, finalText, account.config, log);
|
|
495
|
+
}
|
|
485
496
|
log.info(`[DingTalk][closeStreaming] ✅ AI Card 关闭成功`);
|
|
486
497
|
} catch (error) {
|
|
487
498
|
log.error(`[DingTalk][closeStreaming] ❌ AI Card 关闭失败:${error?.message || String(error)}`);
|
|
@@ -511,14 +522,28 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
511
522
|
lastAnswerText = "";
|
|
512
523
|
processMarkerLogged = false;
|
|
513
524
|
finalMarkerLogged = false;
|
|
514
|
-
|
|
525
|
+
markerSystemActive = false;
|
|
515
526
|
typingCallbacks.onActive?.();
|
|
516
527
|
},
|
|
517
528
|
deliver: async (payload, info) => {
|
|
518
529
|
let text = payload.text ?? "";
|
|
519
|
-
log.
|
|
520
|
-
|
|
521
|
-
|
|
530
|
+
log.debug(`[DingTalk][deliver] kind=${info?.kind}, textLength=${text.length}`);
|
|
531
|
+
observeReply(payload.text, payload);
|
|
532
|
+
if (isNonAnswerPayload(payload)) {
|
|
533
|
+
if (streamingEnabled && currentCardTarget && !asyncMode && finalMarkedText === null) {
|
|
534
|
+
const now = Date.now();
|
|
535
|
+
if (now - lastUpdateTime >= updateInterval) {
|
|
536
|
+
lastUpdateTime = now;
|
|
537
|
+
try {
|
|
538
|
+
await streamAICard(currentCardTarget, displayClean(text), false, account.config, log, account.config?.cardContentVar || "msgContent");
|
|
539
|
+
} catch (e) {
|
|
540
|
+
if (!isQpsLimitError(e)) log.warn(`[DingTalk][deliver] 状态/错误 payload 写卡失败:${e?.message || e}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
log.info(`[DingTalk][deliver] 非答案 payload(isError=${payload.isError},isStatusNotice=${payload.isStatusNotice}),仅展示不计入最终答案`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
522
547
|
if (info?.kind === "final" && text.trim()) {
|
|
523
548
|
const target = isDirect ? {
|
|
524
549
|
type: "user",
|
|
@@ -638,32 +663,30 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
638
663
|
replyOptions: {
|
|
639
664
|
...replyOptions,
|
|
640
665
|
onModelSelected,
|
|
666
|
+
allowToolLifecycleWhenProgressHidden: true,
|
|
667
|
+
suppressDefaultToolProgressMessages: true,
|
|
641
668
|
...streamingEnabled && { onPartialReply: async (payload) => {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (
|
|
645
|
-
log.debug(`[DingTalk][onPartialReply] 空文本,跳过`);
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
log.debug(`[DingTalk][onPartialReply] 收到部分响应,文本长度=${payload.text.length}`);
|
|
649
|
-
observeReply(payload.text, payload.isReasoning);
|
|
669
|
+
if (!payload.text) return;
|
|
670
|
+
observeReply(payload.text, payload);
|
|
671
|
+
if (isNonAnswerPayload(payload)) return;
|
|
650
672
|
if (asyncMode) {
|
|
651
|
-
log.debug(`[DingTalk][onPartialReply] 异步模式,累积响应`);
|
|
652
673
|
asyncModeFullResponse = finalClean(finalMarkedText ?? payload.text);
|
|
653
674
|
return;
|
|
654
675
|
}
|
|
676
|
+
if (finalMarkedText !== null) {
|
|
677
|
+
accumulatedText = payload.text;
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
655
680
|
await startStreaming();
|
|
656
681
|
if (currentCardTarget) {
|
|
657
682
|
accumulatedText = payload.text;
|
|
658
683
|
const now = Date.now();
|
|
659
684
|
if (now - lastUpdateTime >= updateInterval) {
|
|
660
685
|
const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import("./common-C8pYKU_y.mjs");
|
|
661
|
-
const displayContent = displayClean(
|
|
662
|
-
log.debug(`[DingTalk][onPartialReply] 更新 AI Card,显示文本长度=${displayContent.length}`);
|
|
686
|
+
const displayContent = displayClean(accumulatedText).replace(FILE_MARKER_PATTERN, "").replace(VIDEO_MARKER_PATTERN, "").replace(AUDIO_MARKER_PATTERN, "").trim();
|
|
663
687
|
lastUpdateTime = now;
|
|
664
688
|
try {
|
|
665
689
|
await streamAICard(currentCardTarget, displayContent, false, account.config, log, account.config?.cardContentVar || "msgContent");
|
|
666
|
-
log.debug(`[DingTalk][onPartialReply] ✅ AI Card 更新成功`);
|
|
667
690
|
} catch (err) {
|
|
668
691
|
if (isQpsLimitError(err)) log.warn(`[DingTalk][onPartialReply] AI Card 流式更新遇到 QPS 限流,已在内部退避重试;本次跳过,等待下一次 partial 更新补齐内容`);
|
|
669
692
|
else {
|
|
@@ -671,9 +694,26 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
671
694
|
await sendFallbackErrorMessage("sendMessage", err.message);
|
|
672
695
|
}
|
|
673
696
|
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
676
699
|
} },
|
|
700
|
+
onToolStart: async (payload) => {
|
|
701
|
+
const toolName = (payload?.name || "").trim();
|
|
702
|
+
if (!toolName) return;
|
|
703
|
+
if (payload?.phase === "end" || payload?.phase === "complete") return;
|
|
704
|
+
if (!streamingEnabled || asyncMode || finalMarkedText !== null) return;
|
|
705
|
+
try {
|
|
706
|
+
await startStreaming();
|
|
707
|
+
if (!currentCardTarget) return;
|
|
708
|
+
const now = Date.now();
|
|
709
|
+
if (now - lastUpdateTime < updateInterval) return;
|
|
710
|
+
lastUpdateTime = now;
|
|
711
|
+
await streamAICard(currentCardTarget, `🔧 正在调用工具:${toolName}`, false, account.config, log, account.config?.cardContentVar || "msgContent");
|
|
712
|
+
log.debug(`[DingTalk][onToolStart] 工具进度写卡:${toolName}(phase=${payload?.phase})`);
|
|
713
|
+
} catch (e) {
|
|
714
|
+
if (!isQpsLimitError(e)) log.warn(`[DingTalk][onToolStart] 工具进度写卡失败:${e?.message || e}`);
|
|
715
|
+
}
|
|
716
|
+
},
|
|
677
717
|
onCommandOutput: (payload) => {
|
|
678
718
|
const dwsMatch = (payload.title || payload.name || "").match(DWS_PRODUCT_PATTERN) || payload.output?.match(DWS_PRODUCT_PATTERN);
|
|
679
719
|
if (dwsMatch) {
|