@dingtalk-real-ai/dingtalk-connector 0.8.20 → 0.8.21
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 +45 -0
- package/README.en.md +18 -2
- package/README.md +18 -2
- package/dist/{connection-BZd5NXuh.mjs → connection-D4uO_J9G.mjs} +33 -7
- package/dist/entry-bundled.mjs +1 -1
- package/dist/gateway-methods-B0_tBGPn.mjs +2 -0
- package/dist/{gateway-methods-DI8lkjSd.mjs → gateway-methods-BNuB2wXl.mjs} +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{media-DUMfXnwJ.mjs → media-BRqGsKUB.mjs} +8 -8
- package/dist/{media-DEuF7r3G.mjs → media-DD7Rlljd.mjs} +1 -1
- package/dist/{message-handler-_vk6QsWo.mjs → message-handler-CPGT1bgU.mjs} +74 -13
- package/dist/{messaging-CyIJY4h2.mjs → messaging-DQwrrd68.mjs} +90 -10
- package/dist/{runtime-b4xvqwW6.mjs → runtime-BphH7_vR.mjs} +5 -5
- package/dist/{utils-DY1gFCdU.mjs → utils-BqUoUOwd.mjs} +1 -1
- package/dist/utils-QEvgZ2uM.mjs +119 -0
- package/docs/RELEASE_NOTES_V0.8.21-beta.0.md +163 -0
- package/docs/RELEASE_NOTES_V0.8.21.md +154 -0
- package/docs/TROUBLESHOOTING.md +28 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/core/connection.ts +75 -13
- package/src/core/message-handler.ts +15 -1
- package/src/reply-dispatcher.ts +101 -5
- package/src/services/messaging/card.ts +117 -5
- package/src/utils/empty-reply.ts +72 -0
- package/src/utils/index.ts +1 -0
- package/dist/gateway-methods-DtdiDpYK.mjs +0 -2
- package/dist/utils-CIfI_3Jh.mjs +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.8.21] - 2026-05-19
|
|
11
|
+
|
|
12
|
+
晋升自 `0.8.21-beta.0` 的 GA 版本,与 beta.0 内容完全一致,经过社区验证后正式发布。
|
|
13
|
+
GA promotion of `0.8.21-beta.0` after community validation; functionally identical to the beta.
|
|
14
|
+
|
|
15
|
+
### 升级 / Upgrade
|
|
16
|
+
|
|
17
|
+
\`\`\`bash
|
|
18
|
+
npx openclaw@latest add @dingtalk-real-ai/dingtalk-connector@0.8.21
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
以下内容沿用自 `0.8.21-beta.0` 的修复与改进 / Same fixes and improvements as `0.8.21-beta.0`:
|
|
22
|
+
|
|
23
|
+
## [0.8.21-beta.0] - 2026-05-17
|
|
24
|
+
|
|
25
|
+
### 修复 / Fixes
|
|
26
|
+
- 🐛 **WebSocket phantom reconnect 根因修复 (#566)** — `setupPongListener` / `setupMessageListener` / `setupCloseListener` 原本在 `client.connect()` 之前调用,此时 `client.socket === undefined`,可选链让三个 listener 静默 no-op,从未真正挂上。pong 没人接 → `lastSocketAvailableTime` 不更新 → 命中 `TIMEOUT_THRESHOLD = 20s` → 约 30 秒一次的幽灵重连。本次把 setup 移到 `client.connect()` 之后(初次连接 + `doReconnect` 两条路径都覆盖),并放在 `await for OPEN` 之前,消除 race window。感谢 @Majorshi 贡献。
|
|
27
|
+
**Root-cause fix for WebSocket phantom reconnect (#566)** — `setupPongListener` / `setupMessageListener` / `setupCloseListener` were called before `client.connect()` when `client.socket === undefined`, so optional-chaining silently no-op'd and the three listeners were never attached. With no pong handler, `lastSocketAvailableTime` never refreshed, hit `TIMEOUT_THRESHOLD = 20s`, and triggered a phantom reconnect every ~30s. This release moves the setup calls to right after `client.connect()` (covering both initial connect and `doReconnect`), before the OPEN await, eliminating the race window. Credit: @Majorshi.
|
|
28
|
+
|
|
29
|
+
- 🐛 **消息处理保活 interval 兜底 bug (#594)** — `markMessageProcessingStart` 启动的兜底定时器原本 30s 间隔,但 `TIMEOUT_THRESHOLD` 早已从 90s 降到 20s,30s 间隔无法在 AI 长任务期间防住超时(约 21s 就可能触发幽灵重连,下次刷新还要等 9 秒)。本次改为 15s 间隔(< TIMEOUT_THRESHOLD),让保活真正生效。
|
|
30
|
+
**Fix message-processing keepalive interval (#594)** — The fallback `setInterval` in `markMessageProcessingStart` was 30s, but `TIMEOUT_THRESHOLD` had since dropped from 90s to 20s — the 30s interval couldn't prevent the timeout during long AI tasks (could trigger phantom reconnect around the 21s mark, with the next refresh 9s away). This release changes the interval to 15s (< TIMEOUT_THRESHOLD) so the safety net actually works.
|
|
31
|
+
|
|
32
|
+
- 🐛 **过滤上游 SDK `console.info` 噪音 (#571 / #536 / #573)** — 上游 `dingtalk-stream@2.1.4` SDK 在 `client.cjs:138 / :185` 每次 `disconnect()` / `connect()` 时直接 `console.info("Disconnecting.")` / `connect success`,绕过 logger,在频繁重连场景下会刷屏。本次在 `src/core/connection.ts` 加 `silenceDingtalkStreamConsoleNoise()`:模块级一次性 patch `console.info`,**只过滤这两条精确字符串**,其他 `console.info` 不受影响。同时在首个账号连上时通过 `printConnectionNoticeOnce()` 打印一次连接生命周期说明,解释过滤动机以及"高频重连不正常"的预期。
|
|
33
|
+
**Filter upstream SDK `console.info` noise (#571 / #536 / #573)** — Upstream `dingtalk-stream@2.1.4` SDK calls `console.info("Disconnecting.")` / `connect success` directly (`client.cjs:138 / :185`) on every `disconnect()` / `connect()`, bypassing the logger and spamming logs in frequent-reconnect scenarios. This release adds `silenceDingtalkStreamConsoleNoise()` in `src/core/connection.ts`: a module-level one-time `console.info` patch that filters **only these two exact strings**, leaving other `console.info` untouched. It also prints a one-time connection-lifecycle notice via `printConnectionNoticeOnce()` on first connect explaining the filter and setting the expectation that high-frequency reconnects are not normal.
|
|
34
|
+
|
|
35
|
+
### 改进 / Improvements
|
|
36
|
+
- 🩹 **群聊空回复兜底文案带修复指引 (#589)** — 当 OpenClaw `messages.groupChat.visibleReplies` 未设为 `"automatic"` 时,群聊 @ 机器人会因上游 `source-reply-delivery-mode.ts` 走 `message_tool_only` 而拿不到流式 token,最终落到 connector 空回复兜底;以前固定文案「任务执行完成(无文本输出)」无信息量,现群聊场景改为带 `openclaw.json` 配置修复指引的提示,并在 warn 日志中打印完整片段;同时在 `onIdle` / `onError` 时若本轮无任何用户可见输出,主动 nudge 一条配置指引,覆盖上游根本不调 `deliver()` 的盲区。单聊文案保持不变。
|
|
37
|
+
**Group-chat empty-reply fallback now ships actionable hint (#589)** — When OpenClaw’s `messages.groupChat.visibleReplies` is not `"automatic"`, group `@` mentions hit `message_tool_only` upstream and the connector’s accumulated text stays empty, falling through to a cold fallback. The group-chat fallback message now embeds the exact `openclaw.json` fix snippet, and the warn-level log prints the full hint for operators. A new idle nudge in `onIdle` / `onError` proactively sends the same hint when no user-visible output happens this turn — covering the case where upstream never calls `deliver()` at all. DM fallback text unchanged.
|
|
38
|
+
|
|
39
|
+
### 文档 / Docs
|
|
40
|
+
- 📚 **TROUBLESHOOTING (#589)** — 新增「群聊 @ 机器人只返回『任务执行完成(无文本输出)』」条目,给出 `messages.groupChat.visibleReplies = "automatic"` 修复步骤。
|
|
41
|
+
**TROUBLESHOOTING (#589)** — Added "Group `@` only returns 'Task done (no text output)'" entry with the `messages.groupChat.visibleReplies = "automatic"` fix.
|
|
42
|
+
|
|
43
|
+
- 📚 **`silenceDingtalkStreamConsoleNoise` 文案与函数命名收紧 (#592)** — `printLoadBalanceNoticeOnce` → `printConnectionNoticeOnce`,banner 文案改为「SDK noise 已过滤 + 真实重连 connector 自动处理 + 高频重连不正常」的预期;相关注释同步收紧,去掉对重连频率的强归因。
|
|
44
|
+
**Tighten copy & rename for `silenceDingtalkStreamConsoleNoise` (#592)** — Renamed `printLoadBalanceNoticeOnce` to `printConnectionNoticeOnce`; banner copy now reads "SDK noise filtered + real reconnects handled by connector + high-frequency reconnects are not expected"; related comments tightened.
|
|
45
|
+
|
|
46
|
+
- 📚 **清理 stale `90 秒` 文档残留 (#594)** — 文件头 docstring 与消息处理保活注释里的 `90 秒超时` → `20 秒超时`,与实际 `TIMEOUT_THRESHOLD` 保持一致。
|
|
47
|
+
**Clean up stale `90s` doc references (#594)** — File-header docstring and message-processing keepalive comments updated from `90s timeout` to `20s timeout`, matching the actual `TIMEOUT_THRESHOLD`.
|
|
48
|
+
|
|
49
|
+
### 说明 / Notes
|
|
50
|
+
- 不改动 `startKeepAlive` / `setupPongListener` / `lastSocketAvailableTime` 写入时机的语义,不影响 #437 的心跳超时检测修复。
|
|
51
|
+
No changes to `startKeepAlive` / `setupPongListener` / `lastSocketAvailableTime` write semantics — does not affect the #437 heartbeat-timeout fix.
|
|
52
|
+
|
|
8
53
|
## [0.8.20] - 2026-04-28
|
|
9
54
|
|
|
10
55
|
### 修复 / Fixes
|
package/README.en.md
CHANGED
|
@@ -104,11 +104,27 @@ openclaw gateway restart
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
+
## Feedback & Community
|
|
108
|
+
|
|
109
|
+
Before filing an Issue / PR, please skim the pinned notice — clear context helps us debug faster:
|
|
110
|
+
|
|
111
|
+
- English: [[READ FIRST] #585](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues/585)
|
|
112
|
+
- 中文:[【提 Issue 前必看】#584](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues/584)
|
|
113
|
+
|
|
114
|
+
Quick routing:
|
|
115
|
+
|
|
116
|
+
- **Other DingTalk product / open-platform capabilities** (outside this connector) → [DingTalk developer ticket center](https://applink.dingtalk.com/client/aiAgent?assistantId=381bb5860c264d33bc184c51db776fa7&from=development)
|
|
117
|
+
- **OpenClaw upstream** (gateway, models, channels, etc.) → [openclaw/openclaw Issues](https://github.com/openclaw/openclaw/issues)
|
|
118
|
+
- **`dingtalk-workspace-cli` (dws)** (DingTalk workspace CLI behavior) → [dingtalk-workspace-cli Issues](https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/issues)
|
|
119
|
+
- **This connector** → [GitHub Issues](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
107
123
|
## Contributing
|
|
108
124
|
|
|
109
|
-
|
|
125
|
+
PRs are welcome — small, well-described changes with clear verification land fastest; for larger changes, please open an Issue first. Doc-style reference: [#514](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/pull/514/changes) · code-style reference: [#581](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/pull/581).
|
|
110
126
|
|
|
111
|
-
|
|
127
|
+
> **Quarterly thanks**: each quarter, the top 3 PR contributors get a small gift or an invite to a DingTalk on-site visit (details per quarterly announcement). Thanks for co-building the community.
|
|
112
128
|
|
|
113
129
|
---
|
|
114
130
|
|
package/README.md
CHANGED
|
@@ -104,11 +104,27 @@ openclaw gateway restart
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
+
## 反馈与社区
|
|
108
|
+
|
|
109
|
+
提 Issue / PR 前,建议先看一眼置顶说明(信息完整能帮我们更快定位问题):
|
|
110
|
+
|
|
111
|
+
- 中文:[【提 Issue 前必看】#584](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues/584)
|
|
112
|
+
- English: [[READ FIRST] #585](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues/585)
|
|
113
|
+
|
|
114
|
+
简要分流:
|
|
115
|
+
|
|
116
|
+
- **钉钉其他业务能力**(非本 connector 范畴)→ [钉钉开发者侧工单入口](https://applink.dingtalk.com/client/aiAgent?assistantId=381bb5860c264d33bc184c51db776fa7&from=development)
|
|
117
|
+
- **OpenClaw 本体**(网关、模型、多通道等)→ [openclaw/openclaw Issues](https://github.com/openclaw/openclaw/issues)
|
|
118
|
+
- **dws(钉钉工作空间 CLI)** → [dingtalk-workspace-cli Issues](https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/issues)
|
|
119
|
+
- **本仓库 connector** → [GitHub Issues](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/issues)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
107
123
|
## 贡献
|
|
108
124
|
|
|
109
|
-
|
|
125
|
+
欢迎提交 Pull Request:改动越小、描述与验证步骤越清晰,越容易合入;较大改动建议先开 Issue 同步。文档类参考 [#514](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/pull/514/changes),代码类参考 [#581](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/pull/581)。
|
|
110
126
|
|
|
111
|
-
|
|
127
|
+
> **季度致谢**:每个季度对 PR 贡献量居前 3 位的伙伴提供小礼品或钉钉参观交流机会(细则以当季公告为准),感谢大家对社区共建的支持。
|
|
112
128
|
|
|
113
129
|
---
|
|
114
130
|
|
|
@@ -6,7 +6,7 @@ import * as fs from "fs";
|
|
|
6
6
|
*
|
|
7
7
|
* 职责:
|
|
8
8
|
* - 管理单个钉钉账号的 WebSocket 连接
|
|
9
|
-
* - 实现应用层心跳检测(10 秒间隔,
|
|
9
|
+
* - 实现应用层心跳检测(10 秒间隔,20 秒超时)
|
|
10
10
|
* - 处理连接重连逻辑,带指数退避
|
|
11
11
|
* - 消息去重(内置 Map,5 分钟 TTL)
|
|
12
12
|
*
|
|
@@ -23,9 +23,30 @@ const TIMEOUT_THRESHOLD = 20 * 1e3;
|
|
|
23
23
|
const BASE_BACKOFF_DELAY = 1e3;
|
|
24
24
|
/** 最大退避时间(毫秒) */
|
|
25
25
|
const MAX_BACKOFF_DELAY = 30 * 1e3;
|
|
26
|
+
let _streamNoiseSilenced = false;
|
|
27
|
+
function silenceDingtalkStreamConsoleNoise() {
|
|
28
|
+
if (_streamNoiseSilenced) return;
|
|
29
|
+
_streamNoiseSilenced = true;
|
|
30
|
+
const origConsoleInfo = console.info.bind(console);
|
|
31
|
+
console.info = (...args) => {
|
|
32
|
+
const first = args[0];
|
|
33
|
+
if (typeof first === "string") {
|
|
34
|
+
if (first === "Disconnecting.") return;
|
|
35
|
+
if (/^\[[^\]]+\] connect success$/.test(first)) return;
|
|
36
|
+
}
|
|
37
|
+
return origConsoleInfo(...args);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
let _connectionNoticePrinted = false;
|
|
41
|
+
function printConnectionNoticeOnce() {
|
|
42
|
+
if (_connectionNoticePrinted) return;
|
|
43
|
+
_connectionNoticePrinted = true;
|
|
44
|
+
console.log("[dingtalk-connector] ℹ️ 上游 dingtalk-stream SDK 的 `Disconnecting.` / `connect success` 日志已由本插件过滤;真实重连(网络抖动、服务端推 disconnect 等)由 connector 自动处理。正常运行下不应看到高频(≤30s)周期性重连,如有请提 issue。");
|
|
45
|
+
}
|
|
26
46
|
async function monitorSingleAccount(opts) {
|
|
27
47
|
const { cfg, account, runtime, abortSignal, messageHandler, onStatusChange } = opts;
|
|
28
48
|
const { accountId } = account;
|
|
49
|
+
silenceDingtalkStreamConsoleNoise();
|
|
29
50
|
const clawdbotConfig = cfg;
|
|
30
51
|
const log = runtime?.log;
|
|
31
52
|
const { createLoggerFromConfig } = await import("./logger-BeHWErmX.mjs");
|
|
@@ -75,7 +96,7 @@ async function monitorSingleAccount(opts) {
|
|
|
75
96
|
let messageProcessingKeepAliveTimer = null;
|
|
76
97
|
/**
|
|
77
98
|
* 标记消息处理开始,启动定期更新机制
|
|
78
|
-
*
|
|
99
|
+
* 在消息处理期间,定时刷新 lastSocketAvailableTime
|
|
79
100
|
* 防止长时间处理(如复杂的 AI 任务)触发心跳超时
|
|
80
101
|
*/
|
|
81
102
|
function markMessageProcessingStart() {
|
|
@@ -87,7 +108,7 @@ async function monitorSingleAccount(opts) {
|
|
|
87
108
|
lastSocketAvailableTime = Date.now();
|
|
88
109
|
logger.debug(`📝 消息处理中,更新 socket 可用时间`);
|
|
89
110
|
}
|
|
90
|
-
},
|
|
111
|
+
}, 15 * 1e3);
|
|
91
112
|
logger.debug(`📝 消息处理开始,启动活跃标记定时器`);
|
|
92
113
|
}
|
|
93
114
|
/**
|
|
@@ -126,6 +147,9 @@ async function monitorSingleAccount(opts) {
|
|
|
126
147
|
logger.info(`已断开旧连接`);
|
|
127
148
|
}
|
|
128
149
|
await client.connect();
|
|
150
|
+
setupPongListener();
|
|
151
|
+
setupMessageListener();
|
|
152
|
+
setupCloseListener();
|
|
129
153
|
if (!await new Promise((resolve) => {
|
|
130
154
|
const timeout = setTimeout(() => {
|
|
131
155
|
resolve(false);
|
|
@@ -180,6 +204,7 @@ async function monitorSingleAccount(opts) {
|
|
|
180
204
|
try {
|
|
181
205
|
const msg = JSON.parse(data);
|
|
182
206
|
if (msg.type === "SYSTEM" && msg.headers?.topic === "disconnect") {
|
|
207
|
+
logger.debug(`收到服务端 disconnect topic,即将重连`);
|
|
183
208
|
if (!isStopped && !isReconnecting) doReconnect(true).catch((err) => {
|
|
184
209
|
logger.error(`[${accountId}] 重连失败:${err.message}`);
|
|
185
210
|
});
|
|
@@ -263,9 +288,6 @@ async function monitorSingleAccount(opts) {
|
|
|
263
288
|
if (client.socket) client.socket.removeAllListeners();
|
|
264
289
|
logger.debug(`Connection 已停止`);
|
|
265
290
|
}
|
|
266
|
-
setupPongListener();
|
|
267
|
-
setupMessageListener();
|
|
268
|
-
setupCloseListener();
|
|
269
291
|
return new Promise(async (resolve, reject) => {
|
|
270
292
|
if (abortSignal) {
|
|
271
293
|
const onAbort = async () => {
|
|
@@ -367,9 +389,13 @@ async function monitorSingleAccount(opts) {
|
|
|
367
389
|
};
|
|
368
390
|
try {
|
|
369
391
|
await client.connect();
|
|
392
|
+
setupPongListener();
|
|
393
|
+
setupMessageListener();
|
|
394
|
+
setupCloseListener();
|
|
370
395
|
logger.info(`Connected to DingTalk Stream successfully`);
|
|
371
396
|
logger.info(`PID: ${process.pid}`);
|
|
372
|
-
logger.info(`✅ 自定义 keepAlive: true (10 秒心跳,
|
|
397
|
+
logger.info(`✅ 自定义 keepAlive: true (10 秒心跳,20 秒超时), 指数退避重连`);
|
|
398
|
+
printConnectionNoticeOnce();
|
|
373
399
|
onStatusChange?.({
|
|
374
400
|
connected: true,
|
|
375
401
|
lastConnectedAt: Date.now()
|
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-B0_tBGPn.mjs");
|
|
27
27
|
registerGatewayMethods(api);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as resolveDingtalkAccount, t as listDingtalkAccountIds } from "./accounts-CF4oK_HZ.mjs";
|
|
2
2
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { c as prepareMultiBotMentions, i as sendProactive, s as buildBotMentionTable, u as finishAICard } from "./messaging-
|
|
3
|
+
import { i as DINGTALK_API, o as getAccessToken } from "./utils-QEvgZ2uM.mjs";
|
|
4
|
+
import { c as prepareMultiBotMentions, i as sendProactive, s as buildBotMentionTable, u as finishAICard } from "./messaging-DQwrrd68.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 {
|
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-BphH7_vR.mjs";
|
|
2
|
+
import { t as registerGatewayMethods } from "./gateway-methods-BNuB2wXl.mjs";
|
|
3
3
|
//#region index.ts
|
|
4
4
|
/**
|
|
5
5
|
* 检测同一 plugin id 在多个路径被加载的情况。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as dingtalkOapiHttp, t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { a as DINGTALK_OAPI } from "./utils-QEvgZ2uM.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
@@ -287,7 +287,7 @@ async function extractAudioDuration(filePath, log) {
|
|
|
287
287
|
*/
|
|
288
288
|
async function sendVideoMessage(config, sessionWebhook, fileName, mediaId, log, metadata) {
|
|
289
289
|
try {
|
|
290
|
-
const token = await (await import("./utils-
|
|
290
|
+
const token = await (await import("./utils-BqUoUOwd.mjs")).getAccessToken(config);
|
|
291
291
|
const videoMessage = {
|
|
292
292
|
msgtype: "video",
|
|
293
293
|
video: {
|
|
@@ -315,8 +315,8 @@ async function sendVideoMessage(config, sessionWebhook, fileName, mediaId, log,
|
|
|
315
315
|
*/
|
|
316
316
|
async function sendVideoProactive(config, target, videoMediaId, picMediaId, metadata, log) {
|
|
317
317
|
try {
|
|
318
|
-
const token = await (await import("./utils-
|
|
319
|
-
const { DINGTALK_API } = await import("./utils-
|
|
318
|
+
const token = await (await import("./utils-BqUoUOwd.mjs")).getAccessToken(config);
|
|
319
|
+
const { DINGTALK_API } = await import("./utils-BqUoUOwd.mjs");
|
|
320
320
|
const msgParam = {
|
|
321
321
|
duration: metadata?.duration.toString() || "60000",
|
|
322
322
|
videoMediaId,
|
|
@@ -361,8 +361,8 @@ async function sendVideoProactive(config, target, videoMediaId, picMediaId, meta
|
|
|
361
361
|
*/
|
|
362
362
|
async function sendAudioProactive(config, target, fileName, mediaId, log, durationMs) {
|
|
363
363
|
try {
|
|
364
|
-
const token = await (await import("./utils-
|
|
365
|
-
const { DINGTALK_API } = await import("./utils-
|
|
364
|
+
const token = await (await import("./utils-BqUoUOwd.mjs")).getAccessToken(config);
|
|
365
|
+
const { DINGTALK_API } = await import("./utils-BqUoUOwd.mjs");
|
|
366
366
|
const msgParam = {
|
|
367
367
|
mediaId,
|
|
368
368
|
duration: durationMs && durationMs > 0 ? durationMs.toString() : "60000"
|
|
@@ -399,8 +399,8 @@ async function sendAudioProactive(config, target, fileName, mediaId, log, durati
|
|
|
399
399
|
*/
|
|
400
400
|
async function sendFileProactive(config, target, fileInfo, mediaId, log) {
|
|
401
401
|
try {
|
|
402
|
-
const token = await (await import("./utils-
|
|
403
|
-
const { DINGTALK_API } = await import("./utils-
|
|
402
|
+
const token = await (await import("./utils-BqUoUOwd.mjs")).getAccessToken(config);
|
|
403
|
+
const { DINGTALK_API } = await import("./utils-BqUoUOwd.mjs");
|
|
404
404
|
const resolvedFileName = fileInfo.fileName || path.basename(fileInfo.path);
|
|
405
405
|
const msgParam = {
|
|
406
406
|
mediaId,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as processVideoMarkers, i as processRawMediaPaths, l as toLocalPath, s as sendFileProactive } from "./media-
|
|
1
|
+
import { a as processVideoMarkers, i as processRawMediaPaths, l as toLocalPath, s as sendFileProactive } from "./media-BRqGsKUB.mjs";
|
|
2
2
|
export { processRawMediaPaths, processVideoMarkers, sendFileProactive, toLocalPath };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { u as uploadMediaToDingTalk } from "./media-
|
|
1
|
+
import { u as uploadMediaToDingTalk } from "./media-BRqGsKUB.mjs";
|
|
2
2
|
import { a as resolveDingtalkAccount } from "./accounts-CF4oK_HZ.mjs";
|
|
3
|
-
import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-
|
|
3
|
+
import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-BphH7_vR.mjs";
|
|
4
4
|
import { n as createLoggerFromConfig } from "./logger-BDWwViGT.mjs";
|
|
5
5
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { a as sendTextMessage, d as isQpsLimitError, f as streamAICard, i as sendProactive, l as createAICardForTarget, r as sendMessage, t as sendMarkdownMessage, u as finishAICard } from "./messaging-
|
|
6
|
+
import { n as groupChatLacksVisibleRepliesAutomatic, r as pickEmptyReplyFallbackText, s as getOapiAccessToken, t as emptyGroupReplyLogHint } from "./utils-QEvgZ2uM.mjs";
|
|
7
|
+
import { a as sendTextMessage, d as isQpsLimitError, f as streamAICard, i as sendProactive, l as createAICardForTarget, r as sendMessage, t as sendMarkdownMessage, u as finishAICard } from "./messaging-DQwrrd68.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-6p9cf3UB.mjs";
|
|
@@ -222,6 +222,10 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
222
222
|
let currentCardTarget = null;
|
|
223
223
|
let accumulatedText = "";
|
|
224
224
|
const deliveredFinalTexts = /* @__PURE__ */ new Set();
|
|
225
|
+
/** 本轮是否已向用户发出过可见回复(final / 流式更新 / 错误兜底等) */
|
|
226
|
+
let outboundUserVisibleThisTurn = false;
|
|
227
|
+
/** 防止 onIdle / onError 重复发送 visibleReplies 配置指引 */
|
|
228
|
+
let idleConfigNudgeSent = false;
|
|
225
229
|
let asyncModeFullResponse = "";
|
|
226
230
|
const detectedDwsProducts = /* @__PURE__ */ new Set();
|
|
227
231
|
const DWS_PRODUCT_PATTERN = /\bdws\s+(aitable|calendar|chat|contact|todo|approval|attendance|report|ding|workbench|devdoc)\b/;
|
|
@@ -257,6 +261,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
257
261
|
});
|
|
258
262
|
deliveredErrorTypes.add(errorKey);
|
|
259
263
|
lastErrorTime = now;
|
|
264
|
+
outboundUserVisibleThisTurn = true;
|
|
260
265
|
log.info(`[DingTalk][Fallback] ✅ 错误消息发送成功`);
|
|
261
266
|
} catch (fallbackErr) {
|
|
262
267
|
log.error(`[DingTalk][Fallback] ❌ 错误消息发送失败:${fallbackErr.message}`);
|
|
@@ -301,6 +306,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
301
306
|
log.info(`[DingTalk][startStreaming] 复用预创建 AI Card,cardInstanceId=${preCreatedCard.cardInstanceId}`);
|
|
302
307
|
currentCardTarget = preCreatedCard;
|
|
303
308
|
accumulatedText = "";
|
|
309
|
+
outboundUserVisibleThisTurn = true;
|
|
304
310
|
return;
|
|
305
311
|
}
|
|
306
312
|
log.info(`[DingTalk][startStreaming] 开始创建 AI Card...`);
|
|
@@ -338,8 +344,10 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
338
344
|
try {
|
|
339
345
|
let finalText = accumulatedText;
|
|
340
346
|
if (!finalText.trim()) {
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
const isGroup = !isDirect;
|
|
348
|
+
finalText = pickEmptyReplyFallbackText(isGroup);
|
|
349
|
+
log.info(`[DingTalk][closeStreaming] 累积文本为空,使用默认提示文案 (isGroup=${isGroup})`);
|
|
350
|
+
if (isGroup) log.warn?.(`[DingTalk][closeStreaming] ${emptyGroupReplyLogHint()}`);
|
|
343
351
|
}
|
|
344
352
|
const oapiToken = await getOapiAccessToken(account.config);
|
|
345
353
|
const target = isDirect ? {
|
|
@@ -356,7 +364,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
356
364
|
finalText = await processAudioMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
357
365
|
finalText = await uploadAndReplaceFileMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
358
366
|
log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
|
|
359
|
-
const { processRawMediaPaths } = await import("./media-
|
|
367
|
+
const { processRawMediaPaths } = await import("./media-DD7Rlljd.mjs");
|
|
360
368
|
finalText = await processRawMediaPaths(finalText, account.config, oapiToken, log, target);
|
|
361
369
|
log.info(`[DingTalk][closeStreaming] processRawMediaPaths 处理完成`);
|
|
362
370
|
} else log.warn(`[DingTalk][closeStreaming] oapiToken 为空,跳过媒体处理`);
|
|
@@ -389,6 +397,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
389
397
|
log.info(`[DingTalk][closeStreaming] 准备调用 finishAICard,文本长度=${finalText.length}`);
|
|
390
398
|
log.debug(`[DingTalk][closeStreaming] 最终发送内容长度=${finalText.length}`);
|
|
391
399
|
await finishAICard(cardSnapshot, finalText, account.config, log);
|
|
400
|
+
outboundUserVisibleThisTurn = true;
|
|
392
401
|
log.info(`[DingTalk][closeStreaming] ✅ AI Card 关闭成功`);
|
|
393
402
|
} catch (error) {
|
|
394
403
|
log.error(`[DingTalk][closeStreaming] ❌ AI Card 关闭失败:${error?.message || String(error)}`);
|
|
@@ -399,6 +408,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
399
408
|
useMarkdown: true,
|
|
400
409
|
log: params.runtime.log
|
|
401
410
|
});
|
|
411
|
+
outboundUserVisibleThisTurn = true;
|
|
402
412
|
log.info(`[DingTalk][closeStreaming] ✅ 降级发送成功`);
|
|
403
413
|
} catch (sendErr) {
|
|
404
414
|
log.error(`[DingTalk][closeStreaming] ❌ 降级发送失败:${sendErr.message}`);
|
|
@@ -407,12 +417,50 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
407
417
|
accumulatedText = "";
|
|
408
418
|
}
|
|
409
419
|
};
|
|
420
|
+
/**
|
|
421
|
+
* 群聊且 OpenClaw 未配置 `messages.groupChat.visibleReplies=automatic` 时,
|
|
422
|
+
* 若本轮结束时仍没有任何用户可见输出(上游可能未调用空 final 的 deliver),
|
|
423
|
+
* 补发与空 final 一致的配置指引,避免只有「思考中」却无声。
|
|
424
|
+
*/
|
|
425
|
+
const maybeSendGroupVisibleRepliesIdleNudge = async () => {
|
|
426
|
+
if (isDirect) return;
|
|
427
|
+
if (!groupChatLacksVisibleRepliesAutomatic(cfg)) return;
|
|
428
|
+
if (asyncMode) return;
|
|
429
|
+
if (outboundUserVisibleThisTurn) return;
|
|
430
|
+
if (idleConfigNudgeSent) return;
|
|
431
|
+
idleConfigNudgeSent = true;
|
|
432
|
+
log.info(`[DingTalk][idleNudge] 本轮无用户可见回复且群聊未启用 visibleReplies=automatic,发送配置指引`);
|
|
433
|
+
try {
|
|
434
|
+
const text = pickEmptyReplyFallbackText(true);
|
|
435
|
+
log.warn(`[DingTalk][idleNudge] ${emptyGroupReplyLogHint()}`);
|
|
436
|
+
for (const chunk of core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode)) if (isTextMode) if (groupReplyMode === "markdown") await sendMarkdownMessage(account.config, sessionWebhook, chunk.split("\n")[0]?.replace(/^[#*\s\->]+/, "").slice(0, 20) || "Message", chunk, {
|
|
437
|
+
cfg,
|
|
438
|
+
detectBareAliases: true
|
|
439
|
+
});
|
|
440
|
+
else await sendTextMessage(account.config, sessionWebhook, chunk, {
|
|
441
|
+
cfg,
|
|
442
|
+
detectBareAliases: true
|
|
443
|
+
});
|
|
444
|
+
else await sendMessage(account.config, sessionWebhook, chunk, {
|
|
445
|
+
useMarkdown: true,
|
|
446
|
+
log: params.runtime.log,
|
|
447
|
+
cfg,
|
|
448
|
+
detectBareAliases: true
|
|
449
|
+
});
|
|
450
|
+
outboundUserVisibleThisTurn = true;
|
|
451
|
+
log.info(`[DingTalk][idleNudge] ✅ 配置指引已发送`);
|
|
452
|
+
} catch (e) {
|
|
453
|
+
log.error(`[DingTalk][idleNudge] 发送失败: ${e?.message || e}`);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
410
456
|
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
411
457
|
...prefixOptions,
|
|
412
458
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
413
459
|
onReplyStart: () => {
|
|
414
460
|
log.info(`[DingTalk][onReplyStart] 开始回复,流式 enabled=${streamingEnabled}`);
|
|
415
461
|
deliveredFinalTexts.clear();
|
|
462
|
+
outboundUserVisibleThisTurn = false;
|
|
463
|
+
idleConfigNudgeSent = false;
|
|
416
464
|
if (streamingEnabled) startStreaming();
|
|
417
465
|
typingCallbacks.onActive?.();
|
|
418
466
|
},
|
|
@@ -432,7 +480,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
432
480
|
const oapiToken = await getOapiAccessToken(account.config);
|
|
433
481
|
if (oapiToken) {
|
|
434
482
|
log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
|
|
435
|
-
const { processRawMediaPaths } = await import("./media-
|
|
483
|
+
const { processRawMediaPaths } = await import("./media-DD7Rlljd.mjs");
|
|
436
484
|
text = await processRawMediaPaths(text, account.config, oapiToken, log, target);
|
|
437
485
|
log.info(`[DingTalk][deliver] 裸露文件路径处理完成`);
|
|
438
486
|
}
|
|
@@ -443,8 +491,10 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
443
491
|
const hasText = Boolean(text.trim());
|
|
444
492
|
const skipTextForDuplicateFinal = info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
|
|
445
493
|
if (info?.kind === "final" && !hasText) {
|
|
446
|
-
|
|
447
|
-
|
|
494
|
+
const isGroup = !isDirect;
|
|
495
|
+
text = pickEmptyReplyFallbackText(isGroup);
|
|
496
|
+
log.info(`[DingTalk][deliver] final 响应无文本,使用默认提示文案 (isGroup=${isGroup})`);
|
|
497
|
+
if (isGroup) log.warn?.(`[DingTalk][deliver] ${emptyGroupReplyLogHint()}`);
|
|
448
498
|
}
|
|
449
499
|
if (!(Boolean(text.trim()) && !skipTextForDuplicateFinal)) {
|
|
450
500
|
log.info(`[DingTalk][deliver] 跳过发送:hasText=${hasText}, skipTextForDuplicateFinal=${skipTextForDuplicateFinal}`);
|
|
@@ -468,6 +518,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
468
518
|
lastUpdateTime = now;
|
|
469
519
|
try {
|
|
470
520
|
await streamAICard(currentCardTarget, text, false, account.config, log);
|
|
521
|
+
outboundUserVisibleThisTurn = true;
|
|
471
522
|
log.info(`[DingTalk][deliver] ✅ block 更新到 AI Card 成功`);
|
|
472
523
|
} catch (streamErr) {
|
|
473
524
|
log.error(`[DingTalk][deliver] ❌ block 更新 AI Card 失败:${streamErr.message}`);
|
|
@@ -504,6 +555,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
504
555
|
cfg,
|
|
505
556
|
detectBareAliases: true
|
|
506
557
|
});
|
|
558
|
+
outboundUserVisibleThisTurn = true;
|
|
507
559
|
log.info(`[DingTalk][deliver] ✅ 非流式发送成功`);
|
|
508
560
|
deliveredFinalTexts.add(text);
|
|
509
561
|
} catch (error) {
|
|
@@ -519,11 +571,13 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
519
571
|
params.runtime.error?.(`dingtalk[${account.accountId}] ${info.kind} reply failed: ${String(error)}`);
|
|
520
572
|
await closeStreaming();
|
|
521
573
|
typingCallbacks.onIdle?.();
|
|
574
|
+
await maybeSendGroupVisibleRepliesIdleNudge();
|
|
522
575
|
},
|
|
523
576
|
onIdle: async () => {
|
|
524
577
|
log.info(`[DingTalk][onIdle] 回复空闲,关闭 AI Card`);
|
|
525
578
|
typingCallbacks.onIdle?.();
|
|
526
579
|
await closeStreaming();
|
|
580
|
+
await maybeSendGroupVisibleRepliesIdleNudge();
|
|
527
581
|
},
|
|
528
582
|
onCleanup: () => {
|
|
529
583
|
log.info(`[DingTalk][onCleanup] 清理回调`);
|
|
@@ -559,6 +613,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
559
613
|
lastUpdateTime = now;
|
|
560
614
|
try {
|
|
561
615
|
await streamAICard(currentCardTarget, displayContent, false, account.config, log);
|
|
616
|
+
outboundUserVisibleThisTurn = true;
|
|
562
617
|
log.debug(`[DingTalk][onPartialReply] ✅ AI Card 更新成功`);
|
|
563
618
|
} catch (err) {
|
|
564
619
|
if (isQpsLimitError(err)) log.warn(`[DingTalk][onPartialReply] AI Card 流式更新遇到 QPS 限流,已在内部退避重试;本次跳过,等待下一次 partial 更新补齐内容`);
|
|
@@ -1696,13 +1751,19 @@ async function handleDingTalkMessageInternal(params) {
|
|
|
1696
1751
|
finalText = await processVideoMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
1697
1752
|
finalText = await processAudioMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
1698
1753
|
finalText = await uploadAndReplaceFileMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
1699
|
-
const { processRawMediaPaths } = await import("./media-
|
|
1754
|
+
const { processRawMediaPaths } = await import("./media-DD7Rlljd.mjs");
|
|
1700
1755
|
finalText = await processRawMediaPaths(finalText, config, oapiToken, log, mediaTarget);
|
|
1701
1756
|
}
|
|
1702
|
-
|
|
1757
|
+
let textToSend = finalText.trim();
|
|
1758
|
+
if (!textToSend) {
|
|
1759
|
+
const isGroup = !isDirect;
|
|
1760
|
+
textToSend = pickEmptyReplyFallbackText(isGroup);
|
|
1761
|
+
if (isGroup) log?.warn?.(`[DingTalk][asyncMode] ${emptyGroupReplyLogHint()}`);
|
|
1762
|
+
}
|
|
1763
|
+
const title = textToSend.split("\n")[0]?.replace(/^[#*\s\->]+/, "").trim() || "消息";
|
|
1703
1764
|
await sendProactive(config, proactiveTarget, textToSend, {
|
|
1704
1765
|
msgType: "markdown",
|
|
1705
|
-
title
|
|
1766
|
+
title,
|
|
1706
1767
|
useAICard: false,
|
|
1707
1768
|
fallbackToNormal: true,
|
|
1708
1769
|
log
|