@jeik/dingtalk-connector 0.8.21-fix38 → 0.8.21-fix45
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 +23 -0
- package/README.en.md +11 -6
- package/README.md +12 -7
- package/dist/card-content-cache-B3aDF41h.mjs +106 -0
- package/dist/card-content-cache-BbyH0Mx9.mjs +2 -0
- package/dist/entry-bundled.mjs +1 -1
- package/dist/{gateway-methods-DiWw4D-z.mjs → gateway-methods-CCh-LutW.mjs} +1 -1
- package/dist/gateway-methods-b4qQpdOB.mjs +2 -0
- package/dist/index.mjs +2 -2
- package/dist/media-ClfWY5Cw.mjs +2 -0
- package/dist/{media-Cweidg5e.mjs → media-_qA1Qjt_.mjs} +162 -100
- package/dist/{message-handler-DalafQLy.mjs → message-handler-BKmyuozN.mjs} +118 -24
- package/dist/{messaging-Dl8eoTP3.mjs → messaging-CpOPEOI8.mjs} +21 -14
- package/dist/{runtime-CHszjL7M.mjs → runtime-Dj8jSHjE.mjs} +3 -3
- package/docs/RELEASE_NOTES_V0.8.21-fix39.md +63 -0
- package/docs/RELEASE_NOTES_V0.8.21-fix40.md +48 -0
- package/docs/RELEASE_NOTES_V0.8.21-fix45.md +45 -0
- package/package.json +1 -1
- package/src/core/message-handler.ts +191 -11
- package/src/reply-dispatcher.ts +54 -13
- package/src/services/media/common.ts +3 -4
- package/src/services/media.ts +318 -175
- package/src/services/messaging/card-content-cache.ts +180 -0
- package/src/services/messaging/card.ts +14 -0
- package/src/services/messaging.ts +31 -33
- package/dist/gateway-methods-CxMLTfaY.mjs +0 -2
- package/dist/media-i1phl0X3.mjs +0 -2
- /package/dist/{game-xiyou-NPHBMfUy.mjs → game-xiyou-CAT0ilSj.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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-fix45] - 2026-07-20
|
|
9
|
+
|
|
10
|
+
生产稳定版 / Production-stable community release。
|
|
11
|
+
|
|
12
|
+
### 修复 / Fixed
|
|
13
|
+
- 🐛 **message 图 + 下载链接被改成 mediaId** — 钉钉 `sampleMarkdown` 在「`` 与正文同一 URL」时会把下载位改写成 `@lADP…`。现:`` **下载上传 → mediaId**;**下载链接原 URL 留在同一条消息**(不拆第二条气泡)。
|
|
14
|
+
- 🐛 **只处理明确 `![]()`** — 删除裸路径自动扫描(与代码块误转 mediaId 同类根因)。
|
|
15
|
+
- 🐛 **closeStreaming 误报「仍含本地路径 MD 图」** — 代码块/示例 JSON 里的 `/tmp/...` 不再告警。
|
|
16
|
+
- 🐛 **引用 AI 卡片无正文** — 定稿缓存 `outTrackId → 正文`,引用时按会话回填(`CardCache`)。
|
|
17
|
+
- 🐛 **message `media=https://...`** — 远程 media 先下载再上传(fix38)。
|
|
18
|
+
|
|
19
|
+
### 日志 / Logs
|
|
20
|
+
- 精简 `[DingTalk][LocalImage]` / `[MediaIdTrace]`(去掉双前缀刷屏,保留 API 前关键行)。
|
|
21
|
+
|
|
22
|
+
### 安装 / Install
|
|
23
|
+
```bash
|
|
24
|
+
npx -y @jeik/dingtalk-connector install --force
|
|
25
|
+
openclaw gateway restart
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 从 fix38~fix44 升级
|
|
29
|
+
直接 `--force` 装 fix45 即可,包含上述全部修复。
|
|
30
|
+
|
|
8
31
|
## [0.8.21-fix38] - 2026-07-20
|
|
9
32
|
|
|
10
33
|
### 修复 / Fixed
|
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-
|
|
7
|
+
<p><strong>Current published release: <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-fix45</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,20 +23,25 @@
|
|
|
23
23
|
|
|
24
24
|
## 🔧 Recent Updates
|
|
25
25
|
|
|
26
|
-
### 🚀 v0.8.21-
|
|
26
|
+
### 🚀 v0.8.21-fix45 · 2026-07-20 (current)
|
|
27
27
|
|
|
28
|
-
**Theme:
|
|
28
|
+
**Theme: image + download link in one bubble · only `![]` · quote card cache**
|
|
29
29
|
|
|
30
30
|
| | Change |
|
|
31
31
|
|--|--------|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
32
|
+
| 📷 **Image + download URL** | `` → upload mediaId; **original download URL stays in the same message** |
|
|
33
|
+
| ✅ **Only `![]()`** | No bare-path scan; code-block paths not uploaded / no false grey-image warn |
|
|
34
|
+
| 📎 **Quoted AI cards** | Cache final card text for quote backfill |
|
|
35
|
+
| 🌐 **message media** | `media: "https://..."` download-then-upload |
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
npx -y @jeik/dingtalk-connector install --force && openclaw gateway restart
|
|
38
39
|
```
|
|
39
40
|
|
|
41
|
+
### 📦 v0.8.21-fix38 · 2026-07-20
|
|
42
|
+
|
|
43
|
+
Remote `media` download-then-upload.
|
|
44
|
+
|
|
40
45
|
### 📦 v0.8.21-fix37 · 2026-07-20
|
|
41
46
|
|
|
42
47
|
**Theme: local images (incl. `/mnt`) · message media layout · diagnostics**
|
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-
|
|
7
|
+
<p><strong>当前发布版:<a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-fix45</strong>(稳定生产可用;一键安装:`npx -y @jeik/dingtalk-connector install`;本地 tgz:`openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix45.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,20 +23,25 @@
|
|
|
23
23
|
|
|
24
24
|
## 🔧 最近更新
|
|
25
25
|
|
|
26
|
-
### 🚀 v0.8.21-
|
|
26
|
+
### 🚀 v0.8.21-fix45 · 2026-07-20(当前)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
**主题:图 + 下载链接单气泡 · 只认 `![]` · 引用卡片缓存**
|
|
29
29
|
|
|
30
30
|
| | 改动 |
|
|
31
31
|
|--|------|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
32
|
+
| 📷 **图 + 下载链** | `` 上传为 mediaId;**下载链接原 URL 留在同一条消息**(不被钉钉改成 `@lADP`) |
|
|
33
|
+
| ✅ **只处理 `![]()`** | 不扫裸路径;代码块内路径不上传、不误报灰图 |
|
|
34
|
+
| 📎 **引用 AI 卡** | 定稿缓存正文,引用时回填(`CardCache`) |
|
|
35
|
+
| 🌐 **message media** | `media: "https://..."` 下载再上传 |
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
npx -y @jeik/dingtalk-connector install --force && openclaw gateway restart
|
|
38
39
|
```
|
|
39
40
|
|
|
41
|
+
### 📦 v0.8.21-fix38 · 2026-07-20
|
|
42
|
+
|
|
43
|
+
message 远程 `media` 下载上传。
|
|
44
|
+
|
|
40
45
|
### 📦 v0.8.21-fix37 · 2026-07-20
|
|
41
46
|
|
|
42
47
|
**主题:本地图(含 `/mnt`)· message 文图策略 · 可观测日志**
|
|
@@ -59,7 +64,7 @@ npx -y @jeik/dingtalk-connector install --force && openclaw gateway restart
|
|
|
59
64
|
| 2026-06-28 | 上线 npm `@jeik/dingtalk-connector`;过程消息提前定稿修复 |
|
|
60
65
|
| 2026-05 | MD 直链/本地图;多轮刷屏;4.29+ 无文本输出;WebSocket 幻影重连 |
|
|
61
66
|
|
|
62
|
-
完整说明:[CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release
|
|
67
|
+
完整说明:[CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release fix45](docs/RELEASE_NOTES_V0.8.21-fix45.md)
|
|
63
68
|
|
|
64
69
|
---
|
|
65
70
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region src/services/messaging/card-content-cache.ts
|
|
2
|
+
const MAX_ENTRIES = 300;
|
|
3
|
+
/** 默认保留 7 天(引用历史卡片) */
|
|
4
|
+
const DEFAULT_TTL_MS = 10080 * 60 * 1e3;
|
|
5
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6
|
+
/** 每个会话最近若干条(新→旧) */
|
|
7
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
8
|
+
function pruneExpired(now = Date.now(), ttlMs = DEFAULT_TTL_MS) {
|
|
9
|
+
for (const [k, v] of byKey) if (now - v.at > ttlMs) byKey.delete(k);
|
|
10
|
+
if (byKey.size <= MAX_ENTRIES) return;
|
|
11
|
+
const sorted = [...byKey.entries()].sort((a, b) => a[1].at - b[1].at);
|
|
12
|
+
const drop = sorted.length - MAX_ENTRIES;
|
|
13
|
+
for (let i = 0; i < drop; i++) byKey.delete(sorted[i][0]);
|
|
14
|
+
}
|
|
15
|
+
function normalizeKey(kind, id) {
|
|
16
|
+
return `${kind}:${id.trim()}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 记住一张卡的终稿正文
|
|
20
|
+
*/
|
|
21
|
+
function rememberCardContent(params) {
|
|
22
|
+
const text = (params.text || "").trim();
|
|
23
|
+
if (!text) return;
|
|
24
|
+
const entry = {
|
|
25
|
+
text,
|
|
26
|
+
at: Date.now(),
|
|
27
|
+
conversationId: params.conversationId,
|
|
28
|
+
outTrackId: params.outTrackId,
|
|
29
|
+
msgId: params.msgId
|
|
30
|
+
};
|
|
31
|
+
pruneExpired();
|
|
32
|
+
if (params.outTrackId) byKey.set(normalizeKey("outTrack", params.outTrackId), entry);
|
|
33
|
+
if (params.msgId) byKey.set(normalizeKey("msgId", params.msgId), entry);
|
|
34
|
+
if (params.conversationId) {
|
|
35
|
+
const cid = params.conversationId;
|
|
36
|
+
const list = byConversation.get(cid) || [];
|
|
37
|
+
const ref = params.outTrackId || params.msgId || `t${entry.at}`;
|
|
38
|
+
const next = [ref, ...list.filter((x) => x !== ref)].slice(0, 20);
|
|
39
|
+
byConversation.set(cid, next);
|
|
40
|
+
byKey.set(normalizeKey("convRef", `${cid}|${ref}`), entry);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
console.log(`[DingTalk][CardCache] 已缓存 | outTrack=${params.outTrackId || "-"} conv=${params.conversationId || "-"} len=${text.length}`);
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 从引用载荷里可能出现的字段收集候选 id
|
|
48
|
+
*/
|
|
49
|
+
function collectCardLookupIds(repliedMsg, contentObj) {
|
|
50
|
+
const ids = [];
|
|
51
|
+
const push = (v) => {
|
|
52
|
+
if (typeof v === "string" && v.trim()) ids.push(v.trim());
|
|
53
|
+
};
|
|
54
|
+
push(contentObj?.outTrackId);
|
|
55
|
+
push(contentObj?.outTrackID);
|
|
56
|
+
push(contentObj?.cardInstanceId);
|
|
57
|
+
push(contentObj?.cardInstanceID);
|
|
58
|
+
push(contentObj?.trackId);
|
|
59
|
+
push(contentObj?.bizId);
|
|
60
|
+
push(contentObj?.cardBizId);
|
|
61
|
+
push(contentObj?.instanceId);
|
|
62
|
+
push(repliedMsg?.outTrackId);
|
|
63
|
+
push(repliedMsg?.cardInstanceId);
|
|
64
|
+
push(repliedMsg?.msgId);
|
|
65
|
+
push(repliedMsg?.msgID);
|
|
66
|
+
push(repliedMsg?.messageId);
|
|
67
|
+
const nests = [
|
|
68
|
+
contentObj?.cardData,
|
|
69
|
+
contentObj?.data,
|
|
70
|
+
contentObj?.bizData
|
|
71
|
+
];
|
|
72
|
+
for (const n of nests) {
|
|
73
|
+
if (!n || typeof n !== "object") continue;
|
|
74
|
+
push(n.outTrackId);
|
|
75
|
+
push(n.cardInstanceId);
|
|
76
|
+
push(n.trackId);
|
|
77
|
+
}
|
|
78
|
+
return [...new Set(ids)];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 查找缓存正文
|
|
82
|
+
*/
|
|
83
|
+
function lookupCardContent(params) {
|
|
84
|
+
pruneExpired();
|
|
85
|
+
const ids = params.ids || [];
|
|
86
|
+
for (const id of ids) {
|
|
87
|
+
const a = byKey.get(normalizeKey("outTrack", id));
|
|
88
|
+
if (a?.text) return a.text;
|
|
89
|
+
const b = byKey.get(normalizeKey("msgId", id));
|
|
90
|
+
if (b?.text) return b.text;
|
|
91
|
+
}
|
|
92
|
+
if (params.allowConversationRecent !== false && params.conversationId) {
|
|
93
|
+
const list = byConversation.get(params.conversationId) || [];
|
|
94
|
+
for (const ref of list) {
|
|
95
|
+
const e = byKey.get(normalizeKey("outTrack", ref)) || byKey.get(normalizeKey("msgId", ref)) || byKey.get(normalizeKey("convRef", `${params.conversationId}|${ref}`));
|
|
96
|
+
if (e?.text && e.text !== "✅ 思考完成") return e.text;
|
|
97
|
+
}
|
|
98
|
+
for (const ref of list) {
|
|
99
|
+
const e = byKey.get(normalizeKey("outTrack", ref)) || byKey.get(normalizeKey("msgId", ref)) || byKey.get(normalizeKey("convRef", `${params.conversationId}|${ref}`));
|
|
100
|
+
if (e?.text) return e.text;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { lookupCardContent as n, rememberCardContent as r, collectCardLookupIds as t };
|
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-b4qQpdOB.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-
|
|
4
|
+
import { c as prepareMultiBotMentions, d as finishAICard, i as sendProactive, s as buildBotMentionTable } from "./messaging-CpOPEOI8.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-Dj8jSHjE.mjs";
|
|
2
|
+
import { t as registerGatewayMethods } from "./gateway-methods-CCh-LutW.mjs";
|
|
3
3
|
//#region index.ts
|
|
4
4
|
/**
|
|
5
5
|
* 检测同一 plugin id 在多个路径被加载的情况。
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-_qA1Qjt_.mjs";
|
|
2
|
+
export { hasResidualLocalMdImagesOutsideCode, processLocalImages, processRawMediaPaths, processVideoMarkers, sendFileProactive, toLocalPath };
|
|
@@ -30,13 +30,20 @@ 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
|
-
/**
|
|
34
|
-
|
|
33
|
+
/** 是否像远程 URL 或其截断片段(禁止当本地路径上传) */
|
|
34
|
+
function looksLikeRemoteUrl(raw) {
|
|
35
|
+
const p = (raw || "").trim();
|
|
36
|
+
if (!p) return false;
|
|
37
|
+
if (/^https?:\/\//i.test(p)) return true;
|
|
38
|
+
if (/^[a-z]:\/\//i.test(p) && !/^[a-z]:[\\/][^/\\]/i.test(p)) return true;
|
|
39
|
+
if (p.includes("://")) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
35
42
|
/** 判断 markdown 图片 target 是否为需要上传的本地路径(非 http(s)/mediaId) */
|
|
36
43
|
function isLocalImageRef(rawPath) {
|
|
37
44
|
const p = (rawPath || "").trim();
|
|
38
45
|
if (!p) return false;
|
|
39
|
-
if (/^https?:\/\//i.test(p)) return false;
|
|
46
|
+
if (looksLikeRemoteUrl(p) || /^https?:\/\//i.test(p)) return false;
|
|
40
47
|
if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
|
|
41
48
|
if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) return true;
|
|
42
49
|
if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
|
|
@@ -61,15 +68,34 @@ function toLocalPath(raw) {
|
|
|
61
68
|
return filePath;
|
|
62
69
|
}
|
|
63
70
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
71
|
+
* 本地图诊断:默认只 console 一行,不再 log.warn(避免 [sendTextToDingTalk] 双前缀刷屏)。
|
|
72
|
+
* verbose 步骤(文件可读/realpath 等)仅 debug 时通过 log 输出。
|
|
66
73
|
*/
|
|
67
|
-
function logLocalImage(log, step, detail) {
|
|
74
|
+
function logLocalImage(log, step, detail, verbose = false) {
|
|
68
75
|
const line = detail ? `[DingTalk][LocalImage] ${step} | ${detail}` : `[DingTalk][LocalImage] ${step}`;
|
|
76
|
+
if (verbose) {
|
|
77
|
+
try {
|
|
78
|
+
log?.debug?.(line);
|
|
79
|
+
} catch {}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
69
82
|
console.log(line);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
}
|
|
84
|
+
/** 钉钉 mediaId 形态 */
|
|
85
|
+
const MEDIA_ID_RE = /@[A-Za-z0-9_-]{8,}/g;
|
|
86
|
+
/**
|
|
87
|
+
* 精简 MediaId 追踪:默认只在关键节点 / 异常时打一行。
|
|
88
|
+
* force=true 或发现「裸 mediaId」时才输出。
|
|
89
|
+
*/
|
|
90
|
+
function logMediaIdTrace(stage, content, extra, force = false) {
|
|
91
|
+
const text = typeof content === "string" ? content : "";
|
|
92
|
+
const ids = [...new Set([...text.matchAll(MEDIA_ID_RE)].map((m) => m[0]))];
|
|
93
|
+
const inMd = new Set([...text.matchAll(LOCAL_IMAGE_RE)].map((m) => (m[2] || "").trim()));
|
|
94
|
+
const bareIds = ids.filter((id) => !inMd.has(id));
|
|
95
|
+
const suspiciousBare = /type=image|sampleImage|photoURL/i.test(`${stage} ${extra || ""}`) ? [] : bareIds;
|
|
96
|
+
if (!force && suspiciousBare.length === 0 && !stage.includes("API前") && !stage.includes("完成")) return;
|
|
97
|
+
const preview = text.replace(/\n/g, "\\n").slice(0, 160);
|
|
98
|
+
console.log(`[DingTalk][MediaIdTrace] ${stage} | len=${text.length}` + (ids.length ? ` mediaIds=${ids.length}` : "") + (suspiciousBare.length ? ` ⚠️BARE=${suspiciousBare.join(",")}` : "") + (extra ? ` ${extra}` : "") + ` | ${preview}`);
|
|
73
99
|
}
|
|
74
100
|
/** Markdown 代码区域(围栏 ``` 与行内 `...`),其中的路径不应上传 */
|
|
75
101
|
function getMarkdownCodeRanges(content) {
|
|
@@ -98,21 +124,21 @@ async function uploadMediaToDingTalk(filePath, mediaType, oapiToken, maxSize = 2
|
|
|
98
124
|
const tag = `uploadMedia type=${mediaType}`;
|
|
99
125
|
try {
|
|
100
126
|
let absPath = toLocalPath(filePath);
|
|
101
|
-
logLocalImage(log, `${tag} 开始`, `path=${absPath}
|
|
127
|
+
logLocalImage(log, `${tag} 开始`, `path=${absPath}`, true);
|
|
102
128
|
if (!fs.existsSync(absPath)) {
|
|
103
129
|
logLocalImage(log, `${tag} 失败:文件不存在`, absPath);
|
|
104
130
|
return null;
|
|
105
131
|
}
|
|
106
132
|
try {
|
|
107
133
|
const real = fs.realpathSync(absPath);
|
|
108
|
-
if (real !== absPath) logLocalImage(log, `${tag} realpath`, `${absPath} → ${real}
|
|
134
|
+
if (real !== absPath) logLocalImage(log, `${tag} realpath`, `${absPath} → ${real}`, true);
|
|
109
135
|
absPath = real;
|
|
110
136
|
} catch (e) {
|
|
111
|
-
logLocalImage(log, `${tag} realpath跳过`, e?.message || String(e));
|
|
137
|
+
logLocalImage(log, `${tag} realpath跳过`, e?.message || String(e), true);
|
|
112
138
|
}
|
|
113
139
|
const stats = fs.statSync(absPath);
|
|
114
140
|
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
115
|
-
logLocalImage(log, `${tag} 文件可读`, `size=${fileSizeMB}MB
|
|
141
|
+
logLocalImage(log, `${tag} 文件可读`, `size=${fileSizeMB}MB`, true);
|
|
116
142
|
const effectiveMax = mediaType === "image" ? Math.min(maxSize, 10 * 1024 * 1024) : mediaType === "voice" ? Math.min(maxSize, 2 * 1024 * 1024) : maxSize;
|
|
117
143
|
if (stats.size > effectiveMax) {
|
|
118
144
|
const maxSizeMB = (effectiveMax / (1024 * 1024)).toFixed(0);
|
|
@@ -171,26 +197,17 @@ async function uploadMediaToDingTalk(filePath, mediaType, oapiToken, maxSize = 2
|
|
|
171
197
|
*/
|
|
172
198
|
async function uploadLocalImageWithFallback(filePath, oapiToken, log) {
|
|
173
199
|
const absPath = toLocalPath(filePath);
|
|
174
|
-
logLocalImage(log, "fallback 入口", `path=${absPath} exists=${fs.existsSync(absPath)}`);
|
|
175
200
|
if (!fs.existsSync(absPath)) {
|
|
176
201
|
logLocalImage(log, "fallback 失败:文件不存在", absPath);
|
|
177
202
|
return null;
|
|
178
203
|
}
|
|
179
|
-
logLocalImage(log, "fallback 第1次:直接上传", absPath);
|
|
180
204
|
let result = await uploadMediaToDingTalk(absPath, "image", oapiToken, 10 * 1024 * 1024, log);
|
|
181
|
-
if (result?.mediaId)
|
|
182
|
-
logLocalImage(log, "fallback 第1次成功", result.mediaId);
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
205
|
+
if (result?.mediaId) return result;
|
|
185
206
|
const ext = path.extname(absPath) || ".jpg";
|
|
186
207
|
const tmp = path.join(os.tmpdir(), `dingtalk-img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext.toLowerCase()}`);
|
|
187
208
|
try {
|
|
188
209
|
fs.copyFileSync(absPath, tmp);
|
|
189
|
-
const tmpSize = fs.statSync(tmp).size;
|
|
190
|
-
logLocalImage(log, "fallback 第2次:tmp拷贝后上传", `${absPath} → ${tmp} size=${tmpSize}`);
|
|
191
210
|
result = await uploadMediaToDingTalk(tmp, "image", oapiToken, 10 * 1024 * 1024, log);
|
|
192
|
-
if (result?.mediaId) logLocalImage(log, "fallback 第2次成功", result.mediaId);
|
|
193
|
-
else logLocalImage(log, "fallback 第2次仍失败", tmp);
|
|
194
211
|
return result;
|
|
195
212
|
} catch (err) {
|
|
196
213
|
logLocalImage(log, "fallback 拷贝/重试异常", `${absPath} err=${err?.message || err}`);
|
|
@@ -201,99 +218,144 @@ async function uploadLocalImageWithFallback(filePath, oapiToken, log) {
|
|
|
201
218
|
} catch {}
|
|
202
219
|
}
|
|
203
220
|
}
|
|
221
|
+
async function downloadRemoteUrlToTemp(url) {
|
|
222
|
+
try {
|
|
223
|
+
const res = await fetch(url, {
|
|
224
|
+
redirect: "follow",
|
|
225
|
+
signal: AbortSignal.timeout(6e4),
|
|
226
|
+
headers: { "User-Agent": "dingtalk-openclaw-connector/0.8.21" }
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
console.warn(`[DingTalk][MediaIdTrace] 远程图下载失败 HTTP ${res.status} | ${url}`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
233
|
+
if (!buf.length || buf.length > 20 * 1024 * 1024) return null;
|
|
234
|
+
const ct = (res.headers.get("content-type") || "").toLowerCase();
|
|
235
|
+
let ext = ".bin";
|
|
236
|
+
if (ct.includes("png")) ext = ".png";
|
|
237
|
+
else if (ct.includes("jpeg") || ct.includes("jpg")) ext = ".jpg";
|
|
238
|
+
else if (ct.includes("gif")) ext = ".gif";
|
|
239
|
+
else if (ct.includes("webp")) ext = ".webp";
|
|
240
|
+
else {
|
|
241
|
+
const m = (url.split("?")[0] || "").match(/\.(png|jpe?g|gif|webp)$/i);
|
|
242
|
+
if (m) ext = `.${m[1].toLowerCase().replace("jpeg", "jpg")}`;
|
|
243
|
+
else ext = ".jpg";
|
|
244
|
+
}
|
|
245
|
+
const tmp = path.join(os.tmpdir(), `dingtalk-md-remote-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`);
|
|
246
|
+
fs.writeFileSync(tmp, buf);
|
|
247
|
+
console.log(`[DingTalk][MediaIdTrace] 远程图下载成功 | bytes=${buf.length} tmp=${tmp}`);
|
|
248
|
+
return tmp;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
console.warn(`[DingTalk][MediaIdTrace] 远程图下载异常 | ${e?.message || e}`);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
204
254
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* - 匹配所有 ![](),本地判定 isLocalImageRef(含 /mnt、中文、& 文件名)
|
|
208
|
-
* - 上传失败会 /tmp 拷贝重试;仍失败则去掉本地路径(避免钉钉灰图占位)
|
|
209
|
-
* - 关键步骤始终 console.log(不依赖 debug 开关)
|
|
255
|
+
* 把 original 在「非 ![] / 非 ]( 」位置替换为 replacement(同一气泡内保留下载位)。
|
|
210
256
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
function replaceOutsideImageTargets(content, original, replacement) {
|
|
258
|
+
if (!content.includes(original) || original === replacement) return content;
|
|
259
|
+
let out = content;
|
|
260
|
+
let from = 0;
|
|
261
|
+
while (from < out.length) {
|
|
262
|
+
const idx = out.indexOf(original, from);
|
|
263
|
+
if (idx < 0) break;
|
|
264
|
+
if (out.slice(Math.max(0, idx - 2), idx) === "](") {
|
|
265
|
+
from = idx + original.length;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
out = out.slice(0, idx) + replacement + out.slice(idx + original.length);
|
|
269
|
+
from = idx + replacement.length;
|
|
220
270
|
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 出站图片处理(单气泡):
|
|
275
|
+
* - 只处理 ``:本地/远程均上传 → ``
|
|
276
|
+
* - 代码块跳过;不扫裸路径
|
|
277
|
+
* - 远程:![] 改 mediaId 后,正文里「下载链接:http://…」**原样保留在同一条消息**
|
|
278
|
+
* (钉钉改写发生在「![] 与正文共用同一 http」时;![] 改成 mediaId 后即可同泡共存)
|
|
279
|
+
* - 本地:正文重复的本地路径换成 down.dingtalk.com 可点链接(仍同一条消息)
|
|
280
|
+
* - **不再拆第二条 text 消息**
|
|
281
|
+
*/
|
|
282
|
+
async function processImagesForOutbound(content, oapiToken, log) {
|
|
283
|
+
if (!oapiToken || !content || typeof content !== "string") return {
|
|
284
|
+
text: content || "",
|
|
285
|
+
followUpUrls: []
|
|
286
|
+
};
|
|
221
287
|
const codeRanges = getMarkdownCodeRanges(content);
|
|
222
|
-
if (codeRanges.length > 0) logLocalImage(log, "processLocalImages 代码区域", `count=${codeRanges.length}(围栏/行内 code 内的图路径将跳过)`);
|
|
223
|
-
const allMdPreview = [...content.matchAll(LOCAL_IMAGE_RE)].map((m, i) => {
|
|
224
|
-
const p = (m[2] || "").trim();
|
|
225
|
-
const inCode = m.index !== void 0 && isIndexInCodeRange(m.index, codeRanges);
|
|
226
|
-
return `#${i + 1} local=${isLocalImageRef(p)} inCode=${inCode} path=${p.slice(0, 120)}`;
|
|
227
|
-
});
|
|
228
|
-
logLocalImage(log, "processLocalImages 扫描 ![]()", allMdPreview.length ? allMdPreview.join(" || ") : "正文中没有任何 markdown 图片语法");
|
|
229
|
-
let result = content;
|
|
230
288
|
const mdMatches = [...content.matchAll(LOCAL_IMAGE_RE)];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const [fullMatch, alt, rawPath] = match;
|
|
243
|
-
const absPath = toLocalPath(rawPath.replace(/\\ /g, " ").trim());
|
|
244
|
-
logLocalImage(log, "processLocalImages 处理一张", `alt=${alt || "-"} raw=${rawPath} abs=${absPath} exists=${fs.existsSync(absPath)}`);
|
|
289
|
+
let result = content;
|
|
290
|
+
/** 本地路径 → 可点 downloadUrl(仅替换正文中的裸路径,不拆消息) */
|
|
291
|
+
const localPathToDl = /* @__PURE__ */ new Map();
|
|
292
|
+
let uploadCount = 0;
|
|
293
|
+
for (const match of mdMatches) {
|
|
294
|
+
if (match.index !== void 0 && isIndexInCodeRange(match.index, codeRanges)) continue;
|
|
295
|
+
const [fullMatch, alt, rawPath] = match;
|
|
296
|
+
const cleanPath = (rawPath || "").replace(/\\ /g, " ").trim();
|
|
297
|
+
if (!cleanPath) continue;
|
|
298
|
+
if (isLocalImageRef(cleanPath)) {
|
|
299
|
+
const absPath = toLocalPath(cleanPath);
|
|
245
300
|
try {
|
|
246
301
|
const uploadResult = await uploadLocalImageWithFallback(absPath, oapiToken, log);
|
|
247
302
|
if (uploadResult?.mediaId) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const note = `📷 *图片上传异常(${path.basename(absPath)})*`;
|
|
258
|
-
result = result.split(fullMatch).join(note);
|
|
259
|
-
logLocalImage(log, "processLocalImages 异常", `${absPath} err=${err?.message || err}`);
|
|
303
|
+
result = result.split(fullMatch).join(``);
|
|
304
|
+
uploadCount++;
|
|
305
|
+
if (uploadResult.downloadUrl) {
|
|
306
|
+
localPathToDl.set(cleanPath, uploadResult.downloadUrl);
|
|
307
|
+
localPathToDl.set(absPath, uploadResult.downloadUrl);
|
|
308
|
+
}
|
|
309
|
+
} else result = result.split(fullMatch).join(`📷 *图片上传失败(${path.basename(absPath)})*`);
|
|
310
|
+
} catch {
|
|
311
|
+
result = result.split(fullMatch).join(`📷 *图片上传异常(${path.basename(toLocalPath(cleanPath))})*`);
|
|
260
312
|
}
|
|
313
|
+
continue;
|
|
261
314
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const bareOutsideCode = [...result.matchAll(BARE_IMAGE_PATH_RE)].filter((m) => {
|
|
265
|
-
if (m.index === void 0) return false;
|
|
266
|
-
const idx = m.index;
|
|
267
|
-
return !result.slice(Math.max(0, idx - 10), idx).includes("](");
|
|
268
|
-
}).filter((m) => {
|
|
269
|
-
if (m.index === void 0) return true;
|
|
270
|
-
if (isIndexInCodeRange(m.index, codeRanges)) {
|
|
271
|
-
logLocalImage(log, "processLocalImages 跳过代码块内纯路径", (m[1] || "").slice(0, 80));
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
return true;
|
|
275
|
-
});
|
|
276
|
-
if (bareOutsideCode.length > 0) {
|
|
277
|
-
logLocalImage(log, "processLocalImages 纯文本路径", `count=${bareOutsideCode.length}`);
|
|
278
|
-
for (const match of bareOutsideCode.reverse()) {
|
|
279
|
-
const [fullMatch, rawPath] = match;
|
|
280
|
-
const absPath = toLocalPath(rawPath.replace(/\\ /g, " ").trim());
|
|
281
|
-
logLocalImage(log, "processLocalImages 纯文本上传", absPath);
|
|
315
|
+
if (looksLikeRemoteUrl(cleanPath)) {
|
|
316
|
+
let tmp = null;
|
|
282
317
|
try {
|
|
283
|
-
|
|
318
|
+
tmp = await downloadRemoteUrlToTemp(cleanPath);
|
|
319
|
+
if (!tmp) continue;
|
|
320
|
+
const uploadResult = await uploadLocalImageWithFallback(tmp, oapiToken, log);
|
|
284
321
|
if (uploadResult?.mediaId) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
322
|
+
result = result.split(fullMatch).join(``);
|
|
323
|
+
uploadCount++;
|
|
324
|
+
console.log(`[DingTalk][LocalImage] ![]远程→mediaId 下载链保留原URL | ${uploadResult.mediaId}`);
|
|
288
325
|
}
|
|
289
326
|
} catch (err) {
|
|
290
|
-
logLocalImage(log, "
|
|
327
|
+
logLocalImage(log, "远程图处理失败", err?.message || String(err));
|
|
328
|
+
} finally {
|
|
329
|
+
if (tmp) try {
|
|
330
|
+
fs.unlinkSync(tmp);
|
|
331
|
+
} catch {}
|
|
291
332
|
}
|
|
292
333
|
}
|
|
293
334
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
335
|
+
for (const [p, dl] of localPathToDl) result = replaceOutsideImageTargets(result, p, dl);
|
|
336
|
+
console.log(`[DingTalk][LocalImage] processImagesForOutbound 完成 | uploads=${uploadCount} outLen=${result.length}`);
|
|
337
|
+
logMediaIdTrace("processImagesForOutbound:out", result);
|
|
338
|
+
return {
|
|
339
|
+
text: result,
|
|
340
|
+
followUpUrls: []
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** 兼容旧调用 */
|
|
344
|
+
async function processLocalImages(content, oapiToken, log) {
|
|
345
|
+
return (await processImagesForOutbound(content, oapiToken, log)).text;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* 是否仍有「需要上传却未上传」的本地 MD 图。
|
|
349
|
+
* **代码块 / 行内 code 内的示例路径不算**(否则 AI 举例 `` 会误报灰图)。
|
|
350
|
+
*/
|
|
351
|
+
function hasResidualLocalMdImagesOutsideCode(content) {
|
|
352
|
+
if (!content) return false;
|
|
353
|
+
const codeRanges = getMarkdownCodeRanges(content);
|
|
354
|
+
for (const m of content.matchAll(LOCAL_IMAGE_RE)) {
|
|
355
|
+
if (m.index !== void 0 && isIndexInCodeRange(m.index, codeRanges)) continue;
|
|
356
|
+
if (isLocalImageRef((m[2] || "").trim())) return true;
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
297
359
|
}
|
|
298
360
|
/**
|
|
299
361
|
* 提取视频元数据(时长、分辨率)
|
|
@@ -712,4 +774,4 @@ async function processRawMediaPaths(content, config, oapiToken, log, target) {
|
|
|
712
774
|
return processedContent;
|
|
713
775
|
}
|
|
714
776
|
//#endregion
|
|
715
|
-
export {
|
|
777
|
+
export { toLocalPath as _, extractVideoThumbnail as a, __exportAll as b, logMediaIdTrace as c, processLocalImages as d, processRawMediaPaths as f, sendVideoProactive as g, sendFileProactive as h, extractVideoMetadata as i, looksLikeRemoteUrl as l, sendAudioProactive as m, LOCAL_IMAGE_RE as n, hasResidualLocalMdImagesOutsideCode as o, processVideoMarkers as p, VIDEO_MARKER_PATTERN as r, isLocalImageRef as s, IMAGE_EXTENSIONS as t, processImagesForOutbound as u, uploadLocalImageWithFallback as v, uploadMediaToDingTalk as y };
|