download-gemini-images
Download, export, save, or package images from a Google Gemini conversation/chat/app page, especially uploaded images or generated image previews visible in a Gemini thread. Use when the task needs logged-in Chrome/Gemini state, opening Gemini image lightboxes, downloading the larger displayed image files, renaming them in order, and producing a ZIP archive.
1How It Works & Mechanism
The core prerequisite is borrowing the user's already logged-in Chrome rather than launching a standalone browser: Gemini conversations typically require an existing Google login state, so incognito or freshly opened browsers are ruled out in the very first step.
1. Use the Chrome plugin, not a fresh unauthenticated browser, because Gemini conversations usually require the user's existing Google session.
The script lists all open tabs via the host's browser capability, matches them by URL or dual conditions (gemini.google.com + title containing 'Gemini'), and attaches via user.claimTab; if no match is found, it throws an error immediately and never opens a new page on its own.
return await browser.user.claimTab(matches[0]);
The lightbox must be opened to fetch large images: Gemini exposes larger blob: images only in preview mode, so the script clicks buttons one by one and polls until img.currentSrc becomes a new blob: with both naturalWidth and naturalHeight greater than 0.
info.src.startsWith("blob:") &&
Images are renamed sequentially after being downloaded to disk: A UUID regex first extracts the identifier from the end of the blob URL to locate the newly downloaded file in Downloads by uuid+extension (or latest mtime where mtime >= startTime - 2s), and then copyFile renames it to a two-digit sequential name like image_01.jpg.
const outputName = `image_${pad2(index + 1)}${normalizeExtension(downloadedPath)}`;
Packaging is handled by a standalone bash script with built-in dual verification: It first tests integrity with zip -T, then compares the count of image_NN in the directory against the entry count inside the archive, exiting with a non-zero code upon mismatch.
echo "zip image count mismatch: directory=$image_count zip=$zip_count" >&2
Two anti-desynchronization guardrails exist in the loop: Each iteration presses ESC first to close any lingering lightbox, and re-counts buttons every time, throwing immediately if the count diverges from the initial count—preventing asynchronous page loading from making the N-th button point to a different image.
throw new Error(`Gemini image button count changed from ${initialCount} to ${currentCount}`);
The fallback path uses the host tab's pageAssets capability to filter images by URL prefix and package them, explicitly acknowledging quality degradation and requiring that the user be notified.
var uploaded = inventory.assets.filter(a => a.kind === "image" && a.url.includes("lh3.googleusercontent.com/gg/"));
Every run leaves a manifest.json recording page title, page URL, output directory, and each image's index, original download path, and pixel dimensions, making deliverables auditable.
const manifestPath = path.join(outputDir, "manifest.json");
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| 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 关灯箱) |
4Recommended Governance & Evidence
- scripts/: Local script or command execution: download-gemini-images/scripts/download_gemini_images.mjs (in host node_repl J
- 网络端点: Declared external network access: gemini.google.com (drives user browser to visit/remain on this site; script only performs URL matching, actual requests are dispatched by user browser)
- 本地工作目录: File I/O and artifact output: Chrome downloads directory (default ~/Downloads): intermediate files <uuid>.jpg/.png/.webp written by browser itself (script reads only, does not delete)
5Second-Pass Verification
- [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。
6Verdict & Scenarios
2c75fce87072dc44…