fix(hub): strip card markdown images + prefer inline ![] over send_file

Feishu interactive markdown rejects ![](http...) without image_key
(error 230099 empty/missing imagekey). Always mask residual image md in
card builders; skip img tags with empty keys; skip inline-code examples;
fetch remote images with a browser UA and without env HTTP_PROXY.
Steer the agent: use ![alt](workspace-path) for 图文, send_file only for
downloadable attachments.

Release v0.0.37.
This commit is contained in:
2026-07-20 11:38:55 +00:00
parent 73cb0e5b47
commit 93f3f2424c
7 changed files with 71 additions and 16 deletions
+12 -5
View File
@@ -12,7 +12,7 @@
*/
import type { ToolUseTraceStep } from "./trace-store.js";
import type { CardContentSegment } from "../outboundImages.js";
import { maskMarkdownImagesForStreaming, type CardContentSegment } from "../outboundImages.js";
// ---------------------------------------------------------------------------
// Types
@@ -411,6 +411,7 @@ function buildAnswerElements(
let remaining = MAX_TEXT_LENGTH;
for (const segment of contentSegments) {
if (segment.type === "image") {
if (segment.imgKey.trim() === "") continue;
elements.push({
tag: "img",
img_key: segment.imgKey,
@@ -421,9 +422,13 @@ function buildAnswerElements(
continue;
}
if (segment.content === "" || remaining <= 0) continue;
const slice = segment.content.length <= remaining
? segment.content
: truncateText(segment.content, remaining);
// Feishu card markdown rejects ![](url) without a Feishu image_key
// ("card contains images but no imagekey" / empty image key).
const safe = maskMarkdownImagesForStreaming(segment.content);
if (safe === "" || remaining <= 0) continue;
const slice = safe.length <= remaining
? safe
: truncateText(safe, remaining);
remaining -= slice.length;
elements.push({
tag: "markdown",
@@ -433,9 +438,11 @@ function buildAnswerElements(
return elements;
}
if (text === "") return [];
const safe = maskMarkdownImagesForStreaming(text);
if (safe === "") return [];
return [{
tag: "markdown",
content: truncateText(text, MAX_TEXT_LENGTH),
content: truncateText(safe, MAX_TEXT_LENGTH),
}];
}