Skip to content

打通 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 数组,遇到图片会自动下载保存,然后把引用以文本形式拼接到消息里。

支持的图片来源

来源格式处理方式
URLimage_url.url = "https://..."httpGet 下载,保存到 /tmp/openclaw-images/
base64image_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):

javascript
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):

javascript
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,最后合并:

javascript
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 的请求:

bash
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 日志中确认图片已下载:

bash
grep "image" ~/.openclaw/cursor-proxy.log | tail -5
Extracted 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

用本地图片测试

bash
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 测试

bash
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 编码,需要额外的鉴权下载步骤。

最后更新于:

Hosted by GitHub Pages