1实现原理 · 为什么它能做到
核心前提是「借用用户已登录的 Chrome」而不是自己开浏览器:Gemini 会话通常要现成的 Google 登录态,所以第一步就把无痕新浏览器排除掉。
1. Use the Chrome plugin, not a fresh unauthenticated browser, because Gemini conversations usually require the user's existing Google session.
脚本通过宿主的 browser 能力列出所有已开标签页,用 URL 或(gemini.google.com + 标题含 Gemini)双条件匹配,再 user.claimTab 认领;匹配不到直接报错,绝不自行新开页面。
return await browser.user.claimTab(matches[0]);
必须先点开 lightbox 才能拿到大图:Gemini 只在预览态暴露更大的 blob: 图,所以脚本逐张点按钮后轮询等待 img.currentSrc 变成新的 blob:、且 naturalWidth/naturalHeight 都大于 0。
info.src.startsWith("blob:") &&
下载落盘后再按序改名:先从 blob URL 尾部用 UUID 正则抽出标识,到 Downloads 目录按 uuid+扩展名(或最新 mtime 且 mtime>=开始时间-2s)定位刚下载的文件,再 copyFile 成 image_01.jpg 这种两位序号名。
const outputName = `image_${pad2(index + 1)}${normalizeExtension(downloadedPath)}`;
打包是一个独立 bash 脚本,且自带双重自证:先 zip -T 测完整性,再比对目录内 image_NN 数量与压缩包内条目数量,不等即非零退出。
echo "zip image count mismatch: directory=$image_count zip=$zip_count" >&2
循环体里有两道防错位护栏:每张先按 ESC 关掉可能的旧灯箱,且每次重新统计按钮数量,一旦与初始数量不符立刻抛错——避免页面异步加载后第 N 个按钮指向另一张图。
throw new Error(`Gemini image button count changed from ${initialCount} to ${currentCount}`);
降级路径走宿主标签页的 pageAssets 能力按 URL 前缀筛图并打包,且明确承认画质退化并要求告知用户。
var uploaded = inventory.assets.filter(a => a.kind === "image" && a.url.includes("lh3.googleusercontent.com/gg/"));
每次运行都留下 manifest.json 记录页标题、页 URL、输出目录与每张图的序号/原始下载路径/像素尺寸,交付物可回溯。
const manifestPath = path.join(outputDir, "manifest.json");
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| api | 宿主 Chrome 插件能力 browser(user.openTabs / user.claimTab) |
| api | 宿主 node_repl(homeDir、在同一 JS 会话里 import 脚本) |
| api | 宿主标签页 pageAssets 能力(降级路径) |
| network | gemini.google.com(默认标签页匹配串,实际流量由用户浏览器发出) |
| network | lh3.googleusercontent.com(降级路径按该 CDN 前缀筛图片资产) |
| cli | zip / zipinfo(打包与条目校验) |
| cli | find / wc / grep(图片计数与条目数比对) |
| api | 宿主 CUA 按键能力 tab.cua.keypress(发 ESC 关灯箱) |
4风险提醒 风险提醒:黄色 · 留意使用
- 高权限上下文:在用户已登录 Google 的浏览器里真实点击与下载。 — claimTab 借用登录态;误操作(点错按钮、页面结构变化)作用对象是用户自己的账号会话。SKILL.md 的边界只有一句『Do not inspect browser cookies...』,其对「不要在会话里顺手做别的事」的约束弱于 github-ops 那类显式 blast-radius 契约。
- 写盘位置固定且不清理:下载目录会堆积中间文件。 — 浏览器先落到 ~/Downloads/<uuid>.<ext>,脚本只 copyFile 不删除,SKILL.md 也未要求清理;多次运行会留下多份副本。ZIP 输出默认写在输出目录旁(<image_dir>.zip),写前 rm -f 同名文件。
- 页面结构强耦合:选择器是硬编码 aria-label。 — DEFAULT_BUTTON_SELECTOR = 'button[aria-label="Show the uploaded image in a lightbox"]';Gemini 改文案即整体失效。失效方式是可接受的(抛错退出),但意味着维护依赖上游 DOM。
- pageTitle/pageUrl 原文回流给模型。 — manifest.json 会存页面标题与 URL 并返回给调用方;Gemini 会话标题由对话内容决定,属弱注入面(模型若把标题当指令即中招),实际风险低。
- 依赖宿主实现细节,跨宿主不可移植。 — browser.user.openTabs/claimTab、tab.cua.keypress、tab.playwright.locator、pageAssets 都是宿主私有 API;换宿主需改写脚本。
5第二遍独立确认
- [ok] 外部依赖:宿主 browser 能力真实存在(不是推测) — mjs:124 return await browser.user.claimTab(matches[0]); 与 mjs:118 await browser.user.openTabs() 是真实调用点;SKILL.md 第 3 步要求 import 后在 node_repl 里跑,两处对得上。
- [ok] 外部依赖:zip / zipinfo / find / wc 调用点 — package_images.sh 内 zip -qr、zip -T、zipinfo -1、find ... -iname 'image_*.jpg'、wc -l 均实读命中,脚本是真的会跑这些二进制,不是文档口号。
- [ok] 网络端点数量:只有 Google 两个域名 — 全目录域名级检索仅 gemini.google.com 与 lh3.googleusercontent.com;无第三方 API、无 webhook、无 telemetry。注意 mjs 中出现的 blob: 是页面内对象 URL,不是网络端点。
- [discrepancy] 凭证面:是否真的不碰 cookie(找反例) — 〔第二遍标注:措辞需精确〕反例检索结果:源码中确实没有任何 document.cookie / chrome.cookies / localStorage 访问,SKILL.md 也明文禁止。但『不读凭证』不等于『不使用凭证态』——claimTab 借用的正是用户已登录会话。第一遍若把此条简单写成「无凭证接触」会掩盖真实权限面,故在 credential_reads 中改写为『无凭证文件读取,但运行于凭证态上下文』。
- [ok] 『绝不自开新浏览器』是否成立 — claimGeminiTab 在 matches.length === 0 时抛错(No open Gemini tab matched),downloadGeminiImagesFromChrome 中 options.tab 与 claimGeminiTab 二选一,无 puppeteer.launch 之类的建窗代码,与 SKILL.md『claim the already-open Gemini tab』一致。
- [ok] 输出完整性校验是否真做了(非文档承诺) — package_images.sh: zip -T 有实调用,且 image_count 与 zip_count 不等时 exit 1;mjs 侧有 initialCount 校验、按钮数不变量、selectedCount !== 1 抛错——三道都是代码而非描述。
- [ok] expectedCount 语义是否被夸大 — description 未提 expectedCount;SKILL.md 只说『Set `expectedCount` only when the user gave a specific count』,代码里 initialCount !== expectedCount 时抛错,与文档一致,无越界承诺。
- [ok] 元数据(license/stars/last_push) — 取自同 repo 同 commit 的池内既有值(与 auto-repo-setup.json / bilibili-source.json 一致:MIT、1385★、2026-09-09T12:33:29Z);本次未联网复核 gh。
6结论
2c75fce87072dc44…d5c4678cb5