fix(chat): show_image 本地图片路径统一走 /api/file/content,修复裸相对路径静默不显示
- normalizeShowImageSrc 重写:http(s) 与 /user_upload/ 原样保留,其余本地路径 统一映射为 /api/file/content?path=...(与 show_file/view_image 同一通道) - docker/web 模式:支持工作区内任意相对路径(含 ./、/ 前缀归一化); host 模式:另支持任意绝对路径,file:// 前缀剥离后按本地路径处理 - invalid-src 不再静默吞掉:渲染可见错误占位(不支持的图片路径 xxx) - 提示词同步澄清:本地图片写工作区相对路径,host 模式可用绝对路径
This commit is contained in:
parent
d3622ce438
commit
80284f1732
@ -54,7 +54,7 @@
|
|||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
|
|||||||
@ -226,7 +226,7 @@ id: ask_fse_001
|
|||||||
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
- 一次展示优先只输出一个完整标签块(开标签 + 内容 + 闭标签),不要拆段输出
|
||||||
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
- 若还需要解释说明,把说明写在标签之外,且不要再次写标签字面量
|
||||||
|
|
||||||
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`,支持本地路径或网络链接。禁止使用 Markdown 图片语法。
|
如需在界面直接展示图片卡片,使用 `<show_image src="路径" alt="描述" />`。本地图片写工作区相对路径(如 `output/chart.png`;host/宿主机模式也可用任意绝对路径),网络图片写 http(s) 链接。禁止使用 Markdown 图片语法。
|
||||||
|
|
||||||
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
如需在界面直接渲染 HTML 卡片,使用 `<show_html ratio="比例" js="off|on">...</show_html>`。
|
||||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||||
|
|||||||
@ -143,20 +143,61 @@ function collectShowImageOrder(root: ParentNode | null = document) {
|
|||||||
return tokens.slice(0, 200);
|
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) {
|
function normalizeShowImageSrc(src: string) {
|
||||||
if (!src) return '';
|
if (!src) return '';
|
||||||
const trimmed = src.trim();
|
let trimmed = src.trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
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;
|
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
||||||
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
||||||
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
// /workspace 前缀是容器内绝对路径写法,剥掉后按工作区相对路径处理
|
||||||
return trimmed;
|
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) {
|
function parsePixelSize(raw: string | null, fallback: number) {
|
||||||
@ -555,6 +596,17 @@ function renderShowImages(root: ParentNode | null = document) {
|
|||||||
const rawSrc = node.getAttribute('src') || '';
|
const rawSrc = node.getAttribute('src') || '';
|
||||||
const mappedSrc = normalizeShowImageSrc(rawSrc);
|
const mappedSrc = normalizeShowImageSrc(rawSrc);
|
||||||
if (!mappedSrc) {
|
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', '1');
|
||||||
node.setAttribute('data-rendered-error', 'invalid-src');
|
node.setAttribute('data-rendered-error', 'invalid-src');
|
||||||
if (verbose) debugShowImageLog('render:invalid-src', { renderId, rawSrc });
|
if (verbose) debugShowImageLog('render:invalid-src', { renderId, rawSrc });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user