让 agent 自己去搜图
一、起因:图和话,从来不在一个地方
我写文章要配图,做 PPT 要背景图,做界面要参考图。以前的流程是固定的五步:
- 打开浏览器,在百度或 Bing 里敲关键词;
- 一张张翻,右键,另存为;
- 到下载目录里改名,从“微信截图 2026-08-31 (3).jpg”改成看得懂的名字;
- 拖进项目的
images/目录; - 回到会话里,把路径告诉 agent,它才能读。
问题不在哪一步难,而在这五步必须由我亲手做完。agent 能读图、能改图、能把图排进 HTML,唯独“去网上找一张图”这一段断了,因为它在浏览器外面。
有一次我为了三张“秋天街道”的图,折腾了二十分钟。图存下来了,名字是乱的,其中一张后来发现是 400×300 的小图,放大就糊。那一刻我想,这件事应该由 agent 自己干:我把需求说清楚,它去找、去下载、去校验尺寸,最后把图摆在对话里让我挑。
这个插件就是那件事。
二、它其实只做三件事
三个工具,边界切得很清楚:
| 工具 | 干什么 | 关键参数 |
|---|---|---|
image_hunt | 搜 + 下 + 显示,一条龙 | query 必填;count 默认 8(1–40);engine 默认 auto;dir 默认 images |
image_search | 只搜,不落盘 | 无 count、无 dir,结果截前 20 条 |
image_download | 已知直链,批量下载 | urls 1–30 条;prefix 默认 img-;referer 可选 |
我平时只用第一个。剩下两个是我在调试时拆出来的:想知道“到底哪个源返回了什么”就用 image_search,想把某个网页上的图搬下来就用 image_download。
第一个工具还带了几个只对特定源生效的参数——minPixels 卡最短边、noAI 只要非 AI 作品(仅 Pixiv)、collection 和 color 只对 Iconify 图标库有意义。参数不生效的时候它不报错,只是忽略,这比报错更符合实际使用习惯。
三、14 个源,7 个别名
engine 字段能填 21 种值:14 个真实源,加 7 个别名。
先说源,都是我一个个试出来能用的:
| 源 | 类型 | 一次实测的候选数 | 特点 |
|---|---|---|---|
| 百度图片 | 综合 · 中文 | 60 | 中文语义最强,返回的 JSON 最脏 |
| Bing 图片 | 综合 · 国际 | 70 | 原图多,国际站不通会自动退 cn |
| 360 图片 | 综合 · 中文 | 259 | 量大速度快,质量参差 |
| 堆糖 | 插画 · 头像 | 47 | 手账、壁纸、可爱风 |
| 花瓣 | 设计 · 灵感 | 40 | 设计师采集,成套图多 |
| DuckDuckGo | 国外 · 聚合 | 99 | 走代理,聚合 Bing 与 Google 结果 |
| Yandex | 国外 · 原图 | 60 | Pinterest 那类高清原图多 |
| Openverse | CC 授权库 | 60 | 免 key,偏可商用 |
| Wikimedia | 公有领域 | 40 | 素材、地图、纪实照片 |
| Pixiv | 二次元 | 82 | 需要 Referer,有 AI 标记 |
| Wallhaven | 二次元壁纸 | 48–96 | 分辨率高,标签规整 |
| Konachan | 二次元 | 44 | 老牌 booru |
| Safebooru | 二次元 | 100 | 全年龄向 booru |
| Iconify | 图标 | 40 | 输出 SVG,可改色 |
表里的候选数是某一次实测的数字,不是保证值。同一个词今天搜和明天搜,结果条数会变。
再说别名,别名解决的是“我不想记 14 个名字”:
| 别名 | 展开成什么 |
|---|---|
auto | 百度 + 360 + Bing,日常最稳的一档 |
all | 13 个照片源全开,一次能捞 270–371 条 |
both | 百度 + 必应 |
intl | DuckDuckGo + Yandex + Bing |
cc | Openverse + Wikimedia,找能商用的图 |
anime | Pixiv + Wallhaven + Konachan |
moe | anime 再加 Safebooru |
三种写法都行:单个源 id、一个别名、或者 sources 数组直接覆盖 engine。数组写法适合“我就要这两家的图”这种明确需求,比如 sources: ["huaban", "duitang"]。
四、一次 image_hunt 的完整路径
一句话进来,走完六步:
- 查源注册表。
engine和sources归一成一个源列表,每个源带一个foreign标记,决定待会儿走不走代理。 - 并行抓取。每个源一个 Promise,谁也不等谁。单源超时 20–35 秒,一个源挂了只往
errors里记一条,其它源照常返回——这条是整个插件可用性的地基,早先写成串行的时候,一个慢源能把整次搜索拖死。 - 轮转合并。如果按源顺序拼接,百度 60 条会把后面的源全挤到第 60 名之后。改成按源轮转交错,第一名来自百度、第二名来自 Bing、第三名来自 360,翻两页就能看到各家风格。
- 去重与过滤。按原图直链去重,再按
minPixels卡最短边,不合格的条目单独记进skipped,不吞掉。 - 下载校验。单张上限
MAX_BYTES = 40 MB,超了跳过;格式不靠后缀,靠文件头。 - 落盘 + 登记。写进
<dir>/<关键词>/,同时把每一张登记成会话附件(attachment),于是图会直接出现在对话里。
第 6 步的“登记”是这个插件和普通下载脚本最大的区别。它调用 ctx.attachments.saveImage,把文件变成当前会话可寻址的附件——不是给一个磁盘路径让我自己去翻。
五、代理只给国外源
国内源直连,国外源走本机代理。策略看着简单,写起来有三个坑:
坑一:自己造的 socket 会被默认 Agent 忽略。 隧道连好之后,我把 socket 塞进 http.request 的 createConnection,以为请求会顺着走,结果它绕开隧道直连去了。原因是 Node 的隐式默认 Agent 不认你传进去的 socket。最后是 new Agent({ keepAlive: false, maxSockets: 1 }) 覆写 createConnection 才收住。
坑二:CONNECT 的响应里会带 bytes。 代理回的 200 Connection Established 后面可能紧跟第一段 TLS 握手字节。要一直累积到 \r\n\r\n 才算读完响应头,剩下的字节 unshift 回 socket,否则握手失败,报错还特别难懂。
坑三:IPv6 空耗。 直连兜底时强制 IPv4。不强制的话,只有 AAAA 记录的域名会先等一轮超时再回退,一次搜索白等十秒。
代理地址的解析顺序是 IMAGE_HUNT_PROXY → HTTPS_PROXY → HTTP_PROXY,默认 http://127.0.0.1:7890;写 off、none、0、false 都是关掉代理。代理真不通的时候,熔断 60 秒,然后回退直连——大概率也是失败,但会把 code 和 addrs 记进 proxyErrors(只留最近 20 条),让我知道是超时还是被拒。
六、五个真实的坑
① 百度的 JSON 有两处“非法”。 字符串里会出现裸的 TAB 控制字符,还会把单引号转义成 \'——两种都不是合法 JSON。我的处理是三级降级:先原样 JSON.parse;失败就剥掉 [\u0000-\u001f] 控制字符再试;再失败就把 \' 修回 '。三级都不行,走 baiduRows() 的正则兜底,一行一行扒字段。
② booru 只认 ASCII tag。 搜“宫崎骏”喂给 Konachan,结果稳定为空。内置了一张 TAG_MAP:宫崎骏、吉卜力 → ghibli,龙猫 → totoro,千与千寻 → spirited_away,魔女宅急便 → kiki,初音未来 → hatsune_miku。翻译不过来就退化成原词,让上游返回空——比瞎猜一个 tag 老实。
③ Pixiv 不给 Referer 就 403。 原图在 i.pximg.net,下载必须带 Referer: https://www.pixiv.net/。另外 ai_type 这个参数在服务端不生效,客户端自己按 aiType(1 = 非 AI,2 = AI)过滤。
④ 后缀会撒谎。 Safebooru 的 file_url 常以 .jpg 结尾,实际文件是 PNG。sniffExt 按字节头判断:\xFF\xD8\xFF 是 JPEG,\x89PNG 是 PNG,RIFF....WEBP 是 WebP。按后缀存,后面拿去做缩略图会炸。
⑤ 尺寸也会撒谎。 Yandex 报 2844×1600,文件打开是 2730×1536;Wikimedia 报的是 2000×1333 的缩略图,实际原图 3000×2000。所以 imgSize 自己读头部取真实像素,接口给的尺寸另存成 metaWidth / metaHeight——保留,但不信。
七、落盘长什么样
1 | images/ |
meta.json 是数组,每一项对应一张图:
1 | { |
字段不多,但每一个都有来处:url 是原图直链,page 是来源页(想追溯就点这个),site 是中文站名,license 只在 Openverse、Wikimedia 这类有明确授权字段的源才有值,其余写 unknown——不猜授权。
输出的 files[].path 永远是工作区相对路径,safeJoin 把一切输出收口在会话 cwd 里,想往 ../ 写会直接抛错。好处是路径能直接喂给 read_image、拼进 HTML、或者交给 present 挂成文件卡片。
八、命令行也能跑
不想进会话的时候,有 cli.mjs:
1 | node cli.mjs "禾木 冬天" --n 4 --min-pixels 1200 --engine anime --noai \ |
参数和工具调用一一对应:--n 是 count,--min-pixels 是 minPixels,--noai 是 noAI,--out 是落盘目录。最后一行输出 JSON,方便管道接给别的脚本。package.json 里挂了 bin,所以全局装完可以直接 dsh-image-hunt "关键词"。
九、三种装法
1 | # 1. 从 npm |
装完必须重启 DSH。插件的工具是在宿主启动时注册的,热加载不到。
十、搜不到的那些,我也都试过
写插件最花时间的不是把能用的接上,是搞清楚哪些接不上、为什么。这几家我试完放弃了:
| 目标 | 实测结果 | 结论 |
|---|---|---|
| Google 图片 | tbm=isch 只回约 92 KB 的 JS 壳,AF_initDataCallback 与 encrypted-tbn 计数都是 0 | 要跑无头浏览器,不值 |
| 搜狗图片 | napi 直接返回 {"status":1,"info":"forbid"} | 接口关了 |
| X(Twitter) | 未登录只给壳页,登录要过风控 | 放弃 |
| 同上,且图片有 CDN 签名 | 放弃 |
这也是为什么源列表里国内三家、国外四家、二次元四家——能把公开接口用明白的,就这么多。
十一、边界与合规
说清楚它不做什么:
- 不绕登录、不绕付费墙、不破解任何站点。 所有请求都打在公开页面上,得到的也是公开页面能看到的东西。
- 不保证授权。
license字段只在源自己提供时才写,其余是unknown。要商用请回来源页确认,这个插件替不了你。 - 不保证可复现。 图源会变、会限流、会改接口。今天 60 条,明天可能 12 条。
- 不处理 NSFW 过滤。 四个二次元源里 Safebooru 是全年龄向,Pixiv 与 Konachan 需要自己判断;
noAI只是过滤 AI 作品,不是内容分级。
MIT 只覆盖这份代码。图片版权归原作者,下载下来的每一张都该当作“引用”,不是“拥有”。
写这个插件之前,我从来没认真想过“一张图是怎么被找到的”。写完才知道,中间隔着的不是一行 HTTP 请求,是一堆格式撒谎、接口返祖、代理绕路和编码错位。
它现在是我写文章时的固定流程:帮我找 3 张秋天的街道,要横图、最短边 1200 以上。剩下的它自己做,我只需要从对话里挑一张。

