这个插件解决什么问题
豆包 AI 生图页面在展示和下载图片时,有些场景会使用带水印的图片地址。但在页面接口数据里,同一张图片有时也会出现已经签名的原图地址。
这个插件的思路不是做图像修复,也不是用算法擦掉图片上的水印,而是在页面运行时优先使用接口里已经返回的原图 URL。它只在 https://www.doubao.com/* 生效,不上传图片,也不请求第三方服务。
这个边界很重要:扩展做的是本地页面 URL 替换,不是重新生成图片,也不是绕过一个外部图片处理服务。
一开始最容易误判的地方
看到图片 URL 里有 watermark 模板后缀时,很容易以为把这段路径删掉就能得到原图。
实际不行。豆包图片资源使用的是带签名的 ByteImage URL,x-signature 和完整路径绑定。如果把路径里的水印模板直接删掉,原来的签名就不再匹配,请求会返回 403。
所以这个插件不能靠猜 URL。正确做法是从页面接口返回值里收集真实存在、已经签好名的 image_raw 原图地址,再用它替换同一张图片对应的水印地址。
核心映射关系
接口里同一张图片通常会出现多个模板版本。它们的主体文件名相同,例如都属于同一个 rc_gen_image/<filename>,但后面的模板不同:
image_raw表示原图模板。image_pre_watermark更偏页面预览。image_dld_watermark更偏下载。downsize_watermark、hcg_watermark、img_pre_mark也是需要识别的水印变体。
插件用图片主体文件名作为 key,建立这样的映射:
rc_gen_image/<filename> -> 已签名的 image_raw URL
后续只要看到同一个 key 的水印 URL,就可以替换成之前记录下来的原图 URL。
为什么逻辑要运行在页面上下文
这个项目里 Content Script 只负责尽早注入 pageScript.js,真正的拦截逻辑运行在网页上下文中。
原因是浏览器扩展的 Content Script 和页面自己的 JavaScript 是隔离的。如果要改写页面里的 JSON.parse、fetch、XMLHttpRequest.open,逻辑必须进入页面上下文,否则只能看到一部分 DOM,拿不到最早的接口数据。
插件在页面里做了几件事:
- patch
JSON.parse,在接口 JSON 被页面消费前扫描图片字段。 - patch
fetch和XMLHttpRequest.open,尽量覆盖不同请求路径。 - 递归遍历接口对象,先收集原图 URL,再替换水印 URL。
- 观察 DOM,把已经渲染出来或后续新增的
img.src、a.href同步清理。
这里的关键不是只拦截网络,也不是只扫 DOM,而是两者都做。接口数据能拿到更完整的原图信息,DOM 扫描则负责补上已经显示出来的内容。
开关状态要跨上下文同步
扩展默认启用,但用户应该能随时关闭。开关状态保存在 browser.storage.local,popup 修改状态后,Content Script 会把状态同步给页面脚本。
这个设计里有一个细节:关闭时不能只是把变量设成 false,还要恢复被 patch 的原生方法,停止 DOM 监听,避免页面一直被扩展影响。重新开启时再安装 hooks,并给出简短状态提示。
这种可恢复的设计比“刷新页面后生效”更友好,也更容易排查问题。
这个方案的风险
这个插件依赖豆包页面内部接口字段和图片 URL 模板。只要页面改版、接口字段调整、模板命名变化,识别规则就可能失效。
所以代码里把原图模板、水印模板、图片 key 提取规则集中处理。未来维护时可以先抓包确认新的 URL 规律,再更新规则,而不是在整个项目里到处找字符串。
另外,浏览器扩展运行在第三方页面上,也要控制权限范围。当前只声明豆包域名和必要的存储权限,减少无关页面的影响面。
做完后的经验
这个插件的核心经验是:先确认数据来源,再写替换逻辑。
如果没有先弄清楚签名 URL 的规则,很容易把时间花在“清理 URL 字符串”上,最后得到一堆 403。真正稳定的做法是尊重页面已经拿到的数据,只替换成接口中真实存在的 signed URL。
另一个经验是,把浏览器扩展当成一个运行在复杂宿主页面里的小系统。它需要安装、卸载、同步状态、处理页面动态更新,还要尽量在目标网站变化时保留可维护性。
这个项目看起来是在处理图片 URL,实际更像是在练习:如何在不控制宿主页面的情况下,把一个小功能做得足够克制、可关闭、可维护。