@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 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` 对含 `://` 一律当远程,导致 `![x](file:///mnt/...)` / `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-fix45</strong> (production-stable; install: <code>npx -y @jeik/dingtalk-connector install</code>).</p>
7
+ <p><strong>Current published release: <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.21-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-fix45 · 2026-07-20 (current)
26
+ ### 🚀 v0.8.21-fix47 · 2026-07-20 (current)
27
27
 
28
- **Theme: image + download link in one bubble · only `![]` · quote card cache**
28
+ **Theme: fix `file://` misclassified as remote URL**
29
29
 
30
30
  | | Change |
31
31
  |--|--------|
32
- | 📷 **Image + download URL** | `![](http)` → 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 |
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-fix45</strong>(稳定生产可用;一键安装:`npx -y @jeik/dingtalk-connector install`;本地 tgz:`openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix45.tgz --force`)</p>
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-fix45 · 2026-07-20(当前)
26
+ ### 🚀 v0.8.21-fix47 · 2026-07-20(当前)
27
27
 
28
- **主题:图 + 下载链接单气泡 · 只认 `![]` · 引用卡片缓存**
28
+ **主题:`file://` 本地图误判远程修复**
29
29
 
30
30
  | | 改动 |
31
31
  |--|------|
32
- | 📷 **图 + 下载链** | `![](http)` 上传为 mediaId;**下载链接原 URL 留在同一条消息**(不被钉钉改成 `@lADP`) |
33
- | ✅ **只处理 `![]()`** | 不扫裸路径;代码块内路径不上传、不误报灰图 |
34
- | 📎 **引用 AI 卡** | 定稿缓存正文,引用时回填(`CardCache`) |
35
- | 🌐 **message media** | `media: "https://..."` 下载再上传 |
32
+ | 🐛 **`file://` 灰图** | `![x](file:///mnt/...)` / `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 fix45](docs/RELEASE_NOTES_V0.8.21-fix45.md)
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
 
@@ -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-b4qQpdOB.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-DHMdV0k8.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });
@@ -0,0 +1,2 @@
1
+ import { t as registerGatewayMethods } from "./gateway-methods-DNvtOYEG.mjs";
2
+ export { registerGatewayMethods };
@@ -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-CpOPEOI8.mjs";
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-Dj8jSHjE.mjs";
2
- import { t as registerGatewayMethods } from "./gateway-methods-CCh-LutW.mjs";
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-_qA1Qjt_.mjs";
1
+ import { _ as toLocalPath, d as processLocalImages, f as processRawMediaPaths, h as sendFileProactive, o as hasResidualLocalMdImagesOutsideCode, p as processVideoMarkers } from "./media-DnQB2BrL.mjs";
2
2
  export { hasResidualLocalMdImagesOutsideCode, processLocalImages, processRawMediaPaths, processVideoMarkers, sendFileProactive, toLocalPath };
@@ -30,10 +30,18 @@ const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
30
30
  * 注意:旧版只匹配 /tmp|/home|/root 等前缀,**漏掉 /mnt 共享盘**,导致 message 工具 MD 灰图。
31
31
  */
32
32
  const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
33
- /** 是否像远程 URL 或其截断片段(禁止当本地路径上传) */
33
+ /** 本地文件 URI / OpenClaw 媒体前缀(不是 http 远程) */
34
+ function isLocalFileScheme(p) {
35
+ return p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://");
36
+ }
37
+ /**
38
+ * 是否像远程 URL(禁止当本地路径上传)。
39
+ * **不含** file:// / MEDIA: / attachment://(那些是本地引用)。
40
+ */
34
41
  function looksLikeRemoteUrl(raw) {
35
42
  const p = (raw || "").trim();
36
43
  if (!p) return false;
44
+ if (isLocalFileScheme(p)) return false;
37
45
  if (/^https?:\/\//i.test(p)) return true;
38
46
  if (/^[a-z]:\/\//i.test(p) && !/^[a-z]:[\\/][^/\\]/i.test(p)) return true;
39
47
  if (p.includes("://")) return true;
@@ -43,9 +51,9 @@ function looksLikeRemoteUrl(raw) {
43
51
  function isLocalImageRef(rawPath) {
44
52
  const p = (rawPath || "").trim();
45
53
  if (!p) return false;
54
+ if (isLocalFileScheme(p)) return true;
46
55
  if (looksLikeRemoteUrl(p) || /^https?:\/\//i.test(p)) return false;
47
56
  if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
48
- if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) return true;
49
57
  if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
50
58
  if (IMAGE_EXTENSIONS.test(p.split("?")[0] || p)) return true;
51
59
  return !p.includes("://");
@@ -68,8 +76,9 @@ function toLocalPath(raw) {
68
76
  return filePath;
69
77
  }
70
78
  /**
71
- * 本地图诊断:默认只 console 一行,不再 log.warn(避免 [sendTextToDingTalk] 双前缀刷屏)。
72
- * verbose 步骤(文件可读/realpath 等)仅 debug 时通过 log 输出。
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
- * 精简 MediaId 追踪:默认只在关键节点 / 异常时打一行。
88
- * force=true 或发现「裸 mediaId」时才输出。
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 inMd = new Set([...text.matchAll(LOCAL_IMAGE_RE)].map((m) => (m[2] || "").trim()));
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 && 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}`);
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}`, true);
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}`, true);
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`, true);
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
- if (!fs.existsSync(absPath)) {
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) return result;
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
- if (!oapiToken || !content || typeof content !== "string") return {
284
- text: content || "",
285
- followUpUrls: []
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)) continue;
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
- result = result.split(fullMatch).join(`![${alt || path.basename(absPath)}](${uploadResult.mediaId})`);
383
+ const replacement = `![${alt || path.basename(absPath)}](${uploadResult.mediaId})`;
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
- } else result = result.split(fullMatch).join(`📷 *图片上传失败(${path.basename(absPath)})*`);
310
- } catch {
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) continue;
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(`![${alt || "image"}](${uploadResult.mediaId})`);
323
414
  uploadCount++;
324
- console.log(`[DingTalk][LocalImage] ![]远程→mediaId 下载链保留原URL | ${uploadResult.mediaId}`);
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
- 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);
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-Dj8jSHjE.mjs";
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-CpOPEOI8.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-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-ClfWY5Cw.mjs");
65
+ const { processLocalImages: impl } = await import("./media-BqpUGCni.mjs");
66
66
  return impl(content, oapiToken, log);
67
67
  }
68
68
  //#endregion
@@ -581,14 +581,14 @@ function createDingtalkReplyDispatcher(params) {
581
581
  finalText = await processLocalImages(finalText, oapiToken, log);
582
582
  if (beforeImg !== finalText) console.log(`[DingTalk][LocalImage] closeStreaming 图片处理 | ${beforeImg.length}→${finalText.length}`);
583
583
  try {
584
- const { hasResidualLocalMdImagesOutsideCode } = await import("./media-ClfWY5Cw.mjs");
584
+ const { hasResidualLocalMdImagesOutsideCode } = await import("./media-BqpUGCni.mjs");
585
585
  if (hasResidualLocalMdImagesOutsideCode(finalText)) console.warn(`[DingTalk][LocalImage] closeStreaming 定稿后仍有代码块外的本地 MD 图未上传,钉钉可能灰图`);
586
586
  } catch {}
587
587
  finalText = await processVideoMarkers(finalText, "", account.config, oapiToken, log, true, target);
588
588
  finalText = await processAudioMarkers(finalText, "", account.config, oapiToken, log, true, target);
589
589
  finalText = await uploadAndReplaceFileMarkers(finalText, "", account.config, oapiToken, log, true, target);
590
590
  log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
591
- const { processRawMediaPaths } = await import("./media-ClfWY5Cw.mjs");
591
+ const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
592
592
  finalText = await processRawMediaPaths(finalText, account.config, oapiToken, log, target);
593
593
  log.info(`[DingTalk][closeStreaming] processRawMediaPaths 处理完成`);
594
594
  } else log.warn(`[DingTalk][closeStreaming] oapiToken 为空,跳过媒体处理`);
@@ -744,7 +744,7 @@ function createDingtalkReplyDispatcher(params) {
744
744
  const oapiToken = await getOapiAccessToken(account.config);
745
745
  if (oapiToken) {
746
746
  log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
747
- const { processRawMediaPaths } = await import("./media-ClfWY5Cw.mjs");
747
+ const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
748
748
  text = await processRawMediaPaths(text, account.config, oapiToken, log, target);
749
749
  log.info(`[DingTalk][deliver] 裸露文件路径处理完成`);
750
750
  }
@@ -2184,7 +2184,7 @@ async function handleDingTalkMessageInternal(params) {
2184
2184
  finalText = await processVideoMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2185
2185
  finalText = await processAudioMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2186
2186
  finalText = await uploadAndReplaceFileMarkers(finalText, "", config, oapiToken, log, true, mediaTarget);
2187
- const { processRawMediaPaths } = await import("./media-ClfWY5Cw.mjs");
2187
+ const { processRawMediaPaths } = await import("./media-BqpUGCni.mjs");
2188
2188
  finalText = await processRawMediaPaths(finalText, config, oapiToken, log, mediaTarget);
2189
2189
  }
2190
2190
  const textToSend = finalText.trim() || "✅ 任务执行完成(无文本输出)";
@@ -1,4 +1,4 @@
1
- import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-_qA1Qjt_.mjs";
1
+ import { c as logMediaIdTrace, d as processLocalImages, u as processImagesForOutbound, y as uploadMediaToDingTalk } from "./media-DnQB2BrL.mjs";
2
2
  import { n as createLoggerFromConfig } from "./logger-mZ9OSbmD.mjs";
3
3
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
4
4
  import { i as getOapiAccessToken, r as getAccessToken, t as DINGTALK_API } from "./utils-CIfI_3Jh.mjs";
@@ -848,7 +848,7 @@ async function resolveMediaSourceToLocalFile(mediaUrl, mediaLocalRoots, log) {
848
848
  cleanup: tmp
849
849
  };
850
850
  }
851
- const { toLocalPath } = await import("./media-ClfWY5Cw.mjs");
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
- if (oapiToken && text) text = (await processImagesForOutbound(text, oapiToken, log)).text;
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-ClfWY5Cw.mjs");
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-ClfWY5Cw.mjs");
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-_qA1Qjt_.mjs";
1
+ import { b as __exportAll } from "./media-DnQB2BrL.mjs";
2
2
  import { a as resolveDingtalkAccount, c as addWildcardAllowFrom, d as hasConfiguredSecretInput, f as normalizeAccountId, l as createDefaultChannelRuntimeState, m as resolveDefaultGroupPolicy, o as resolveDingtalkCredentials, p as resolveAllowlistProviderRuntimeGroupPolicy, r as resolveDefaultDingtalkAccountId, s as DEFAULT_ACCOUNT_ID, t as listDingtalkAccountIds, u as formatDocsLink } from "./accounts-BAzdqkAV.mjs";
3
3
  import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
4
4
  import { t as dingtalkHttp } from "./http-client-DFWZgO1n.mjs";
5
5
  import "./utils-CIfI_3Jh.mjs";
6
- import { S as finalClean, f as getActiveCardForConversation, n as sendMediaToDingTalk, o as sendTextToDingTalk } from "./messaging-CpOPEOI8.mjs";
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-BKmyuozN.mjs"),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeik/dingtalk-connector",
3
- "version": "0.8.21-fix45",
3
+ "version": "0.8.21-fix47",
4
4
  "description": "OpenClaw DingTalk channel plugin (fork) | 钉钉 OpenClaw 渠道插件(fork 自官方,含标记/卡片渲染修复)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -28,18 +28,17 @@ export const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
28
28
  */
29
29
  export const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
30
30
 
31
- /** 是否为需上传的本地图引用 */
31
+ /** 是否为需上传的本地图引用(与 media.ts 权威实现一致:file:// 必须算本地) */
32
32
  export function isLocalImageRef(rawPath: string): boolean {
33
33
  const p = (rawPath || "").trim();
34
34
  if (!p) return false;
35
- if (/^https?:\/\//i.test(p)) return false;
36
- if (p.includes("://") && !p.startsWith("file://") && !p.startsWith("MEDIA:") && !p.startsWith("attachment://")) {
37
- return false;
38
- }
39
- if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
35
+ // file:// 等本地 scheme 优先(勿被 includes("://") 挡掉)
40
36
  if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) {
41
37
  return true;
42
38
  }
39
+ if (/^https?:\/\//i.test(p)) return false;
40
+ if (p.includes("://")) return false;
41
+ if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
43
42
  if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
44
43
  if (IMAGE_EXTENSIONS.test(p.split("?")[0] || p)) return true;
45
44
  return !p.includes("://");
@@ -45,11 +45,26 @@ export const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
45
45
  */
46
46
  export const LOCAL_IMAGE_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
47
47
 
48
- /** 是否像远程 URL 或其截断片段(禁止当本地路径上传) */
48
+ /** 本地文件 URI / OpenClaw 媒体前缀(不是 http 远程) */
49
+ function isLocalFileScheme(p: string): boolean {
50
+ return (
51
+ p.startsWith("file://") ||
52
+ p.startsWith("MEDIA:") ||
53
+ p.startsWith("attachment://")
54
+ );
55
+ }
56
+
57
+ /**
58
+ * 是否像远程 URL(禁止当本地路径上传)。
59
+ * **不含** file:// / MEDIA: / attachment://(那些是本地引用)。
60
+ */
49
61
  export function looksLikeRemoteUrl(raw: string): boolean {
50
62
  const p = (raw || "").trim();
51
63
  if (!p) return false;
64
+ // 本地 scheme 优先:否则 file:// 会被 includes("://") 误判成远程 → fetch 失败 → 灰图
65
+ if (isLocalFileScheme(p)) return false;
52
66
  if (/^https?:\/\//i.test(p)) return true;
67
+ // 伪协议截断如 p://(Windows 路径被当 URL 的历史坑)
53
68
  if (/^[a-z]:\/\//i.test(p) && !/^[a-z]:[\\/][^/\\]/i.test(p)) return true;
54
69
  if (p.includes("://")) return true;
55
70
  return false;
@@ -156,12 +171,11 @@ export function protectNonImageOccurrencesOfTargets(
156
171
  export function isLocalImageRef(rawPath: string): boolean {
157
172
  const p = (rawPath || "").trim();
158
173
  if (!p) return false;
174
+ // file:// / MEDIA: / attachment:// 一律本地(必须先于 looksLikeRemoteUrl)
175
+ if (isLocalFileScheme(p)) return true;
159
176
  // 已是远程 URL 或钉钉 mediaId(@ 开头)→ 不上传
160
177
  if (looksLikeRemoteUrl(p) || /^https?:\/\//i.test(p)) return false;
161
178
  if (p.startsWith("@") && !p.includes("/") && !p.includes("\\")) return false;
162
- if (p.startsWith("file://") || p.startsWith("MEDIA:") || p.startsWith("attachment://")) {
163
- return true;
164
- }
165
179
  // Unix 绝对路径 / Windows 盘符
166
180
  if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) {
167
181
  // 有图片后缀,或路径中明显是文件(含扩展名)
@@ -211,8 +225,9 @@ export interface UploadResult {
211
225
  }
212
226
 
213
227
  /**
214
- * 本地图诊断:默认只 console 一行,不再 log.warn(避免 [sendTextToDingTalk] 双前缀刷屏)。
215
- * verbose 步骤(文件可读/realpath 等)仅 debug 时通过 log 输出。
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
- * 精简 MediaId 追踪:默认只在关键节点 / 异常时打一行。
237
- * force=true 或发现「裸 mediaId」时才输出。
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 inMd = new Set(
248
- [...text.matchAll(LOCAL_IMAGE_RE)].map((m) => (m[2] || "").trim()),
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
- if (!force && suspiciousBare.length === 0 && !stage.includes("API前") && !stage.includes("完成")) {
255
- return;
256
- }
257
- const preview = text.replace(/\n/g, "\\n").slice(0, 160);
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
- (ids.length ? ` mediaIds=${ids.length}` : "") +
261
- (suspiciousBare.length ? ` ⚠️BARE=${suspiciousBare.join(",")}` : "") +
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
- ` | ${preview}`,
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
- logLocalImage(log, `${tag} 开始`, `path=${absPath}`, true);
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}`, true);
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`, true);
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
- if (!fs.existsSync(absPath)) {
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
- if (!oapiToken || !content || typeof content !== "string") {
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)) continue;
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
- result = result
575
- .split(fullMatch)
576
- .join(`![${alt || path.basename(absPath)}](${uploadResult.mediaId})`);
700
+ const replacement = `![${alt || path.basename(absPath)}](${uploadResult.mediaId})`;
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) continue;
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(`![${alt || "image"}](${uploadResult.mediaId})`);
604
745
  uploadCount++;
605
- console.log(
606
- `[DingTalk][LocalImage] ![]远程→mediaId 下载链保留原URL | ${uploadResult.mediaId}`,
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
- console.log(
629
- `[DingTalk][LocalImage] processImagesForOutbound 完成 | uploads=${uploadCount} outLen=${result.length}`,
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
- logMediaIdTrace("processImagesForOutbound:out", result);
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,
@@ -1,2 +0,0 @@
1
- import { t as registerGatewayMethods } from "./gateway-methods-CCh-LutW.mjs";
2
- export { registerGatewayMethods };