From 80284f173264dbd97c781e613e9721ed52fd00cd Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 23 Aug 2026 17:12:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(chat):=20show=5Fimage=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=B7=AF=E5=BE=84=E7=BB=9F=E4=B8=80=E8=B5=B0?= =?UTF-8?q?=20/api/file/content=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=A3=B8?= =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E8=B7=AF=E5=BE=84=E9=9D=99=E9=BB=98=E4=B8=8D?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - normalizeShowImageSrc 重写:http(s) 与 /user_upload/ 原样保留,其余本地路径 统一映射为 /api/file/content?path=...(与 show_file/view_image 同一通道) - docker/web 模式:支持工作区内任意相对路径(含 ./、/ 前缀归一化); host 模式:另支持任意绝对路径,file:// 前缀剥离后按本地路径处理 - invalid-src 不再静默吞掉:渲染可见错误占位(不支持的图片路径 xxx) - 提示词同步澄清:本地图片写工作区相对路径,host 模式可用绝对路径 --- prompts/main_system.txt | 2 +- prompts/main_system_vl.txt | 2 +- prompts/multi_agent/master.txt | 2 +- static/src/app/bootstrap.ts | 60 +++++++++++++++++++++++++++++++--- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/prompts/main_system.txt b/prompts/main_system.txt index 994d3518..79e9c5ad 100644 --- a/prompts/main_system.txt +++ b/prompts/main_system.txt @@ -54,7 +54,7 @@ - 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出 - 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量 -如需在界面直接展示图片卡片,使用 ``,支持本地路径或网络链接。禁止使用 Markdown 图片语法。 +如需在界面直接展示图片卡片,使用 ``。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。 如需在界面直接渲染 HTML 卡片,使用 `...`。 - `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4` diff --git a/prompts/main_system_vl.txt b/prompts/main_system_vl.txt index 794b74e5..e993871f 100644 --- a/prompts/main_system_vl.txt +++ b/prompts/main_system_vl.txt @@ -54,7 +54,7 @@ - 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出 - 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量 -如需在界面直接展示图片卡片,使用 ``,支持本地路径或网络链接。禁止使用 Markdown 图片语法。 +如需在界面直接展示图片卡片,使用 ``。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。 如需在界面直接渲染 HTML 卡片,使用 `...`。 - `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4` diff --git a/prompts/multi_agent/master.txt b/prompts/multi_agent/master.txt index e5846dfc..34c7dfab 100644 --- a/prompts/multi_agent/master.txt +++ b/prompts/multi_agent/master.txt @@ -226,7 +226,7 @@ id: ask_fse_001 - 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出 - 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量 -如需在界面直接展示图片卡片,使用 ``,支持本地路径或网络链接。禁止使用 Markdown 图片语法。 +如需在界面直接展示图片卡片,使用 ``。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。 如需在界面直接渲染 HTML 卡片,使用 `...`。 - `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4` diff --git a/static/src/app/bootstrap.ts b/static/src/app/bootstrap.ts index 537df08b..76fa264a 100644 --- a/static/src/app/bootstrap.ts +++ b/static/src/app/bootstrap.ts @@ -143,20 +143,61 @@ function collectShowImageOrder(root: ParentNode | null = document) { return tokens.slice(0, 200); } +/* host/docker 模式缓存读取:键名与 app/state.ts 的 WORKSPACE_MODE_STORAGE_KEY 一致。 + 模式是部署级属性几乎不变,socket 连接后会校正回写 localStorage,这里直接读缓存即可。 */ +function getShowImageWorkspaceMode(): 'host' | 'docker' { + try { + return window.localStorage.getItem('agents_workspace_mode') === 'host' ? 'host' : 'docker'; + } catch { + return 'docker'; + } +} + +/** + * 归一化 show_image 的 src: + * - http(s) 网络链接、/user_upload/ 上传文件:原样使用; + * - 其余一律视为本地文件路径,统一走 /api/file/content 通道(inline 预览,MIME 白名单由服务端控制): + * - docker/web 模式:仅工作区内相对路径,越界(../、项目外绝对路径)由后端 _validate_path 拒绝; + * - host 模式:工作区相对路径或任意绝对路径均可(后端 _validate_path host 分支全放行)。 + * 无法识别时返回空串,由调用方渲染可见的错误占位(不再静默吞掉)。 + */ function normalizeShowImageSrc(src: string) { if (!src) return ''; - const trimmed = src.trim(); + let trimmed = src.trim(); + if (!trimmed) return ''; if (/^https?:\/\//i.test(trimmed)) return trimmed; + // file:// 前缀剥离按本地路径处理(浏览器禁止 http 页面直接加载 file:// 资源) + if (/^file:\/\//i.test(trimmed)) { + trimmed = trimmed.replace(/^file:\/\//i, ''); + // Windows file:///C:/... → C:/... + trimmed = trimmed.replace(/^\/(?=[A-Za-z]:\/)/, ''); + } if (trimmed.startsWith('/user_upload/')) return trimmed; // 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx const idx = trimmed.toLowerCase().indexOf('/user_upload/'); if (idx >= 0) { return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length); } - if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) { - return trimmed; + // /workspace 前缀是容器内绝对路径写法,剥掉后按工作区相对路径处理 + if (trimmed === '/workspace') return ''; + if (trimmed.startsWith('/workspace/')) { + trimmed = trimmed.slice('/workspace/'.length); } - return ''; + // 前导 ./ 语义等同工作区相对路径,剥掉保持整洁 + if (trimmed.startsWith('./')) { + trimmed = trimmed.slice(2); + } + if (!trimmed) return ''; + if (trimmed.startsWith('/')) { + if (getShowImageWorkspaceMode() === 'host') { + // host 模式保留绝对路径语义(如 /Users/xxx/img.png),后端全放行 + return `/api/file/content?path=${encodeURIComponent(trimmed)}`; + } + // docker 模式下前导 / 视为工作区根相对路径(真绝对路径会被后端拒绝) + trimmed = trimmed.replace(/^\/+/, ''); + if (!trimmed) return ''; + } + return `/api/file/content?path=${encodeURIComponent(trimmed)}`; } function parsePixelSize(raw: string | null, fallback: number) { @@ -555,6 +596,17 @@ function renderShowImages(root: ParentNode | null = document) { const rawSrc = node.getAttribute('src') || ''; const mappedSrc = normalizeShowImageSrc(rawSrc); if (!mappedSrc) { + // 不再静默吞掉:渲染可见错误占位,方便定位路径问题 + const figure = document.createElement('figure'); + figure.className = + 'chat-inline-card chat-inline-card--image chat-inline-image chat-inline-card--error chat-inline-image--error'; + const tip = document.createElement('div'); + tip.className = 'chat-inline-card__error chat-inline-image__error'; + tip.textContent = rawSrc + ? `无法显示图片:不支持的图片路径 ${rawSrc}` + : '无法显示图片:缺少图片路径'; + figure.appendChild(tip); + node.replaceChildren(figure); node.setAttribute('data-rendered', '1'); node.setAttribute('data-rendered-error', 'invalid-src'); if (verbose) debugShowImageLog('render:invalid-src', { renderId, rawSrc });