让 Claw 看懂 WPS 协作里的图片
上一篇打通了 URL 和本地图片的分析链路——proxy 下载图片到本地,Cursor Agent 用 Read 工具看。但 WPS 协作的图片既不是 URL 也不是本地文件,而是一个 storage_key 编码标识,需要额外的鉴权步骤。
这篇在上一篇的基础上,增加两个东西:
- proxy 层自动解析
storage_key(被动:协作消息推送进来时自动处理) - wpsv7-skill 的
download-image命令(主动:Agent 拉取历史消息后手动下载)
前置条件:已完成 OpenClaw + WPS 协作接入。
WPS 图片的存储机制
WPS IM 的图片消息不直接给 URL,而是给一个 storage_key:
{
"type": "image",
"content": {
"image": {
"storage_key": "066F31F2cGljLzNhZDA0NmI1YzJm...",
"type": "image/png",
"width": 569,
"height": 472
}
}
}storage_key 是 base64 编码的内部存储标识,解码后包含 KS3 对象存储的 bucket 和 key。066F31F2 是 chat 图片的固定前缀。
下载需要两步鉴权:
storage_key ──MIME API──→ 签名 CDN URL ──GET──→ 图片文件MIME API:
GET https://365.kdocs.cn/woa/api/v4/mime/download?biz_source=chat&key=<storage_key>
Cookie: wps_sid=<sid>; csrf=<sid>
Referer: https://365.kdocs.cn/woa/im/messages返回带签名的临时 CDN 链接,直接 GET 即可下载。Cookie 和 Referer 缺一不可,否则 403。
被动路径:proxy 层自动解析
Agentspace 插件在收到图片消息时,把 storage_key 以固定格式拼入消息文本:
[Attached image: wps_storage_key:066F31F2cGljLzNhZDA0...]streaming-proxy 的 resolveWpsKeysInText 用正则匹配这个格式,自动调 MIME API 换取签名 URL,下载到本地,再替换为本地路径:
[Attached image: /tmp/openclaw-images/img-a1b2c3d4e5f6.png]之后就和上一篇一样了——Cursor Agent 用 Read 工具读取本地图片。
关键实现
proxy 里的 WPS 图片解析链:
// 正则匹配 storage_key 标记
const WPS_STORAGE_KEY_RE = /\[Attached image: wps_storage_key:([A-Za-z0-9+/=]+)\]/g;
// MIME API 换取签名 URL
async function resolveWpsStorageKey(storageKey) {
const sid = loadWpsSid();
const key = storageKey.startsWith("066F31F2") ? storageKey : `066F31F2${storageKey}`;
const apiUrl = `${WPS_MIME_API}?biz_source=chat&key=${encodeURIComponent(key)}`;
const json = await httpGetJson(apiUrl, {
cookie: `wps_sid=${sid}; csrf=${sid}`,
Referer: "https://365.kdocs.cn/woa/im/messages",
});
return json?.url || null;
}
// 下载并保存
async function saveWpsImage(storageKey) {
const signedUrl = await resolveWpsStorageKey(storageKey);
if (!signedUrl) return null;
return saveImageFromUrl(signedUrl); // 复用上一篇的通用下载函数
}loadWpsSid() 从 ~/.openclaw/openclaw.json 动态读取 wps_sid,不硬编码。
限制
被动路径依赖 Agentspace 插件的消息推送。但 Agentspace WebSocket 目前只推送文本消息,图片事件不会触发推送。所以被动路径的实际触发场景有限——主要是插件层面把 storage_key 拼入文本后转发时才会走到。
主动路径:download-image 命令
更可靠的方式是让 Agent 主动拉取历史消息,找到图片后用命令下载。
下载函数
在 wpsv7client/im.py 中添加:
def download_image(
storage_key: str,
output_path: Optional[str] = None,
client: Optional[WpsV7Client] = None,
) -> dict:
"""通过 MIME API 将 storage_key 解析为签名 CDN URL 后下载。"""
import hashlib
import json as _json
c = client or WpsV7Client()
if not c.sid:
raise ValueError("缺少用户凭证: 请设置环境变量 wps_sid 或 WPS_SID")
mime_base = "https://365.kdocs.cn"
key = storage_key if storage_key.startswith("066F31F2") else f"066F31F2{storage_key}"
mime_url = f"{mime_base}/woa/api/v4/mime/download?biz_source=chat&key={key}"
headers = {
"cookie": f"wps_sid={c.sid}; csrf={c.sid}",
"Referer": "https://365.kdocs.cn/woa/im/messages",
}
req = urllib.request.Request(mime_url, headers=headers)
with urllib.request.urlopen(req, timeout=15) as resp:
mime_resp = _json.loads(resp.read())
signed_url = mime_resp.get("url")
if not signed_url:
return {"code": -1, "msg": "MIME API 未返回 url", "raw": mime_resp}
if not output_path:
os.makedirs("/tmp/openclaw-images", exist_ok=True)
short_hash = hashlib.md5(storage_key.encode()).hexdigest()[:12]
output_path = f"/tmp/openclaw-images/wps-{short_hash}.jpg"
dl_req = urllib.request.Request(signed_url)
with urllib.request.urlopen(dl_req, timeout=30) as resp:
data = resp.read()
with open(output_path, "wb") as f:
f.write(data)
return {"url": signed_url, "output": output_path, "size": len(data)}命令行入口
在 skills/im/run.py 注册 download-image 子命令:
p = sub.add_parser("download-image", help="下载 IM 图片(通过 storage_key)")
p.add_argument("storage_key", nargs="?", default=None,
help="图片的 storage_key(base64 编码)")
p.add_argument("--output", "-o", default=None,
help="保存路径,默认 /tmp/openclaw-images/wps-<hash>.jpg")
p.set_defaults(func=cmd_download_image)SKILL.md:告诉 Agent 怎么用
在 skills/im/SKILL.md 加上醒目的说明:
## ⚠️ 图片下载(重要)
当历史消息中出现 `type=image` 的消息,其 `content.image.storage_key` 就是图片标识。
**必须用此命令下载,不要手动拼 URL:**
python skills/im/run.py download-image "<storage_key>"
**典型流程:** history 拉消息 → 找到 type=image → 取 storage_key → download-image 下载 → Read 查看"不要手动拼 URL"是关键——实测中 Agent 在没有明确指引时花了 44 次 tool call 试遍各种下载方式后宣布"图片无解"。
wps_sid 管理
不管是 proxy 还是 skill,都需要 wps_sid 鉴权。唯一真实来源是 ~/.openclaw/openclaw.json:
~/.openclaw/openclaw.json
└─ channels.agentspace.accounts.default.wps_sid
│
├──→ streaming-proxy.mjs (loadWpsSid() 动态读)
├──→ gateway-wrapper.sh (启动时注入环境变量)
└──→ wpsv7-skill/base.py (环境变量优先,fallback 读 json)Gateway 启动脚本
~/.local/bin/openclaw-gateway-wrapper.sh 每次启动时从配置读取 wps_sid,注入环境变量给 Agent 进程继承:
#!/bin/bash
CONFIG="$HOME/.openclaw/openclaw.json"
if [ -f "$CONFIG" ]; then
SID=$(python3 -c "
import json
print(json.load(open('$CONFIG'))
.get('channels',{})
.get('agentspace',{})
.get('accounts',{})
.get('default',{})
.get('wps_sid',''))" 2>/dev/null)
if [ -n "$SID" ]; then
export WPS_SID="$SID"
export wps_sid="$SID"
fi
fi
exec openclaw gatewaybase.py Fallback
即使环境变量不可用,skill 也能直接从配置文件读取:
class WpsV7Client:
def __init__(self, base_url=None, sid=None):
self.sid = (sid
or os.environ.get("wps_sid")
or os.environ.get("WPS_SID")
or self._load_sid_from_config())
@staticmethod
def _load_sid_from_config():
try:
cfg_path = os.path.join(os.path.expanduser("~"),
".openclaw", "openclaw.json")
with open(cfg_path) as f:
cfg = json.load(f)
return (cfg.get("channels", {})
.get("agentspace", {})
.get("accounts", {})
.get("default", {})
.get("wps_sid"))
except Exception:
return Nonewps_sid 过期后重新登录,重启 Gateway 即全部生效:
openclaw config # 重新 OAuth
systemctl --user restart openclaw-gateway验证
手动测试
cd ~/Documents/OpenClaw/skills/wpsv7-skill
# 找图片消息
python skills/im/run.py search-messages \
--chat-ids "<chat_id>" --msg-types "image" --page-size 1
# 下载
python skills/im/run.py download-image "066F31F2cGljLzNhZDA0NmI1YzJm..."协作端测试
在 WPS 协作里对 Claw 说:
帮我看看这个群里最新的图片是什么内容
Agent 的正确行为链:history → 找 type=image → download-image → Read → 回复。
日志确认:
grep -E "history|download-image" ~/.openclaw/cursor-proxy.log | tail -10如果 Agent 在尝试各种 curl 而不是 download-image,说明 SKILL.md 没生效——清 session 后重试:
rm -f ~/.openclaw/cursor-sessions.json
systemctl --user restart openclaw-gateway踩过的坑
| 现象 | 根因 |
|---|---|
| Agent 花 44 次 tool call 后说"图片无解" | SKILL.md 缺图片下载说明 |
| 更新 SKILL.md 后 Agent 还说"看不了" | 没删 ~/.openclaw/cursor-sessions.json,复用了老 session |
| Agent 调 skill 超时 300s | wps_sid 不在环境变量里,base.py 也没 fallback |
| MIME API 返回 403 | 缺 Cookie 或 Referer |
| storage_key 前缀重复导致下载失败 | V7 API 返回的 key 已含 066F31F2,代码要判断去重 |
| 用户发图 Agent 无反应 | Agentspace 不推送图片事件,需要文字触发 Agent 主动拉取 |