@jeik/dingtalk-connector 0.8.21-fix45 → 0.8.21-fix47
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 +29 -0
- package/README.en.md +12 -7
- package/README.md +13 -8
- package/dist/entry-bundled.mjs +1 -1
- package/dist/gateway-methods-DHMdV0k8.mjs +2 -0
- package/dist/{gateway-methods-CCh-LutW.mjs → gateway-methods-DNvtOYEG.mjs} +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{media-ClfWY5Cw.mjs → media-BqpUGCni.mjs} +1 -1
- package/dist/{media-_qA1Qjt_.mjs → media-DnQB2BrL.mjs} +133 -30
- package/dist/{message-handler-BKmyuozN.mjs → message-handler-DVXM5olz.mjs} +7 -7
- package/dist/{messaging-CpOPEOI8.mjs → messaging-msXFq5IJ.mjs} +14 -6
- package/dist/{runtime-Dj8jSHjE.mjs → runtime-f78QdJj5.mjs} +3 -3
- package/docs/RELEASE_NOTES_V0.8.21-fix46.md +45 -0
- package/docs/RELEASE_NOTES_V0.8.21-fix47.md +32 -0
- package/package.json +1 -1
- package/src/services/media/common.ts +5 -6
- package/src/services/media.ts +217 -35
- package/src/services/messaging.ts +18 -1
- package/dist/gateway-methods-b4qQpdOB.mjs +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ 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-fix47] - 2026-07-20
|
|
9
|
+
|
|
10
|
+
### 修复 / Fixed
|
|
11
|
+
- 🐛 **`file://` 本地图被误判为远程** — `looksLikeRemoteUrl` 对含 `://` 一律当远程,导致 `` / `file:///root/...` 走 `fetch` → `fetch failed`,原样发出灰图。现:`file://` / `MEDIA:` / `attachment://` **优先按本地上传**(`toLocalPath` 后 oapi)。
|
|
12
|
+
日志特征(fix46):`kind=http path=file://...` + `远程图下载异常 | fetch failed` + `localTried=0`。
|
|
13
|
+
|
|
14
|
+
### 安装 / Install
|
|
15
|
+
```bash
|
|
16
|
+
npx -y @jeik/dingtalk-connector install --force
|
|
17
|
+
openclaw gateway restart
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## [0.8.21-fix46] - 2026-07-20
|
|
21
|
+
|
|
22
|
+
### 诊断 / Diagnostics
|
|
23
|
+
- ✨ **message 本地 MD 图灰图排查日志** — 恢复并加强 always-on `[DingTalk][LocalImage]` / `[MediaIdTrace]`:
|
|
24
|
+
- `sendTextToDingTalk` 入口 / 处理后 / API前
|
|
25
|
+
- `processImagesForOutbound` 扫描每张 `![]`(kind=local|http|mediaId、inCode、exists、size)
|
|
26
|
+
- 本地上传成败 / 远程下载上传 / `residualLocal` 残留告警
|
|
27
|
+
- MediaIdTrace 固定字段:`mediaIdCount` / `mdImgs` / `residualLocal` / preview
|
|
28
|
+
|
|
29
|
+
逻辑仍基于 fix45(图+下载同泡、只认 `![]`、代码块跳过)。
|
|
30
|
+
|
|
31
|
+
### 安装 / Install
|
|
32
|
+
```bash
|
|
33
|
+
npx -y @jeik/dingtalk-connector install --force
|
|
34
|
+
openclaw gateway restart
|
|
35
|
+
```
|
|
36
|
+
|
|
8
37
|
## [0.8.21-fix45] - 2026-07-20
|
|
9
38
|
|
|
10
39
|
生产稳定版 / Production-stable community release。
|
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-fix47</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,21 +23,26 @@
|
|
|
23
23
|
|
|
24
24
|
## 🔧 Recent Updates
|
|
25
25
|
|
|
26
|
-
### 🚀 v0.8.21-
|
|
26
|
+
### 🚀 v0.8.21-fix47 · 2026-07-20 (current)
|
|
27
27
|
|
|
28
|
-
**Theme:
|
|
28
|
+
**Theme: fix `file://` misclassified as remote URL**
|
|
29
29
|
|
|
30
30
|
| | Change |
|
|
31
31
|
|--|--------|
|
|
32
|
-
|
|
|
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 |
|
|
32
|
+
| 🐛 **`file://` grey images** | Treat `file://` / `MEDIA:` / `attachment://` as local upload, not `fetch` |
|
|
36
33
|
|
|
37
34
|
```bash
|
|
38
35
|
npx -y @jeik/dingtalk-connector install --force && openclaw gateway restart
|
|
39
36
|
```
|
|
40
37
|
|
|
38
|
+
### 📦 v0.8.21-fix46 · 2026-07-20
|
|
39
|
+
|
|
40
|
+
Always-on local MD image diagnostics.
|
|
41
|
+
|
|
42
|
+
### 📦 v0.8.21-fix45 · 2026-07-20
|
|
43
|
+
|
|
44
|
+
Image + download link one bubble · only `![]` · quote cache · residual false-positive fix.
|
|
45
|
+
|
|
41
46
|
### 📦 v0.8.21-fix38 · 2026-07-20
|
|
42
47
|
|
|
43
48
|
Remote `media` download-then-upload.
|
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-fix47</strong>(稳定生产可用;一键安装:`npx -y @jeik/dingtalk-connector install`;本地 tgz:`openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix47.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,21 +23,26 @@
|
|
|
23
23
|
|
|
24
24
|
## 🔧 最近更新
|
|
25
25
|
|
|
26
|
-
### 🚀 v0.8.21-
|
|
26
|
+
### 🚀 v0.8.21-fix47 · 2026-07-20(当前)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
**主题:`file://` 本地图误判远程修复**
|
|
29
29
|
|
|
30
30
|
| | 改动 |
|
|
31
31
|
|--|------|
|
|
32
|
-
|
|
|
33
|
-
| ✅ **只处理 `![]()`** | 不扫裸路径;代码块内路径不上传、不误报灰图 |
|
|
34
|
-
| 📎 **引用 AI 卡** | 定稿缓存正文,引用时回填(`CardCache`) |
|
|
35
|
-
| 🌐 **message media** | `media: "https://..."` 下载再上传 |
|
|
32
|
+
| 🐛 **`file://` 灰图** | `` / `file:///root/...` 按本地上传,不再 `fetch failed` |
|
|
36
33
|
|
|
37
34
|
```bash
|
|
38
35
|
npx -y @jeik/dingtalk-connector install --force && openclaw gateway restart
|
|
39
36
|
```
|
|
40
37
|
|
|
38
|
+
### 📦 v0.8.21-fix46 · 2026-07-20
|
|
39
|
+
|
|
40
|
+
本地 MD 图灰图诊断日志(LocalImage / MediaIdTrace)。
|
|
41
|
+
|
|
42
|
+
### 📦 v0.8.21-fix45 · 2026-07-20
|
|
43
|
+
|
|
44
|
+
图 + 下载链接单气泡 · 只认 `![]` · 引用卡片缓存 · residual 误报修复。
|
|
45
|
+
|
|
41
46
|
### 📦 v0.8.21-fix38 · 2026-07-20
|
|
42
47
|
|
|
43
48
|
message 远程 `media` 下载上传。
|
|
@@ -64,7 +69,7 @@ message 远程 `media` 下载上传。
|
|
|
64
69
|
| 2026-06-28 | 上线 npm `@jeik/dingtalk-connector`;过程消息提前定稿修复 |
|
|
65
70
|
| 2026-05 | MD 直链/本地图;多轮刷屏;4.29+ 无文本输出;WebSocket 幻影重连 |
|
|
66
71
|
|
|
67
|
-
完整说明:[CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release
|
|
72
|
+
完整说明:[CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md) · [Release fix47](docs/RELEASE_NOTES_V0.8.21-fix47.md)
|
|
68
73
|
|
|
69
74
|
---
|
|
70
75
|
|
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-DHMdV0k8.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-msXFq5IJ.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-f78QdJj5.mjs";
|
|
2
|
+
import { t as registerGatewayMethods } from "./gateway-methods-DNvtOYEG.mjs";
|
|
3
3
|
//#region index.ts
|
|
4
4
|
/**
|
|
5
5
|
* 检测同一 plugin id 在多个路径被加载的情况。
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-
|
|
1
|
+
import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-DnQB2BrL.mjs";
|
|
2
2
|
export { hasResidualLocalMdImagesOutsideCode, processLocalImages, processRawMediaPaths, processVideoMarkers, sendFileProactive, toLocalPath };
|
|
@@ -30,10 +30,18 @@ const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
|
|
|
30
30
|
* 注意:旧版只匹配 /tmp|/home|/root 等前缀,**漏掉 /mnt 共享盘**,导致 message 工具 MD 灰图。
|
|
31
31
|
*/
|
|
32
32
|
const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
33
|
-
/**
|
|
33
|
+
/** 本地文件 URI / OpenClaw 媒体前缀(不是 http 远程) */
|
|
34
|
+
function isLocalFileScheme(p) {
|
|
35
|
+
return p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 是否像远程 URL(禁止当本地路径上传)。
|
|
39
|
+
* **不含** file:// / MEDIA: / attachment://(那些是本地引用)。
|
|
40
|
+
*/
|
|
34
41
|
function looksLikeRemoteUrl(raw) {
|
|
35
42
|
const p = (raw || "").trim();
|
|
36
43
|
if (!p) return false;
|
|
44
|
+
if (isLocalFileScheme(p)) return false;
|
|
37
45
|
if (/^https?:\/\//i.test(p)) return true;
|
|
38
46
|
if (/^[a-z]:\/\//i.test(p) && !/^[a-z]:[\\/][^/\\]/i.test(p)) return true;
|
|
39
47
|
if (p.includes("://")) return true;
|
|
@@ -43,9 +51,9 @@ function looksLikeRemoteUrl(raw) {
|
|
|
43
51
|
function isLocalImageRef(rawPath) {
|
|
44
52
|
const p = (rawPath || "").trim();
|
|
45
53
|
if (!p) return false;
|
|
54
|
+
if (isLocalFileScheme(p)) return true;
|
|
46
55
|
if (looksLikeRemoteUrl(p) || /^https?:\/\//i.test(p)) return false;
|
|
47
56
|
if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
|
|
48
|
-
if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) return true;
|
|
49
57
|
if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
|
|
50
58
|
if (IMAGE_EXTENSIONS.test(p.split("?")[0] || p)) return true;
|
|
51
59
|
return !p.includes("://");
|
|
@@ -68,8 +76,9 @@ function toLocalPath(raw) {
|
|
|
68
76
|
return filePath;
|
|
69
77
|
}
|
|
70
78
|
/**
|
|
71
|
-
*
|
|
72
|
-
* verbose
|
|
79
|
+
* 本地图诊断:默认 console.log 一行(不走 log.warn,避免 [sendTextToDingTalk] 双前缀)。
|
|
80
|
+
* verbose=true 的细节(realpath 等)仅在 debug 时输出。
|
|
81
|
+
* 决策路径(匹配/exists/上传成败/残留)务必 always-on,方便查灰图。
|
|
73
82
|
*/
|
|
74
83
|
function logLocalImage(log, step, detail, verbose = false) {
|
|
75
84
|
const line = detail ? `[DingTalk][LocalImage] ${step} | ${detail}` : `[DingTalk][LocalImage] ${step}`;
|
|
@@ -81,21 +90,36 @@ function logLocalImage(log, step, detail, verbose = false) {
|
|
|
81
90
|
}
|
|
82
91
|
console.log(line);
|
|
83
92
|
}
|
|
84
|
-
/** 钉钉 mediaId
|
|
93
|
+
/** 钉钉 mediaId 形态(oapi 返回常带 @) */
|
|
85
94
|
const MEDIA_ID_RE = /@[A-Za-z0-9_-]{8,}/g;
|
|
95
|
+
function classifyMdImgTarget(target) {
|
|
96
|
+
const t = (target || "").trim();
|
|
97
|
+
if (!t) return "other";
|
|
98
|
+
if (t.startsWith("@") && !t.includes("/") && !t.includes("\\")) return "mediaId";
|
|
99
|
+
if (looksLikeRemoteUrl(t) || /^https?:\/\//i.test(t)) return "http";
|
|
100
|
+
if (isLocalImageRef(t)) return "local";
|
|
101
|
+
return "other";
|
|
102
|
+
}
|
|
86
103
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
104
|
+
* MediaId 追踪:关键节点 / force / 裸 mediaId 时输出。
|
|
105
|
+
* 固定字段:mediaIdCount / ids / BARE_ids / mdImgs / httpCount / residualLocal / preview
|
|
106
|
+
* (灰图排查:看 mdImgs 是 path:本地 还是 mediaId:@xxx,以及 residualLocal)
|
|
89
107
|
*/
|
|
90
108
|
function logMediaIdTrace(stage, content, extra, force = false) {
|
|
91
109
|
const text = typeof content === "string" ? content : "";
|
|
92
110
|
const ids = [...new Set([...text.matchAll(MEDIA_ID_RE)].map((m) => m[0]))];
|
|
93
|
-
const
|
|
111
|
+
const mdTargets = [...text.matchAll(LOCAL_IMAGE_RE)].map((m) => (m[2] || "").trim());
|
|
112
|
+
const inMd = new Set(mdTargets);
|
|
94
113
|
const bareIds = ids.filter((id) => !inMd.has(id));
|
|
114
|
+
const mdImgs = mdTargets.slice(0, 8).map((t) => {
|
|
115
|
+
return `${classifyMdImgTarget(t)}:${t.length > 100 ? `${t.slice(0, 100)}…` : t}`;
|
|
116
|
+
});
|
|
117
|
+
const residualLocal = mdTargets.filter((t) => isLocalImageRef(t));
|
|
118
|
+
const httpCount = (text.match(/https?:\/\//gi) || []).length;
|
|
95
119
|
const suspiciousBare = /type=image|sampleImage|photoURL/i.test(`${stage} ${extra || ""}`) ? [] : bareIds;
|
|
96
|
-
if (!force
|
|
97
|
-
const preview = text.replace(/\n/g, "\\n").slice(0,
|
|
98
|
-
console.log(`[DingTalk][MediaIdTrace] ${stage} | len=${text.length}` + (ids.length ? `
|
|
120
|
+
if (!(force || suspiciousBare.length > 0 || residualLocal.length > 0 || /API前|完成|:out|:in|after-process|processImages/i.test(stage))) return;
|
|
121
|
+
const preview = text.replace(/\n/g, "\\n").slice(0, 200);
|
|
122
|
+
console.log(`[DingTalk][MediaIdTrace] ${stage} | len=${text.length} mediaIdCount=${ids.length}` + (ids.length ? ` ids=[${ids.slice(0, 4).join(",")}]` : "") + ` BARE_ids=${suspiciousBare.length ? suspiciousBare.join(",") : "无"} mdImgs=[${mdImgs.length ? mdImgs.join(" || ") : "-"}] httpCount=${httpCount}` + (residualLocal.length ? ` residualLocal=${residualLocal.length}:${residualLocal.slice(0, 3).map((p) => p.slice(0, 80)).join("|")}` : " residualLocal=0") + (extra ? ` ${extra}` : "") + ` preview="${preview}"`);
|
|
99
123
|
}
|
|
100
124
|
/** Markdown 代码区域(围栏 ``` 与行内 `...`),其中的路径不应上传 */
|
|
101
125
|
function getMarkdownCodeRanges(content) {
|
|
@@ -124,21 +148,21 @@ async function uploadMediaToDingTalk(filePath, mediaType, oapiToken, maxSize = 2
|
|
|
124
148
|
const tag = `uploadMedia type=${mediaType}`;
|
|
125
149
|
try {
|
|
126
150
|
let absPath = toLocalPath(filePath);
|
|
127
|
-
logLocalImage(log, `${tag} 开始`, `path=${absPath}
|
|
151
|
+
logLocalImage(log, `${tag} 开始`, `path=${absPath}`);
|
|
128
152
|
if (!fs.existsSync(absPath)) {
|
|
129
153
|
logLocalImage(log, `${tag} 失败:文件不存在`, absPath);
|
|
130
154
|
return null;
|
|
131
155
|
}
|
|
132
156
|
try {
|
|
133
157
|
const real = fs.realpathSync(absPath);
|
|
134
|
-
if (real !== absPath) logLocalImage(log, `${tag} realpath`, `${absPath} → ${real}
|
|
158
|
+
if (real !== absPath) logLocalImage(log, `${tag} realpath`, `${absPath} → ${real}`);
|
|
135
159
|
absPath = real;
|
|
136
160
|
} catch (e) {
|
|
137
161
|
logLocalImage(log, `${tag} realpath跳过`, e?.message || String(e), true);
|
|
138
162
|
}
|
|
139
163
|
const stats = fs.statSync(absPath);
|
|
140
164
|
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
141
|
-
logLocalImage(log, `${tag} 文件可读`, `size=${fileSizeMB}MB
|
|
165
|
+
logLocalImage(log, `${tag} 文件可读`, `size=${fileSizeMB}MB mode=${stats.mode}`);
|
|
142
166
|
const effectiveMax = mediaType === "image" ? Math.min(maxSize, 10 * 1024 * 1024) : mediaType === "voice" ? Math.min(maxSize, 2 * 1024 * 1024) : maxSize;
|
|
143
167
|
if (stats.size > effectiveMax) {
|
|
144
168
|
const maxSizeMB = (effectiveMax / (1024 * 1024)).toFixed(0);
|
|
@@ -197,17 +221,26 @@ async function uploadMediaToDingTalk(filePath, mediaType, oapiToken, maxSize = 2
|
|
|
197
221
|
*/
|
|
198
222
|
async function uploadLocalImageWithFallback(filePath, oapiToken, log) {
|
|
199
223
|
const absPath = toLocalPath(filePath);
|
|
200
|
-
|
|
224
|
+
const exists = fs.existsSync(absPath);
|
|
225
|
+
logLocalImage(log, "fallback 入口", `path=${absPath} exists=${exists}`);
|
|
226
|
+
if (!exists) {
|
|
201
227
|
logLocalImage(log, "fallback 失败:文件不存在", absPath);
|
|
202
228
|
return null;
|
|
203
229
|
}
|
|
230
|
+
logLocalImage(log, "fallback 第1次:直接上传", absPath);
|
|
204
231
|
let result = await uploadMediaToDingTalk(absPath, "image", oapiToken, 10 * 1024 * 1024, log);
|
|
205
|
-
if (result?.mediaId)
|
|
232
|
+
if (result?.mediaId) {
|
|
233
|
+
logLocalImage(log, "fallback 第1次成功", result.mediaId);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
206
236
|
const ext = path.extname(absPath) || ".jpg";
|
|
207
237
|
const tmp = path.join(os.tmpdir(), `dingtalk-img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext.toLowerCase()}`);
|
|
208
238
|
try {
|
|
209
239
|
fs.copyFileSync(absPath, tmp);
|
|
240
|
+
logLocalImage(log, "fallback 第2次:tmp重试", `${absPath} → ${tmp}`);
|
|
210
241
|
result = await uploadMediaToDingTalk(tmp, "image", oapiToken, 10 * 1024 * 1024, log);
|
|
242
|
+
if (result?.mediaId) logLocalImage(log, "fallback 第2次成功", result.mediaId);
|
|
243
|
+
else logLocalImage(log, "fallback 第2次仍失败", tmp);
|
|
211
244
|
return result;
|
|
212
245
|
} catch (err) {
|
|
213
246
|
logLocalImage(log, "fallback 拷贝/重试异常", `${absPath} err=${err?.message || err}`);
|
|
@@ -278,51 +311,109 @@ function replaceOutsideImageTargets(content, original, replacement) {
|
|
|
278
311
|
* (钉钉改写发生在「![] 与正文共用同一 http」时;![] 改成 mediaId 后即可同泡共存)
|
|
279
312
|
* - 本地:正文重复的本地路径换成 down.dingtalk.com 可点链接(仍同一条消息)
|
|
280
313
|
* - **不再拆第二条 text 消息**
|
|
314
|
+
*
|
|
315
|
+
* 日志约定(always-on,查灰图必看):
|
|
316
|
+
* - 开始 / 扫描每张 ![] / 本地 exists / 上传成败 / 远程下载 / 残留 / out
|
|
281
317
|
*/
|
|
282
318
|
async function processImagesForOutbound(content, oapiToken, log) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
319
|
+
logLocalImage(log, "processImagesForOutbound 开始", `contentLen=${content?.length ?? 0} hasToken=${!!oapiToken}`);
|
|
320
|
+
if (!oapiToken) {
|
|
321
|
+
logLocalImage(log, "processImagesForOutbound 跳过", "无 oapiToken(本地路径无法上传→必灰图)");
|
|
322
|
+
logMediaIdTrace("processImagesForOutbound:skip-no-token", content, void 0, true);
|
|
323
|
+
return {
|
|
324
|
+
text: content || "",
|
|
325
|
+
followUpUrls: []
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (!content || typeof content !== "string") {
|
|
329
|
+
logLocalImage(log, "processImagesForOutbound 跳过", "content 为空");
|
|
330
|
+
return {
|
|
331
|
+
text: content || "",
|
|
332
|
+
followUpUrls: []
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
logMediaIdTrace("processImagesForOutbound:in", content, void 0, true);
|
|
287
336
|
const codeRanges = getMarkdownCodeRanges(content);
|
|
337
|
+
if (codeRanges.length > 0) logLocalImage(log, "processImagesForOutbound 代码区域", `count=${codeRanges.length}(围栏/行内 code 内的图路径将跳过)`);
|
|
288
338
|
const mdMatches = [...content.matchAll(LOCAL_IMAGE_RE)];
|
|
339
|
+
const scanPreview = mdMatches.map((m, i) => {
|
|
340
|
+
const p = (m[2] || "").trim();
|
|
341
|
+
const inCode = m.index !== void 0 && isIndexInCodeRange(m.index, codeRanges);
|
|
342
|
+
const kind = classifyMdImgTarget(p);
|
|
343
|
+
return `#${i + 1} kind=${kind} inCode=${inCode} path=${p.slice(0, 120)}`;
|
|
344
|
+
});
|
|
345
|
+
logLocalImage(log, "processImagesForOutbound 扫描 ![]()", scanPreview.length ? scanPreview.join(" || ") : "正文中没有任何 markdown 图片语法");
|
|
289
346
|
let result = content;
|
|
290
347
|
/** 本地路径 → 可点 downloadUrl(仅替换正文中的裸路径,不拆消息) */
|
|
291
348
|
const localPathToDl = /* @__PURE__ */ new Map();
|
|
292
349
|
let uploadCount = 0;
|
|
350
|
+
let localTried = 0;
|
|
351
|
+
let remoteTried = 0;
|
|
352
|
+
let skippedCode = 0;
|
|
353
|
+
let skippedOther = 0;
|
|
293
354
|
for (const match of mdMatches) {
|
|
294
|
-
if (match.index !== void 0 && isIndexInCodeRange(match.index, codeRanges))
|
|
355
|
+
if (match.index !== void 0 && isIndexInCodeRange(match.index, codeRanges)) {
|
|
356
|
+
skippedCode++;
|
|
357
|
+
logLocalImage(log, "processImagesForOutbound 跳过代码块内", (match[2] || "").trim().slice(0, 120));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
295
360
|
const [fullMatch, alt, rawPath] = match;
|
|
296
361
|
const cleanPath = (rawPath || "").replace(/\\ /g, " ").trim();
|
|
297
362
|
if (!cleanPath) continue;
|
|
298
363
|
if (isLocalImageRef(cleanPath)) {
|
|
364
|
+
localTried++;
|
|
299
365
|
const absPath = toLocalPath(cleanPath);
|
|
366
|
+
let exists = false;
|
|
367
|
+
let sizeMB = "-";
|
|
368
|
+
try {
|
|
369
|
+
exists = fs.existsSync(absPath);
|
|
370
|
+
if (exists) sizeMB = (fs.statSync(absPath).size / (1024 * 1024)).toFixed(2);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
logLocalImage(log, "processImagesForOutbound 本地 stat 异常", e?.message || String(e));
|
|
373
|
+
}
|
|
374
|
+
logLocalImage(log, "processImagesForOutbound 处理本地图", `alt=${alt || "-"} raw=${cleanPath.slice(0, 120)} abs=${absPath} exists=${exists} sizeMB=${sizeMB}`);
|
|
375
|
+
if (!exists) {
|
|
376
|
+
result = result.split(fullMatch).join(`📷 *图片文件不存在(${path.basename(absPath)})*`);
|
|
377
|
+
logLocalImage(log, "processImagesForOutbound 本地文件不存在→失败提示", absPath);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
300
380
|
try {
|
|
301
381
|
const uploadResult = await uploadLocalImageWithFallback(absPath, oapiToken, log);
|
|
302
382
|
if (uploadResult?.mediaId) {
|
|
303
|
-
|
|
383
|
+
const replacement = ``;
|
|
384
|
+
result = result.split(fullMatch).join(replacement);
|
|
304
385
|
uploadCount++;
|
|
305
386
|
if (uploadResult.downloadUrl) {
|
|
306
387
|
localPathToDl.set(cleanPath, uploadResult.downloadUrl);
|
|
307
388
|
localPathToDl.set(absPath, uploadResult.downloadUrl);
|
|
308
389
|
}
|
|
309
|
-
|
|
310
|
-
|
|
390
|
+
logLocalImage(log, "processImagesForOutbound 本地→mediaId 成功", `${absPath} → ${uploadResult.mediaId}`);
|
|
391
|
+
} else {
|
|
392
|
+
result = result.split(fullMatch).join(`📷 *图片上传失败(${path.basename(absPath)})*`);
|
|
393
|
+
logLocalImage(log, "processImagesForOutbound 本地上传失败→失败提示", absPath);
|
|
394
|
+
}
|
|
395
|
+
} catch (err) {
|
|
311
396
|
result = result.split(fullMatch).join(`📷 *图片上传异常(${path.basename(toLocalPath(cleanPath))})*`);
|
|
397
|
+
logLocalImage(log, "processImagesForOutbound 本地上传异常", `${absPath} err=${err?.message || err}`);
|
|
312
398
|
}
|
|
313
399
|
continue;
|
|
314
400
|
}
|
|
315
401
|
if (looksLikeRemoteUrl(cleanPath)) {
|
|
402
|
+
remoteTried++;
|
|
403
|
+
logLocalImage(log, "processImagesForOutbound 远程图", cleanPath.slice(0, 160));
|
|
316
404
|
let tmp = null;
|
|
317
405
|
try {
|
|
318
406
|
tmp = await downloadRemoteUrlToTemp(cleanPath);
|
|
319
|
-
if (!tmp)
|
|
407
|
+
if (!tmp) {
|
|
408
|
+
logLocalImage(log, "processImagesForOutbound 远程下载失败(保留原![]URL)", cleanPath.slice(0, 120));
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
320
411
|
const uploadResult = await uploadLocalImageWithFallback(tmp, oapiToken, log);
|
|
321
412
|
if (uploadResult?.mediaId) {
|
|
322
413
|
result = result.split(fullMatch).join(``);
|
|
323
414
|
uploadCount++;
|
|
324
|
-
|
|
325
|
-
}
|
|
415
|
+
logLocalImage(log, "processImagesForOutbound 远程→mediaId 下载链保留原URL", `${cleanPath.slice(0, 80)} → ${uploadResult.mediaId}`);
|
|
416
|
+
} else logLocalImage(log, "processImagesForOutbound 远程上传失败(保留原![]URL)", cleanPath.slice(0, 120));
|
|
326
417
|
} catch (err) {
|
|
327
418
|
logLocalImage(log, "远程图处理失败", err?.message || String(err));
|
|
328
419
|
} finally {
|
|
@@ -330,11 +421,23 @@ async function processImagesForOutbound(content, oapiToken, log) {
|
|
|
330
421
|
fs.unlinkSync(tmp);
|
|
331
422
|
} catch {}
|
|
332
423
|
}
|
|
424
|
+
continue;
|
|
333
425
|
}
|
|
426
|
+
skippedOther++;
|
|
427
|
+
logLocalImage(log, "processImagesForOutbound 跳过非本地非远程", `kind=${classifyMdImgTarget(cleanPath)} path=${cleanPath.slice(0, 120)}`);
|
|
428
|
+
}
|
|
429
|
+
for (const [p, dl] of localPathToDl) {
|
|
430
|
+
const before = result;
|
|
431
|
+
result = replaceOutsideImageTargets(result, p, dl);
|
|
432
|
+
if (before !== result) logLocalImage(log, "processImagesForOutbound 正文裸本地路径→downloadUrl", `${p.slice(0, 80)} → ${dl}`);
|
|
334
433
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
434
|
+
const residual = [...result.matchAll(LOCAL_IMAGE_RE)].filter((m) => {
|
|
435
|
+
if (m.index !== void 0 && isIndexInCodeRange(m.index, getMarkdownCodeRanges(result))) return false;
|
|
436
|
+
return isLocalImageRef((m[2] || "").trim());
|
|
437
|
+
}).map((m) => (m[2] || "").trim());
|
|
438
|
+
logLocalImage(log, "processImagesForOutbound 完成", `uploads=${uploadCount} localTried=${localTried} remoteTried=${remoteTried} skippedCode=${skippedCode} skippedOther=${skippedOther} outLen=${result.length}` + (residual.length ? ` ⚠️residualLocal=${residual.length}:${residual.slice(0, 3).join("|")}` : " residualLocal=0"));
|
|
439
|
+
if (residual.length) logLocalImage(log, "processImagesForOutbound ⚠️仍有代码块外本地路径未替换(钉钉必灰图)", residual.join(" | "));
|
|
440
|
+
logMediaIdTrace("processImagesForOutbound:out", result, void 0, true);
|
|
338
441
|
return {
|
|
339
442
|
text: result,
|
|
340
443
|
followUpUrls: []
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { a as resolveDingtalkAccount } from "./accounts-BAzdqkAV.mjs";
|
|
2
|
-
import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-
|
|
2
|
+
import { r as CHANNEL_ID, t as getDingtalkRuntime } from "./runtime-f78QdJj5.mjs";
|
|
3
3
|
import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
|
|
4
4
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
5
5
|
import { i as getOapiAccessToken } from "./utils-CIfI_3Jh.mjs";
|
|
6
|
-
import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-
|
|
6
|
+
import { S as finalClean, _ as FINAL_TAG, a as sendTextMessage, b as estimateTokens, d as finishAICard, g as unregisterActiveCard, h as streamAICard, i as sendProactive, m as registerActiveCard, p as isQpsLimitError, r as sendMessage, t as sendMarkdownMessage, u as createAICardForTarget, v as PROCESS_TAG, x as extractFinal, y as displayClean } from "./messaging-msXFq5IJ.mjs";
|
|
7
7
|
import { a as QUEUE_BUSY_ACK_PHRASES, n as normalizeSlashCommand, t as buildSessionContext } from "./session-DJ4jYqPv.mjs";
|
|
8
8
|
import { d as recallEmotionReply, o as getAccessToken, r as addEmotionReply, s as getOapiAccessToken$1, t as DINGTALK_API } from "./utils-legacy-CALCPP1t.mjs";
|
|
9
9
|
import "./chunk-upload-BBQgGtcZ.mjs";
|
|
@@ -62,7 +62,7 @@ function resolveAgentWorkspaceDir(cfg, agentId) {
|
|
|
62
62
|
* 直接复用 media.ts 权威实现,避免 reply 路径与 message 路径两套逻辑分叉。
|
|
63
63
|
*/
|
|
64
64
|
async function processLocalImages(content, oapiToken, log) {
|
|
65
|
-
const { processLocalImages: impl } = await import("./media-
|
|
65
|
+
const { processLocalImages: impl } = await import("./media-BqpUGCni.mjs");
|
|
66
66
|
return impl(content, oapiToken, log);
|
|
67
67
|
}
|
|
68
68
|
//#endregion
|
|
@@ -581,14 +581,14 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
581
581
|
finalText = await processLocalImages(finalText, oapiToken, log);
|
|
582
582
|
if (beforeImg !== finalText) console.log(`[DingTalk][LocalImage] closeStreaming 图片处理 | ${beforeImg.length}→${finalText.length}`);
|
|
583
583
|
try {
|
|
584
|
-
const { hasResidualLocalMdImagesOutsideCode } = await import("./media-
|
|
584
|
+
const { hasResidualLocalMdImagesOutsideCode } = await import("./media-BqpUGCni.mjs");
|
|
585
585
|
if (hasResidualLocalMdImagesOutsideCode(finalText)) console.warn(`[DingTalk][LocalImage] closeStreaming 定稿后仍有代码块外的本地 MD 图未上传,钉钉可能灰图`);
|
|
586
586
|
} catch {}
|
|
587
587
|
finalText = await processVideoMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
588
588
|
finalText = await processAudioMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
589
589
|
finalText = await uploadAndReplaceFileMarkers(finalText, "", account.config, oapiToken, log, true, target);
|
|
590
590
|
log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
|
|
591
|
-
const { processRawMediaPaths } = await import("./media-
|
|
591
|
+
const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
|
|
592
592
|
finalText = await processRawMediaPaths(finalText, account.config, oapiToken, log, target);
|
|
593
593
|
log.info(`[DingTalk][closeStreaming] processRawMediaPaths 处理完成`);
|
|
594
594
|
} else log.warn(`[DingTalk][closeStreaming] oapiToken 为空,跳过媒体处理`);
|
|
@@ -744,7 +744,7 @@ function createDingtalkReplyDispatcher(params) {
|
|
|
744
744
|
const oapiToken = await getOapiAccessToken(account.config);
|
|
745
745
|
if (oapiToken) {
|
|
746
746
|
log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
|
|
747
|
-
const { processRawMediaPaths } = await import("./media-
|
|
747
|
+
const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
|
|
748
748
|
text = await processRawMediaPaths(text, account.config, oapiToken, log, target);
|
|
749
749
|
log.info(`[DingTalk][deliver] 裸露文件路径处理完成`);
|
|
750
750
|
}
|
|
@@ -2184,7 +2184,7 @@ async function handleDingTalkMessageInternal(params) {
|
|
|
2184
2184
|
finalText = await processVideoMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
2185
2185
|
finalText = await processAudioMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
2186
2186
|
finalText = await uploadAndReplaceFileMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
|
|
2187
|
-
const { processRawMediaPaths } = await import("./media-
|
|
2187
|
+
const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
|
|
2188
2188
|
finalText = await processRawMediaPaths(finalText, config, oapiToken, log, mediaTarget);
|
|
2189
2189
|
}
|
|
2190
2190
|
const textToSend = finalText.trim() || "✅ 任务执行完成(无文本输出)";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-
|
|
1
|
+
import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-DnQB2BrL.mjs";
|
|
2
2
|
import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
|
|
3
3
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
4
4
|
import { i as getOapiAccessToken, r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
|
|
@@ -848,7 +848,7 @@ async function resolveMediaSourceToLocalFile(mediaUrl, mediaLocalRoots, log) {
|
|
|
848
848
|
cleanup: tmp
|
|
849
849
|
};
|
|
850
850
|
}
|
|
851
|
-
const { toLocalPath } = await import("./media-
|
|
851
|
+
const { toLocalPath } = await import("./media-BqpUGCni.mjs");
|
|
852
852
|
const _fs = await import("fs");
|
|
853
853
|
const _path = await import("path");
|
|
854
854
|
let resolved = toLocalPath(mediaUrl);
|
|
@@ -883,14 +883,22 @@ async function sendTextToDingTalk(params) {
|
|
|
883
883
|
};
|
|
884
884
|
}
|
|
885
885
|
const targetParam = resolveOutboundTarget(target);
|
|
886
|
+
console.log(`[DingTalk][LocalImage] sendTextToDingTalk 入口 | target=${target} textLen=${text?.length ?? 0}`);
|
|
887
|
+
logMediaIdTrace("sendTextToDingTalk:in", text, `target=${target}`, true);
|
|
888
|
+
const beforeLen = text?.length ?? 0;
|
|
886
889
|
try {
|
|
887
890
|
const oapiToken = await getOapiAccessToken(config);
|
|
888
|
-
|
|
891
|
+
console.log(`[DingTalk][LocalImage] sendTextToDingTalk 入口 | target=${target} textLen=${beforeLen} hasToken=${!!oapiToken}`);
|
|
892
|
+
if (oapiToken && text) {
|
|
893
|
+
text = (await processImagesForOutbound(text, oapiToken, log)).text;
|
|
894
|
+
console.log(`[DingTalk][LocalImage] sendTextToDingTalk 处理后 | ${beforeLen}→${text.length} 字`);
|
|
895
|
+
logMediaIdTrace("sendTextToDingTalk:after-process", text, void 0, true);
|
|
896
|
+
} else if (!oapiToken) console.warn(`[DingTalk][LocalImage] sendTextToDingTalk 无 oapiToken,跳过图片上传(本地 MD 图会灰)`);
|
|
889
897
|
} catch (err) {
|
|
890
898
|
console.warn(`[DingTalk][LocalImage] sendTextToDingTalk 图片处理失败: ${err?.message || err}`);
|
|
891
899
|
}
|
|
892
900
|
const msgType = shouldSendAsMarkdown(text) ? "markdown" : "text";
|
|
893
|
-
logMediaIdTrace("sendTextToDingTalk:API前", text, `msgType=${msgType}
|
|
901
|
+
logMediaIdTrace("sendTextToDingTalk:API前", text, `msgType=${msgType}`, true);
|
|
894
902
|
return sendProactive(config, targetParam, text, {
|
|
895
903
|
msgType,
|
|
896
904
|
replyToId,
|
|
@@ -1113,7 +1121,7 @@ async function sendMediaToDingTalk(params) {
|
|
|
1113
1121
|
const fileName = mediaUrl.split("/").filter(Boolean).pop()?.split("?")[0] || "file";
|
|
1114
1122
|
if (mediaType === "video") {
|
|
1115
1123
|
const videoMarker = `[DINGTALK_VIDEO]{"path":"${resolvedMediaUrl}"}[/DINGTALK_VIDEO]`;
|
|
1116
|
-
const { processVideoMarkers } = await import("./media-
|
|
1124
|
+
const { processVideoMarkers } = await import("./media-BqpUGCni.mjs");
|
|
1117
1125
|
await processVideoMarkers(videoMarker, "", config, oapiToken, console, true, targetParam);
|
|
1118
1126
|
return {
|
|
1119
1127
|
ok: true,
|
|
@@ -1132,7 +1140,7 @@ async function sendMediaToDingTalk(params) {
|
|
|
1132
1140
|
fileName,
|
|
1133
1141
|
fileType: ext && ext.length < 12 ? ext : "file"
|
|
1134
1142
|
};
|
|
1135
|
-
const { sendFileProactive } = await import("./media-
|
|
1143
|
+
const { sendFileProactive } = await import("./media-BqpUGCni.mjs");
|
|
1136
1144
|
await sendFileProactive(config, targetParam, fileInfo, uploadResult.mediaId, log);
|
|
1137
1145
|
return {
|
|
1138
1146
|
ok: true,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { b as __exportAll } from "./media-
|
|
1
|
+
import { b as __exportAll } from "./media-DnQB2BrL.mjs";
|
|
2
2
|
import { a as resolveDingtalkAccount, c as addWildcardAllowFrom, d as hasConfiguredSecretInput, f as normalizeAccountId, l as createDefaultChannelRuntimeState, m as resolveDefaultGroupPolicy, o as resolveDingtalkCredentials, p as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultDingtalkAccountId, s as DEFAULT_ACCOUNT_ID, t as listDingtalkAccountIds, u as formatDocsLink } from "./accounts-BAzdqkAV.mjs";
|
|
3
3
|
import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
|
|
4
4
|
import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
|
|
5
5
|
import "./utils-CIfI_3Jh.mjs";
|
|
6
|
-
import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-
|
|
6
|
+
import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-msXFq5IJ.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { z, z as z$1 } from "zod";
|
|
9
9
|
//#region src/secret-input.ts
|
|
@@ -930,7 +930,7 @@ async function monitorDingtalkProvider(opts = {}) {
|
|
|
930
930
|
const log = createLogger(cfg.channels?.["dingtalk-connector"]?.debug ?? false);
|
|
931
931
|
const [accountsModule, monitorAccountModule, monitorSingleModule] = await Promise.all([
|
|
932
932
|
import("./accounts-BQptOmgB.mjs"),
|
|
933
|
-
import("./message-handler-
|
|
933
|
+
import("./message-handler-DVXM5olz.mjs"),
|
|
934
934
|
import("./connection-DHHFFNQJ.mjs")
|
|
935
935
|
]);
|
|
936
936
|
const { resolveDingtalkAccount, listEnabledDingtalkAccounts } = accountsModule;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Release Notes — v0.8.21-fix46(2026-07-20)
|
|
2
|
+
|
|
3
|
+
生产稳定版 / Production-stable community build of `@jeik/dingtalk-connector`。
|
|
4
|
+
|
|
5
|
+
在 **fix45** 行为不变的基础上,补强 **message 本地 MD 图** 诊断日志,方便排查灰图。
|
|
6
|
+
|
|
7
|
+
## 安装 / Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @jeik/dingtalk-connector install --force
|
|
11
|
+
openclaw gateway restart
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
本地 tgz:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix46.tgz --force
|
|
18
|
+
openclaw gateway restart
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 本版要点 / Highlights
|
|
22
|
+
|
|
23
|
+
| 领域 | 说明 |
|
|
24
|
+
|------|------|
|
|
25
|
+
| 诊断日志 | always-on 本地图流水线:匹配 → exists → 上传 → API 前 payload → residual |
|
|
26
|
+
| MediaIdTrace | `mdImgs=[local:…\|mediaId:…]`、`residualLocal`、preview |
|
|
27
|
+
| 业务逻辑 | **与 fix45 相同**(图+下载同泡、只认 `![]`、代码块跳过、CardCache) |
|
|
28
|
+
|
|
29
|
+
## 查灰图时看哪些行
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
[DingTalk][LocalImage] sendTextToDingTalk 入口 | ...
|
|
33
|
+
[DingTalk][LocalImage] processImagesForOutbound 扫描 ![]() | #1 kind=local exists=...
|
|
34
|
+
[DingTalk][LocalImage] processImagesForOutbound 本地→mediaId 成功 | ... → @lADP...
|
|
35
|
+
[DingTalk][MediaIdTrace] sendTextToDingTalk:API前 | mdImgs=[mediaId:@lADP...] residualLocal=0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `kind=local` 但 `exists=false` → 路径在 gateway 机上不存在
|
|
39
|
+
- 上传失败 → 会看到失败提示 / fallback 第2次
|
|
40
|
+
- API前仍是 `local:/tmp/...` 或 `residualLocal>0` → 未换成 mediaId
|
|
41
|
+
- API前已是 `mediaId:@lADP...` 仍灰 → 偏钉钉渲染/账号侧
|
|
42
|
+
|
|
43
|
+
## 升级
|
|
44
|
+
|
|
45
|
+
从 fix45 直接 `--force` 安装即可。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Release Notes — v0.8.21-fix47(2026-07-20)
|
|
2
|
+
|
|
3
|
+
## 安装 / Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx -y @jeik/dingtalk-connector install --force
|
|
7
|
+
openclaw gateway restart
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## 修复 / Fixed
|
|
11
|
+
|
|
12
|
+
### `file://` MD 本地图灰图
|
|
13
|
+
|
|
14
|
+
**现象(fix46 日志):**
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
扫描 ![]() | #2 kind=http path=file:///mnt/smb/...
|
|
18
|
+
processImagesForOutbound 远程图 | file:///mnt/...
|
|
19
|
+
远程图下载异常 | fetch failed
|
|
20
|
+
localTried=0 remoteTried=3
|
|
21
|
+
API前 mdImgs 仍含 http:file:///...
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**原因:** `looksLikeRemoteUrl` 把任意含 `://` 的字符串当远程;`isLocalImageRef` 又先调用它,`file://` 永远进不了本地上传。
|
|
25
|
+
|
|
26
|
+
**修复:** `file://` / `MEDIA:` / `attachment://` 优先判本地 → `toLocalPath` → 上传 mediaId。
|
|
27
|
+
|
|
28
|
+
裸绝对路径 `/mnt/...`、`/tmp/...` 本来就正常;本版修的是 **带 `file://` 前缀** 的写法。
|
|
29
|
+
|
|
30
|
+
## 升级
|
|
31
|
+
|
|
32
|
+
从 fix46 直接 `--force` 安装。
|
package/package.json
CHANGED
|
@@ -28,18 +28,17 @@ export const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
|
|
|
28
28
|
*/
|
|
29
29
|
export const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/** 是否为需上传的本地图引用(与 media.ts 权威实现一致:file:// 必须算本地) */
|
|
32
32
|
export function isLocalImageRef(rawPath: string): boolean {
|
|
33
33
|
const p = (rawPath || "").trim();
|
|
34
34
|
if (!p) return false;
|
|
35
|
-
|
|
36
|
-
if (p.includes("://") && !p.startsWith("file://") && !p.startsWith("MEDIA:") && !p.startsWith("attachment://")) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
|
|
35
|
+
// file:// 等本地 scheme 优先(勿被 includes("://") 挡掉)
|
|
40
36
|
if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) {
|
|
41
37
|
return true;
|
|
42
38
|
}
|
|
39
|
+
if (/^https?:\/\//i.test(p)) return false;
|
|
40
|
+
if (p.includes("://")) return false;
|
|
41
|
+
if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
|
|
43
42
|
if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
|
|
44
43
|
if (IMAGE_EXTENSIONS.test(p.split("?")[0] || p)) return true;
|
|
45
44
|
return !p.includes("://");
|
package/src/services/media.ts
CHANGED
|
@@ -45,11 +45,26 @@ export const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
|
|
|
45
45
|
*/
|
|
46
46
|
export const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
47
47
|
|
|
48
|
-
/**
|
|
48
|
+
/** 本地文件 URI / OpenClaw 媒体前缀(不是 http 远程) */
|
|
49
|
+
function isLocalFileScheme(p: string): boolean {
|
|
50
|
+
return (
|
|
51
|
+
p.startsWith("file://") ||
|
|
52
|
+
p.startsWith("MEDIA:") ||
|
|
53
|
+
p.startsWith("attachment://")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 是否像远程 URL(禁止当本地路径上传)。
|
|
59
|
+
* **不含** file:// / MEDIA: / attachment://(那些是本地引用)。
|
|
60
|
+
*/
|
|
49
61
|
export function looksLikeRemoteUrl(raw: string): boolean {
|
|
50
62
|
const p = (raw || "").trim();
|
|
51
63
|
if (!p) return false;
|
|
64
|
+
// 本地 scheme 优先:否则 file:// 会被 includes("://") 误判成远程 → fetch 失败 → 灰图
|
|
65
|
+
if (isLocalFileScheme(p)) return false;
|
|
52
66
|
if (/^https?:\/\//i.test(p)) return true;
|
|
67
|
+
// 伪协议截断如 p://(Windows 路径被当 URL 的历史坑)
|
|
53
68
|
if (/^[a-z]:\/\//i.test(p) && !/^[a-z]:[\\/][^/\\]/i.test(p)) return true;
|
|
54
69
|
if (p.includes("://")) return true;
|
|
55
70
|
return false;
|
|
@@ -156,12 +171,11 @@ export function protectNonImageOccurrencesOfTargets(
|
|
|
156
171
|
export function isLocalImageRef(rawPath: string): boolean {
|
|
157
172
|
const p = (rawPath || "").trim();
|
|
158
173
|
if (!p) return false;
|
|
174
|
+
// file:// / MEDIA: / attachment:// 一律本地(必须先于 looksLikeRemoteUrl)
|
|
175
|
+
if (isLocalFileScheme(p)) return true;
|
|
159
176
|
// 已是远程 URL 或钉钉 mediaId(@ 开头)→ 不上传
|
|
160
177
|
if (looksLikeRemoteUrl(p) || /^https?:\/\//i.test(p)) return false;
|
|
161
178
|
if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
|
|
162
|
-
if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) {
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
179
|
// Unix 绝对路径 / Windows 盘符
|
|
166
180
|
if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
|
|
167
181
|
// 有图片后缀,或路径中明显是文件(含扩展名)
|
|
@@ -211,8 +225,9 @@ export interface UploadResult {
|
|
|
211
225
|
}
|
|
212
226
|
|
|
213
227
|
/**
|
|
214
|
-
*
|
|
215
|
-
* verbose
|
|
228
|
+
* 本地图诊断:默认 console.log 一行(不走 log.warn,避免 [sendTextToDingTalk] 双前缀)。
|
|
229
|
+
* verbose=true 的细节(realpath 等)仅在 debug 时输出。
|
|
230
|
+
* 决策路径(匹配/exists/上传成败/残留)务必 always-on,方便查灰图。
|
|
216
231
|
*/
|
|
217
232
|
function logLocalImage(log: any, step: string, detail?: string, verbose = false) {
|
|
218
233
|
const line = detail
|
|
@@ -229,12 +244,22 @@ function logLocalImage(log: any, step: string, detail?: string, verbose = false)
|
|
|
229
244
|
console.log(line);
|
|
230
245
|
}
|
|
231
246
|
|
|
232
|
-
/** 钉钉 mediaId
|
|
247
|
+
/** 钉钉 mediaId 形态(oapi 返回常带 @) */
|
|
233
248
|
const MEDIA_ID_RE = /@[A-Za-z0-9_-]{8,}/g;
|
|
234
249
|
|
|
250
|
+
function classifyMdImgTarget(target: string): "mediaId" | "http" | "local" | "other" {
|
|
251
|
+
const t = (target || "").trim();
|
|
252
|
+
if (!t) return "other";
|
|
253
|
+
if (t.startsWith("@") && !t.includes("/") && !t.includes("\\")) return "mediaId";
|
|
254
|
+
if (looksLikeRemoteUrl(t) || /^https?:\/\//i.test(t)) return "http";
|
|
255
|
+
if (isLocalImageRef(t)) return "local";
|
|
256
|
+
return "other";
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
260
|
+
* MediaId 追踪:关键节点 / force / 裸 mediaId 时输出。
|
|
261
|
+
* 固定字段:mediaIdCount / ids / BARE_ids / mdImgs / httpCount / residualLocal / preview
|
|
262
|
+
* (灰图排查:看 mdImgs 是 path:本地 还是 mediaId:@xxx,以及 residualLocal)
|
|
238
263
|
*/
|
|
239
264
|
export function logMediaIdTrace(
|
|
240
265
|
stage: string,
|
|
@@ -244,23 +269,42 @@ export function logMediaIdTrace(
|
|
|
244
269
|
): void {
|
|
245
270
|
const text = typeof content === "string" ? content : "";
|
|
246
271
|
const ids = [...new Set([...text.matchAll(MEDIA_ID_RE)].map((m) => m[0]))];
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
);
|
|
272
|
+
const mdTargets = [...text.matchAll(LOCAL_IMAGE_RE)].map((m) => (m[2] || "").trim());
|
|
273
|
+
const inMd = new Set(mdTargets);
|
|
250
274
|
const bareIds = ids.filter((id) => !inMd.has(id));
|
|
275
|
+
const mdImgs = mdTargets.slice(0, 8).map((t) => {
|
|
276
|
+
const kind = classifyMdImgTarget(t);
|
|
277
|
+
const short = t.length > 100 ? `${t.slice(0, 100)}…` : t;
|
|
278
|
+
return `${kind}:${short}`;
|
|
279
|
+
});
|
|
280
|
+
const residualLocal = mdTargets.filter((t) => isLocalImageRef(t));
|
|
281
|
+
const httpCount = (text.match(/https?:\/\//gi) || []).length;
|
|
251
282
|
// sampleImageMsg 的 photoURL 就是裸 mediaId,不算异常
|
|
252
283
|
const isImageMsg = /type=image|sampleImage|photoURL/i.test(`${stage} ${extra || ""}`);
|
|
253
284
|
const suspiciousBare = isImageMsg ? [] : bareIds;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
285
|
+
const always =
|
|
286
|
+
force ||
|
|
287
|
+
suspiciousBare.length > 0 ||
|
|
288
|
+
residualLocal.length > 0 ||
|
|
289
|
+
/API前|完成|:out|:in|after-process|processImages/i.test(stage);
|
|
290
|
+
if (!always) return;
|
|
291
|
+
|
|
292
|
+
const preview = text.replace(/\n/g, "\\n").slice(0, 200);
|
|
258
293
|
console.log(
|
|
259
294
|
`[DingTalk][MediaIdTrace] ${stage} | len=${text.length}` +
|
|
260
|
-
|
|
261
|
-
(
|
|
295
|
+
` mediaIdCount=${ids.length}` +
|
|
296
|
+
(ids.length ? ` ids=[${ids.slice(0, 4).join(",")}]` : "") +
|
|
297
|
+
` BARE_ids=${suspiciousBare.length ? suspiciousBare.join(",") : "无"}` +
|
|
298
|
+
` mdImgs=[${mdImgs.length ? mdImgs.join(" || ") : "-"}]` +
|
|
299
|
+
` httpCount=${httpCount}` +
|
|
300
|
+
(residualLocal.length
|
|
301
|
+
? ` residualLocal=${residualLocal.length}:${residualLocal
|
|
302
|
+
.slice(0, 3)
|
|
303
|
+
.map((p) => p.slice(0, 80))
|
|
304
|
+
.join("|")}`
|
|
305
|
+
: " residualLocal=0") +
|
|
262
306
|
(extra ? ` ${extra}` : "") +
|
|
263
|
-
`
|
|
307
|
+
` preview="${preview}"`,
|
|
264
308
|
);
|
|
265
309
|
}
|
|
266
310
|
|
|
@@ -298,7 +342,8 @@ export async function uploadMediaToDingTalk(
|
|
|
298
342
|
const tag = `uploadMedia type=${mediaType}`;
|
|
299
343
|
try {
|
|
300
344
|
let absPath = toLocalPath(filePath);
|
|
301
|
-
|
|
345
|
+
// 上传起止 always-on:灰图时必须能看到是否走到 OAPI、文件是否可读
|
|
346
|
+
logLocalImage(log, `${tag} 开始`, `path=${absPath}`);
|
|
302
347
|
|
|
303
348
|
if (!fs.existsSync(absPath)) {
|
|
304
349
|
logLocalImage(log, `${tag} 失败:文件不存在`, absPath);
|
|
@@ -309,7 +354,7 @@ export async function uploadMediaToDingTalk(
|
|
|
309
354
|
try {
|
|
310
355
|
const real = fs.realpathSync(absPath);
|
|
311
356
|
if (real !== absPath) {
|
|
312
|
-
logLocalImage(log, `${tag} realpath`, `${absPath} → ${real}
|
|
357
|
+
logLocalImage(log, `${tag} realpath`, `${absPath} → ${real}`);
|
|
313
358
|
}
|
|
314
359
|
absPath = real;
|
|
315
360
|
} catch (e: any) {
|
|
@@ -319,7 +364,7 @@ export async function uploadMediaToDingTalk(
|
|
|
319
364
|
// 检查文件大小
|
|
320
365
|
const stats = fs.statSync(absPath);
|
|
321
366
|
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
322
|
-
logLocalImage(log, `${tag} 文件可读`, `size=${fileSizeMB}MB
|
|
367
|
+
logLocalImage(log, `${tag} 文件可读`, `size=${fileSizeMB}MB mode=${stats.mode}`);
|
|
323
368
|
|
|
324
369
|
// 钉钉 image 类型官方上限约 10MB;调用方若传入更大 limit 也按类型收紧
|
|
325
370
|
const effectiveMax =
|
|
@@ -429,13 +474,17 @@ export async function uploadLocalImageWithFallback(
|
|
|
429
474
|
log?: any,
|
|
430
475
|
): Promise<UploadResult | null> {
|
|
431
476
|
const absPath = toLocalPath(filePath);
|
|
432
|
-
|
|
477
|
+
const exists = fs.existsSync(absPath);
|
|
478
|
+
logLocalImage(log, "fallback 入口", `path=${absPath} exists=${exists}`);
|
|
479
|
+
if (!exists) {
|
|
433
480
|
logLocalImage(log, "fallback 失败:文件不存在", absPath);
|
|
434
481
|
return null;
|
|
435
482
|
}
|
|
436
483
|
|
|
484
|
+
logLocalImage(log, "fallback 第1次:直接上传", absPath);
|
|
437
485
|
let result = await uploadMediaToDingTalk(absPath, "image", oapiToken, 10 * 1024 * 1024, log);
|
|
438
486
|
if (result?.mediaId) {
|
|
487
|
+
logLocalImage(log, "fallback 第1次成功", result.mediaId);
|
|
439
488
|
return result;
|
|
440
489
|
}
|
|
441
490
|
|
|
@@ -447,7 +496,13 @@ export async function uploadLocalImageWithFallback(
|
|
|
447
496
|
);
|
|
448
497
|
try {
|
|
449
498
|
fs.copyFileSync(absPath, tmp);
|
|
499
|
+
logLocalImage(log, "fallback 第2次:tmp重试", `${absPath} → ${tmp}`);
|
|
450
500
|
result = await uploadMediaToDingTalk(tmp, "image", oapiToken, 10 * 1024 * 1024, log);
|
|
501
|
+
if (result?.mediaId) {
|
|
502
|
+
logLocalImage(log, "fallback 第2次成功", result.mediaId);
|
|
503
|
+
} else {
|
|
504
|
+
logLocalImage(log, "fallback 第2次仍失败", tmp);
|
|
505
|
+
}
|
|
451
506
|
return result;
|
|
452
507
|
} catch (err: any) {
|
|
453
508
|
logLocalImage(
|
|
@@ -543,68 +598,158 @@ function replaceOutsideImageTargets(
|
|
|
543
598
|
* (钉钉改写发生在「![] 与正文共用同一 http」时;![] 改成 mediaId 后即可同泡共存)
|
|
544
599
|
* - 本地:正文重复的本地路径换成 down.dingtalk.com 可点链接(仍同一条消息)
|
|
545
600
|
* - **不再拆第二条 text 消息**
|
|
601
|
+
*
|
|
602
|
+
* 日志约定(always-on,查灰图必看):
|
|
603
|
+
* - 开始 / 扫描每张 ![] / 本地 exists / 上传成败 / 远程下载 / 残留 / out
|
|
546
604
|
*/
|
|
547
605
|
export async function processImagesForOutbound(
|
|
548
606
|
content: string,
|
|
549
607
|
oapiToken: string | null,
|
|
550
608
|
log?: any,
|
|
551
609
|
): Promise<ProcessImagesOutboundResult> {
|
|
552
|
-
|
|
610
|
+
logLocalImage(
|
|
611
|
+
log,
|
|
612
|
+
"processImagesForOutbound 开始",
|
|
613
|
+
`contentLen=${content?.length ?? 0} hasToken=${!!oapiToken}`,
|
|
614
|
+
);
|
|
615
|
+
if (!oapiToken) {
|
|
616
|
+
logLocalImage(log, "processImagesForOutbound 跳过", "无 oapiToken(本地路径无法上传→必灰图)");
|
|
617
|
+
logMediaIdTrace("processImagesForOutbound:skip-no-token", content, undefined, true);
|
|
618
|
+
return { text: content || "", followUpUrls: [] };
|
|
619
|
+
}
|
|
620
|
+
if (!content || typeof content !== "string") {
|
|
621
|
+
logLocalImage(log, "processImagesForOutbound 跳过", "content 为空");
|
|
553
622
|
return { text: content || "", followUpUrls: [] };
|
|
554
623
|
}
|
|
555
624
|
|
|
625
|
+
logMediaIdTrace("processImagesForOutbound:in", content, undefined, true);
|
|
626
|
+
|
|
556
627
|
const codeRanges = getMarkdownCodeRanges(content);
|
|
628
|
+
if (codeRanges.length > 0) {
|
|
629
|
+
logLocalImage(
|
|
630
|
+
log,
|
|
631
|
+
"processImagesForOutbound 代码区域",
|
|
632
|
+
`count=${codeRanges.length}(围栏/行内 code 内的图路径将跳过)`,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
557
636
|
const mdMatches = [...content.matchAll(LOCAL_IMAGE_RE)];
|
|
637
|
+
const scanPreview = mdMatches.map((m, i) => {
|
|
638
|
+
const p = (m[2] || "").trim();
|
|
639
|
+
const inCode = m.index !== undefined && isIndexInCodeRange(m.index, codeRanges);
|
|
640
|
+
const kind = classifyMdImgTarget(p);
|
|
641
|
+
return `#${i + 1} kind=${kind} inCode=${inCode} path=${p.slice(0, 120)}`;
|
|
642
|
+
});
|
|
643
|
+
logLocalImage(
|
|
644
|
+
log,
|
|
645
|
+
"processImagesForOutbound 扫描 ![]()",
|
|
646
|
+
scanPreview.length ? scanPreview.join(" || ") : "正文中没有任何 markdown 图片语法",
|
|
647
|
+
);
|
|
648
|
+
|
|
558
649
|
let result = content;
|
|
559
650
|
/** 本地路径 → 可点 downloadUrl(仅替换正文中的裸路径,不拆消息) */
|
|
560
651
|
const localPathToDl = new Map<string, string>();
|
|
561
652
|
let uploadCount = 0;
|
|
653
|
+
let localTried = 0;
|
|
654
|
+
let remoteTried = 0;
|
|
655
|
+
let skippedCode = 0;
|
|
656
|
+
let skippedOther = 0;
|
|
562
657
|
|
|
563
658
|
for (const match of mdMatches) {
|
|
564
|
-
if (match.index !== undefined && isIndexInCodeRange(match.index, codeRanges))
|
|
659
|
+
if (match.index !== undefined && isIndexInCodeRange(match.index, codeRanges)) {
|
|
660
|
+
skippedCode++;
|
|
661
|
+
logLocalImage(
|
|
662
|
+
log,
|
|
663
|
+
"processImagesForOutbound 跳过代码块内",
|
|
664
|
+
(match[2] || "").trim().slice(0, 120),
|
|
665
|
+
);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
565
668
|
const [fullMatch, alt, rawPath] = match;
|
|
566
669
|
const cleanPath = (rawPath || "").replace(/\\ /g, " ").trim();
|
|
567
670
|
if (!cleanPath) continue;
|
|
568
671
|
|
|
569
672
|
if (isLocalImageRef(cleanPath)) {
|
|
673
|
+
localTried++;
|
|
570
674
|
const absPath = toLocalPath(cleanPath);
|
|
675
|
+
let exists = false;
|
|
676
|
+
let sizeMB = "-";
|
|
677
|
+
try {
|
|
678
|
+
exists = fs.existsSync(absPath);
|
|
679
|
+
if (exists) {
|
|
680
|
+
sizeMB = (fs.statSync(absPath).size / (1024 * 1024)).toFixed(2);
|
|
681
|
+
}
|
|
682
|
+
} catch (e: any) {
|
|
683
|
+
logLocalImage(log, "processImagesForOutbound 本地 stat 异常", e?.message || String(e));
|
|
684
|
+
}
|
|
685
|
+
logLocalImage(
|
|
686
|
+
log,
|
|
687
|
+
"processImagesForOutbound 处理本地图",
|
|
688
|
+
`alt=${alt || "-"} raw=${cleanPath.slice(0, 120)} abs=${absPath} exists=${exists} sizeMB=${sizeMB}`,
|
|
689
|
+
);
|
|
690
|
+
if (!exists) {
|
|
691
|
+
result = result
|
|
692
|
+
.split(fullMatch)
|
|
693
|
+
.join(`📷 *图片文件不存在(${path.basename(absPath)})*`);
|
|
694
|
+
logLocalImage(log, "processImagesForOutbound 本地文件不存在→失败提示", absPath);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
571
697
|
try {
|
|
572
698
|
const uploadResult = await uploadLocalImageWithFallback(absPath, oapiToken, log);
|
|
573
699
|
if (uploadResult?.mediaId) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.join(``);
|
|
700
|
+
const replacement = ``;
|
|
701
|
+
result = result.split(fullMatch).join(replacement);
|
|
577
702
|
uploadCount++;
|
|
578
703
|
if (uploadResult.downloadUrl) {
|
|
579
704
|
localPathToDl.set(cleanPath, uploadResult.downloadUrl);
|
|
580
705
|
localPathToDl.set(absPath, uploadResult.downloadUrl);
|
|
581
706
|
}
|
|
707
|
+
logLocalImage(
|
|
708
|
+
log,
|
|
709
|
+
"processImagesForOutbound 本地→mediaId 成功",
|
|
710
|
+
`${absPath} → ${uploadResult.mediaId}`,
|
|
711
|
+
);
|
|
582
712
|
} else {
|
|
583
713
|
result = result
|
|
584
714
|
.split(fullMatch)
|
|
585
715
|
.join(`📷 *图片上传失败(${path.basename(absPath)})*`);
|
|
716
|
+
logLocalImage(log, "processImagesForOutbound 本地上传失败→失败提示", absPath);
|
|
586
717
|
}
|
|
587
|
-
} catch {
|
|
718
|
+
} catch (err: any) {
|
|
588
719
|
result = result
|
|
589
720
|
.split(fullMatch)
|
|
590
721
|
.join(`📷 *图片上传异常(${path.basename(toLocalPath(cleanPath))})*`);
|
|
722
|
+
logLocalImage(
|
|
723
|
+
log,
|
|
724
|
+
"processImagesForOutbound 本地上传异常",
|
|
725
|
+
`${absPath} err=${err?.message || err}`,
|
|
726
|
+
);
|
|
591
727
|
}
|
|
592
728
|
continue;
|
|
593
729
|
}
|
|
594
730
|
|
|
595
731
|
if (looksLikeRemoteUrl(cleanPath)) {
|
|
732
|
+
remoteTried++;
|
|
733
|
+
logLocalImage(log, "processImagesForOutbound 远程图", cleanPath.slice(0, 160));
|
|
596
734
|
let tmp: string | null = null;
|
|
597
735
|
try {
|
|
598
736
|
tmp = await downloadRemoteUrlToTemp(cleanPath);
|
|
599
|
-
if (!tmp)
|
|
737
|
+
if (!tmp) {
|
|
738
|
+
logLocalImage(log, "processImagesForOutbound 远程下载失败(保留原![]URL)", cleanPath.slice(0, 120));
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
600
741
|
const uploadResult = await uploadLocalImageWithFallback(tmp, oapiToken, log);
|
|
601
742
|
if (uploadResult?.mediaId) {
|
|
602
743
|
// 只改 ![] 为 mediaId;正文「下载链接:http://…」保持原 URL,同一条消息
|
|
603
744
|
result = result.split(fullMatch).join(``);
|
|
604
745
|
uploadCount++;
|
|
605
|
-
|
|
606
|
-
|
|
746
|
+
logLocalImage(
|
|
747
|
+
log,
|
|
748
|
+
"processImagesForOutbound 远程→mediaId 下载链保留原URL",
|
|
749
|
+
`${cleanPath.slice(0, 80)} → ${uploadResult.mediaId}`,
|
|
607
750
|
);
|
|
751
|
+
} else {
|
|
752
|
+
logLocalImage(log, "processImagesForOutbound 远程上传失败(保留原![]URL)", cleanPath.slice(0, 120));
|
|
608
753
|
}
|
|
609
754
|
} catch (err: any) {
|
|
610
755
|
logLocalImage(log, "远程图处理失败", err?.message || String(err));
|
|
@@ -617,18 +762,55 @@ export async function processImagesForOutbound(
|
|
|
617
762
|
}
|
|
618
763
|
}
|
|
619
764
|
}
|
|
765
|
+
continue;
|
|
620
766
|
}
|
|
767
|
+
|
|
768
|
+
skippedOther++;
|
|
769
|
+
logLocalImage(
|
|
770
|
+
log,
|
|
771
|
+
"processImagesForOutbound 跳过非本地非远程",
|
|
772
|
+
`kind=${classifyMdImgTarget(cleanPath)} path=${cleanPath.slice(0, 120)}`,
|
|
773
|
+
);
|
|
621
774
|
}
|
|
622
775
|
|
|
623
776
|
// 本地路径若在「下载链接」等处再出现:换成可点 downloadUrl(仍同一气泡)
|
|
624
777
|
for (const [p, dl] of localPathToDl) {
|
|
778
|
+
const before = result;
|
|
625
779
|
result = replaceOutsideImageTargets(result, p, dl);
|
|
780
|
+
if (before !== result) {
|
|
781
|
+
logLocalImage(
|
|
782
|
+
log,
|
|
783
|
+
"processImagesForOutbound 正文裸本地路径→downloadUrl",
|
|
784
|
+
`${p.slice(0, 80)} → ${dl}`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
626
787
|
}
|
|
627
788
|
|
|
628
|
-
|
|
629
|
-
|
|
789
|
+
const residual = [...result.matchAll(LOCAL_IMAGE_RE)]
|
|
790
|
+
.filter((m) => {
|
|
791
|
+
if (m.index !== undefined && isIndexInCodeRange(m.index, getMarkdownCodeRanges(result))) {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
return isLocalImageRef((m[2] || "").trim());
|
|
795
|
+
})
|
|
796
|
+
.map((m) => (m[2] || "").trim());
|
|
797
|
+
|
|
798
|
+
logLocalImage(
|
|
799
|
+
log,
|
|
800
|
+
"processImagesForOutbound 完成",
|
|
801
|
+
`uploads=${uploadCount} localTried=${localTried} remoteTried=${remoteTried} skippedCode=${skippedCode} skippedOther=${skippedOther} outLen=${result.length}` +
|
|
802
|
+
(residual.length
|
|
803
|
+
? ` ⚠️residualLocal=${residual.length}:${residual.slice(0, 3).join("|")}`
|
|
804
|
+
: " residualLocal=0"),
|
|
630
805
|
);
|
|
631
|
-
|
|
806
|
+
if (residual.length) {
|
|
807
|
+
logLocalImage(
|
|
808
|
+
log,
|
|
809
|
+
"processImagesForOutbound ⚠️仍有代码块外本地路径未替换(钉钉必灰图)",
|
|
810
|
+
residual.join(" | "),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
logMediaIdTrace("processImagesForOutbound:out", result, undefined, true);
|
|
632
814
|
|
|
633
815
|
return { text: result, followUpUrls: [] };
|
|
634
816
|
}
|
|
@@ -842,12 +842,29 @@ export async function sendTextToDingTalk(params: {
|
|
|
842
842
|
|
|
843
843
|
const targetParam = resolveOutboundTarget(target);
|
|
844
844
|
|
|
845
|
+
console.log(
|
|
846
|
+
`[DingTalk][LocalImage] sendTextToDingTalk 入口 | target=${target} textLen=${text?.length ?? 0}`,
|
|
847
|
+
);
|
|
848
|
+
logMediaIdTrace("sendTextToDingTalk:in", text, `target=${target}`, true);
|
|
849
|
+
|
|
845
850
|
// 图片:![] 上传为 mediaId;下载链接原 URL 留在同一条 markdown(不拆消息)
|
|
851
|
+
const beforeLen = text?.length ?? 0;
|
|
846
852
|
try {
|
|
847
853
|
const oapiToken = await getOapiAccessToken(config);
|
|
854
|
+
console.log(
|
|
855
|
+
`[DingTalk][LocalImage] sendTextToDingTalk 入口 | target=${target} textLen=${beforeLen} hasToken=${!!oapiToken}`,
|
|
856
|
+
);
|
|
848
857
|
if (oapiToken && text) {
|
|
849
858
|
const processed = await processImagesForOutbound(text, oapiToken, log);
|
|
850
859
|
text = processed.text;
|
|
860
|
+
console.log(
|
|
861
|
+
`[DingTalk][LocalImage] sendTextToDingTalk 处理后 | ${beforeLen}→${text.length} 字`,
|
|
862
|
+
);
|
|
863
|
+
logMediaIdTrace("sendTextToDingTalk:after-process", text, undefined, true);
|
|
864
|
+
} else if (!oapiToken) {
|
|
865
|
+
console.warn(
|
|
866
|
+
`[DingTalk][LocalImage] sendTextToDingTalk 无 oapiToken,跳过图片上传(本地 MD 图会灰)`,
|
|
867
|
+
);
|
|
851
868
|
}
|
|
852
869
|
} catch (err: any) {
|
|
853
870
|
console.warn(
|
|
@@ -856,7 +873,7 @@ export async function sendTextToDingTalk(params: {
|
|
|
856
873
|
}
|
|
857
874
|
|
|
858
875
|
const msgType: DingTalkMsgType = shouldSendAsMarkdown(text) ? "markdown" : "text";
|
|
859
|
-
logMediaIdTrace("sendTextToDingTalk:API前", text, `msgType=${msgType}
|
|
876
|
+
logMediaIdTrace("sendTextToDingTalk:API前", text, `msgType=${msgType}`, true);
|
|
860
877
|
|
|
861
878
|
return sendProactive(config, targetParam, text, {
|
|
862
879
|
msgType,
|