http://192.168.31.76:8680(会话「敦煌飞天 Qwen Image 2.1 出图」)2026-09-24-ZACP-Grok-WebUI-unknown-session-id诊断与修复.md(同一个服务的前一个问题)图片不是"加载失败",而是压根没请求到图片:agent(grok)在回复里写的是 工作区相对路径 ,而 ZACP 的聊天渲染 没有把这类相对路径转换成文件直链——浏览器于是按同源相对 URL 去请求:
请求 http://127.0.0.1:8680/sessions/comfyuir%E4%BB%BB%E5%8A%A1/feitian_1_00001_.png
返回 200 text/html(SPA fallback = index.html,4360 字节)
浏览器拿到一坨 HTML 当图片解 → 经典断裂图。
ZACP 自己是有文件直链能力的(GET /api/v1/workspaces/:id/files/raw?path=…&token=…), 而且文件浏览器的图片预览就在用它——只是聊天渲染这条路径没接上。 本次补上这一环:把消息里 markdown 图片的工作区相对路径改写成直链。
说明:这不是上一次「unknown session id」修复引入的。官方 v0.9.0(同一 commit
9360fc6) 的前端源码里同样没有任何聊天图片路径改写逻辑,属于上游既有缺口。
$ node check-chat-images.cjs http://127.0.0.1:8680/sessions/3
{ "total": 20, "brokenCount": 9..., "items": [
{ "src": "comfyuir任务/dunhuang_apsara_v2_00001_.png", "naturalWidth": 0 }, ← 断裂
{ "src": "comfyuir任务/feitian_1_00001_.png", "naturalWidth": 0 },
...
网络面板里对每一张图的请求都是:
| 请求 URL | 状态 | Content-Type |
|---|---|---|
/sessions/comfyuir%E4%BB%BB%E5%8A%A1/feitian_1_00001_.png | 200 | text/html ← 关键 |
同时确认后端与文件都没问题(20 个路径逐个实测):
comfyuir任务/feitian_1_00001_.png raw=200:image/png onDisk:2223645
...(20/20 全部 200 image/png,文件都在 /home/zyw/Pictures/Wallpapers/comfyuir任务/)
src/components/chat/MessageItem.vue 用 IncremarkContent 渲染 markdown, 全文没有任何 <img src> 处理(grep 全 src 目录:聊天组件里没有 img/src 改写代码)。/sessions/:id,于是拼成 /sessions/comfyuir任务/xxx.png → 命中 SPA fallback → index.html。GET /api/v1/workspaces/:id/files/raw?path=…(<img> 直链,支持 ?token= 资源 token)POST /api/v1/workspaces/:id/files/preview-token(换 12 小时有效的直链,绑定 workspace+path)components/files/FileExplorer.vue 的图片预览就是用 fetchPreviewUrl() 拿直链的。api/index.ts 里,聊天路径从未调用。frontend/src/composables/useWorkspaceImageUrls.tsisWorkspaceRelativeImageSrc():严格判定才改写——带协议(http:/data:/blob:…)、 以 / / // / # 开头的一律不动(保护外链与内联数据)。resolveWorkspaceImageUrl():优先用 fetchPreviewUrl() 换 资源 token 直链 (认证开启后 <img> 也能用,token 与登录态分离),换取失败退化为无 token 直链; 按 workspaceId + path 做 Promise 缓存,同图只请求一次。rewriteWorkspaceImages(root, workspaceId):扫描 root 下所有 <img>, 把相对路径替换成直链。幂等(dataset 打标记),失败静默(宁可维持原状也不抛错)。frontend/src/components/chat/MessageItem.vueref="rootRef"(作为扫描范围)。workspaceId = sessionStore.activeSession?.workspaceId——图片路径是相对会话工作区根的, 沿用它才能拼出正确直链。onMounted 先扫一次(历史消息)+ MutationObserver(childList + subtree + src 属性变化)跟随流式渲染新增的 <img>;watch(message.content, workspaceId) 兜底 (工作区 id 晚于消息到达时补扫);onBeforeUnmount 断开 observer。副作用说明:图片元素插入的瞬间浏览器会先用旧相对地址发一次请求(必然是 HTML), 随后被改写为直链再发一次。多一次无用小请求,但换来"无需预处理 markdown"的简单实现。
$ node check-chat-images.cjs http://127.0.0.1:8680/sessions/3
{
"total": 20,
"rewritten": 20, ← 20 张全部被改写为 /files/raw 直链
"rewrittenLoaded": 20, ← 20 张 naturalWidth > 0(真的解码成功)
"rewrittenFailed": [],
"stillRelative": [], ← 没有漏网的相对路径
"errors": [] ← 无 JS 报错
}
网络面板对应变成:
/api/v1/workspaces/1/files/raw?path=comfyuir%E4%BB%BB%E5%8A%A1%2Ffeitian_1_00001_.png&token=... → 200 image/png
测试注意:第一次测量只显示 11/20,是因为消息列表按需分页 + 图片 loading=lazy, 未被请求的图自然 naturalWidth=0,看起来像失败。验证脚本已改成 「上下滚动加载全部历史 + 强制 loading=eager + 等所有图 settle」再统计, 避免这种假阴性(脚本见附件)。
现在一共两个补丁,都已放在本目录,重建脚本会按文件名顺序自动全部应用:
| 补丁 | 修的问题 |
|---|---|
grok-unknown-session-fix.patch | 「离开较久再发消息」报 unknown session id(后端 Go,6 行) |
chat-image-workspace-url.patch | 聊天里 markdown 图片不显示(前端 Vue,2 文件 / +139 行) |
bash rebuild-zacp-grokfix.sh # 默认 v0.9.0
VERSION=0.9.1 bash rebuild-zacp-grokfix.sh # 升级后改版本号重跑
脚本本次已完整验证:全新克隆 → 两个补丁依次应用 → 前端 bun + 后端 go 构建 → 回归测试 → 原子替换二进制 → pm2 重启 → 健康探测(冷启动约 8s,脚本最多等 40s)。
回滚:
ln -sfn $HOME/.zacp/bin/zacp-0.9.0 $HOME/.local/bin/zacp && pm2 restart zacp
| 文件 | 说明 |
|---|---|
chat-image-workspace-url.patch | 前端补丁(新增 composable + MessageItem 接线) |
check-chat-images.cjs | 真实浏览器验证脚本(Playwright,统计改写数/加载数/漏网相对路径) |
rebuild-zacp-grokfix.sh | 一键重建(自动应用本目录所有 *.patch) |
grok-unknown-session-fix.patch | 上一次的会话失效补丁 |
验证脚本依赖本机已缓存的 Playwright(
~/Downloads/.tools/node_modules/playwright-core
~/.cache/ms-playwright),无需联网安装。
用户反馈「grok 生成的图片再次无法显示」。实际只有 1 张(会话 #3 的消息 #45), 且不是渲染层的问题:
| 检查 | 结果 |
|---|---|
| markdown 引用 |  |
images/1.jpg 在哪 | ~/.grok/sessions/%2F…Wallpapers/01a0d2df-…/images/1.jpg ← grok 的会话私有目录,webui 访问不到 |
| 工作区里的真实文件 | comfyuir任务/feitian_grok_00001.jpg(447848 B,与私有目录里那份字节数相同,是 cp 出来的副本) |
| raw 端点 | images/1.jpg → 404;comfyuir任务/feitian_grok_00001.jpg → 200 image/jpeg |
根因:grok 的图片工具把成品写到自己的会话目录 $GROK_HOME/sessions/<cwd>/<session-id>/images/1.jpg (同名文件、每张覆盖上一张),然后 grok 在 markdown 里直接引用了这个内部路径。 它虽然已经 cp 了一份到工作区,但引用的是内部路径。grok 自己的思考里写得很直白:
{"type":"agent_thought","text":"Present the image using images/1.jpg as the markdown path.
Also mention the copy in comfyuir任务. Don't describe appearance."}
渲染层按「工作区根」解析 images/1.jpg → 文件不存在 → 404 → 断裂图。 这一层无法在渲染端补救:私有目录里的文件同名覆盖,把它映射出来只会显示"最新那张", 与消息当时的图对不上。
~/.grok/rules/image-paths.md(grok 的 global 规则目录, 不依赖目录信任),要求内嵌图片必须引用工作目录下的真实路径,禁止引用 $GROK_HOME/sessions/.../images/。 验证:cd /home/zyw/Pictures/Wallpapers && grok inspect → Project Instructions (1) └ /home/zyw/.grok/rules/image-paths.md (global, ~209 tokens)。events 里的 agent_message 文本(见 composables/useMessageBlocks.ts 的 deriveBlocks),不是 messages.content 列 —— 所以只改 content 页面依旧断图,必须同时改 events。 → ; 只替换 agent_message 文本,cp "…/images/1.jpg" … 的命令原文保持不动。 出现次数校验为 1 才执行(防误改);原文备份 message45-backup.json。pm2 restart zacp(规则在 agent 启动时加载;会话经 session/load 自动恢复,用户无感)。真实 Chromium 复查会话 #3:21/21 图片改写且加载成功,0 断裂,0 JS 报错。
AGENTS.md / $GROK_HOME/rules/*.md)是这类"行为纠正"的正规落点, 且 global 规则不需要目录信任;grok inspect 可验证是否真的加载。messages.content 会漏掉渲染真正用的 events(本次就是先改了 content、 页面照旧断裂,才发现渲染源是 events)。