打通 OpenClaw + Cursor 的图片分析链路
Cursor Agent CLI 天然支持多模态——Read 工具可以直接"看"本地图片文件。但 OpenClaw 作为中间层,收到的是 OpenAI 格式的消息,图片可能是 URL、base64、本地路径等各种形态。需要在消息到达 Cursor 之前,统一把图片转成本地文件。
这篇记录 streaming-proxy 的图片预处理机制,以及如何验证整条链路。
前置条件:已完成 OpenClaw + Cursor 接入。
Cursor Agent 怎么看图片
Cursor Agent CLI 的 Read 工具支持读取图片文件(jpg、png、gif、webp),读取后模型能直接理解图片内容。但它只认本地文件路径,不认 URL 也不认 base64。
所以问题变成:怎么把各种来源的图片变成本地文件路径。
streaming-proxy 的图片预处理
OpenClaw 到 Cursor Agent CLI 之间有一个 streaming-proxy.mjs 做协议转换。它在 extractUserMessage 函数里处理 OpenAI 格式的 messages 数组,遇到图片会自动下载保存,然后把引用以文本形式拼接到消息里。
支持的图片来源
| 来源 | 格式 | 处理方式 |
|---|---|---|
| URL | image_url.url = "https://..." | httpGet 下载,保存到 /tmp/openclaw-images/ |
| base64 | image_url.url = "data:image/png;base64,..." | 解码,保存到 /tmp/openclaw-images/ |
| 本地路径 | image_url.url = "/path/to/file" | 直接透传 |
处理后,所有图片引用统一变成文本:
[Attached image: /tmp/openclaw-images/img-a1b2c3d4e5f6.png]Cursor Agent 看到这个文本后,会用 Read 工具读取对应的本地文件,触发多模态分析。
核心流程
客户端发送 OpenAI 格式消息
│
│ messages: [{ role: "user", content: [
│ { type: "text", text: "这张图片是什么" },
│ { type: "image_url", image_url: { url: "https://..." } }
│ ]}]
│
▼
streaming-proxy extractUserMessage()
│
├─ text 部分 → 直接保留
│
├─ image_url (https://) → httpGet 下载 → 保存到本地
├─ image_url (data:image/) → base64 解码 → 保存到本地
├─ image_url (/path/...) → 直接用
│
▼
合并为纯文本 prompt:
"这张图片是什么
[Attached image: /tmp/openclaw-images/img-a1b2c3d4e5f6.png]"
│
▼
Cursor Agent CLI
│
├─ 看到 "[Attached image: ...]"
├─ 用 Read 工具读取图片
├─ 多模态模型分析
│
▼
回复图片描述关键实现
图片下载(saveImageFromUrl):
async function saveImageFromUrl(url) {
ensureImageDir();
const buf = await httpGet(url);
const hash = createHash("md5").update(buf).digest("hex").slice(0, 12);
const ext = extFromMime(null);
const filePath = join(IMAGE_DIR, `img-${hash}${ext}`);
writeFileSync(filePath, buf);
return filePath;
}httpGet 支持 HTTP/HTTPS 自动切换和 3xx 重定向跟随,超时 15 秒。文件名用内容 MD5 前 12 位,相同图片不会重复下载。
base64 解码(saveImageFromBase64):
function saveImageFromBase64(dataUri) {
ensureImageDir();
const match = dataUri.match(/^data:image\/(\w+);base64,(.+)$/);
if (!match) return null;
const ext = "." + match[1].replace("jpeg", "jpg");
const buf = Buffer.from(match[2], "base64");
const hash = createHash("md5").update(buf).digest("hex").slice(0, 12);
const filePath = join(IMAGE_DIR, `img-${hash}${ext}`);
writeFileSync(filePath, buf);
return filePath;
}消息提取(extractUserMessage)遍历 content 数组,分拣 text 和 image_url,最后合并:
let result = textParts.join("\n");
if (imagePaths.length) {
const refs = imagePaths.map((p) => `[Attached image: ${p}]`).join("\n");
result = result ? `${result}\n\n${refs}` : refs;
}所有图片统一写到 /tmp/openclaw-images/,这个目录由 proxy 自动创建。
验证
用 URL 图片测试
直接用 curl 模拟一个带图片 URL 的请求:
curl http://localhost:18888/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "cursor",
"stream": true,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "描述一下这张图片"},
{"type": "image_url", "image_url": {"url": "https://picsum.photos/400/300"}}
]
}]
}'在 proxy 日志中确认图片已下载:
grep "image" ~/.openclaw/cursor-proxy.log | tail -5Extracted 1 image(s): /tmp/openclaw-images/img-a1b2c3d4e5f6.png然后在 Cursor Agent 的 tool call 中应该能看到 Read 调用:
tool:start readToolCall args={"path":"/tmp/openclaw-images/img-a1b2c3d4e5f6.png"}
tool:done readToolCall ok=true用本地图片测试
curl http://localhost:18888/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "cursor",
"stream": true,
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "这张图片里有什么"},
{"type": "image_url", "image_url": {"url": "/home/user/photo.jpg"}}
]
}]
}'本地路径直接透传,不会触发下载。
通过 CLI 测试
openclaw agent --message "帮我看看这张图片 https://picsum.photos/400/300" --channel cli如果 Agent 回复了图片内容描述,说明整条链路是通的。
注意事项
| 现象 | 原因 |
|---|---|
| Agent 说"我无法查看图片" | 可能是老 session 认知残留,清 ~/.openclaw/cursor-sessions.json 后重启 |
| 图片下载超时 | httpGet 默认 15 秒超时,大图或慢网络可能不够 |
| 相同图片重复下载 | 不会——文件名用内容 MD5,相同内容对应同一文件 |
/tmp/openclaw-images/ 积累太多文件 | /tmp 由 systemd-tmpfiles 自动清理(Fedora 默认 10 天未访问) |
小结
这套机制的核心思路是:proxy 层负责把各种图片格式统一为本地文件路径,Cursor Agent 只需要会用 Read 工具读本地文件。两者各管各的,通过 [Attached image: /path] 这个简单约定连接。
下一篇在此基础上,处理 WPS 协作中的图片——WPS 的图片用 storage_key 编码,需要额外的鉴权下载步骤。