Compare commits

..

4 Commits

Author SHA1 Message Date
Jim Liu 宝玉 aa1a967a9f chore: release v1.114.1 2026-05-08 17:45:03 -05:00
Jim Liu 宝玉 d643bad53c test(baoyu-danger-gemini-web): cover generated image response fallback 2026-05-08 17:43:31 -05:00
Jim Liu 宝玉 0d977787b5 Merge pull request #146 from evilstar2016/fix/gemini-generated-image-detection
fix(gemini-webapi): add fallback scan for generated images
2026-05-08 17:40:45 -05:00
evilstar2016 516803feb4 fix(gemini-webapi): add fallback scan for generated images when wants_generated fails
The `wants_generated` detection checks `candidate[12][7][0]` and an old
`googleusercontent.com/image_generation_content/` URL pattern in the response
text. Both are no longer present in the current Gemini Web API response format,
causing the entire generated-image extraction block to be skipped even when
Gemini successfully generates images — resulting in "No image returned in
response" errors.

Generated image URLs now appear as `https://lh3.googleusercontent.com/gg-dl/`
somewhere in the response parts. This commit adds an unconditional fallback that
scans all response parts for those URLs when `generated_images` is still empty
after the existing `wants_generated` block, reusing the already-present
`collect_strings` helper and `GeneratedImage` constructor.

The existing code path is untouched — the fallback only runs when no images
were found through the original logic, so old response formats continue to work.
2026-05-06 18:48:08 +09:00
5 changed files with 106 additions and 23 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.114.0"
"version": "1.114.1"
},
"plugins": [
{
+5
View File
@@ -2,6 +2,11 @@
English | [中文](./CHANGELOG.zh.md)
## 1.114.1 - 2026-05-08
### Fixes
- `baoyu-danger-gemini-web`: restore generated-image extraction for current Gemini Web responses where image URLs appear as `https://lh3.googleusercontent.com/gg-dl/` without the legacy generated-image markers. Adds regression coverage for the fallback response shape. (by @evilstar2016)
## 1.114.0 - 2026-05-05
### Features
+5
View File
@@ -2,6 +2,11 @@
[English](./CHANGELOG.md) | 中文
## 1.114.1 - 2026-05-08
### 修复
- `baoyu-danger-gemini-web`:修复当前 Gemini Web 响应中生成图 URL 以 `https://lh3.googleusercontent.com/gg-dl/` 形式出现、但不再包含旧版生成图 marker 时的图片提取失败问题。补充该响应形态的回归测试。 (by @evilstar2016)
## 1.114.0 - 2026-05-05
### 新功能
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import test from "node:test";
import { collect_generated_image_urls_from_response_parts } from "./client.ts";
test("response part fallback finds generated images when legacy generated markers are absent", () => {
const generatedUrl = "https://lh3.googleusercontent.com/gg-dl/example-generated-image";
const initialCandidate = ["rcid-1", ["image generated successfully"]];
const imageCandidate = [
"rcid-1",
["image generated successfully"],
{ nestedPayload: [{ media: generatedUrl }] },
];
const responseJson = [
["wrb.fr", null, JSON.stringify([null, [], null, null, [initialCandidate]])],
["wrb.fr", null, JSON.stringify([null, [], null, null, [imageCandidate]])],
];
assert.equal(initialCandidate[12], undefined);
assert.equal(
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(String(initialCandidate[1]?.[0])),
false,
);
assert.deepEqual(collect_generated_image_urls_from_response_parts(responseJson, 0, 0), [generatedUrl]);
});
@@ -37,6 +37,8 @@ type InitOptions = {
type RequestKwargs = RequestInit & { timeout_ms?: number };
const GENERATED_IMAGE_URL_PREFIX = 'https://lh3.googleusercontent.com/gg-dl/';
function normalize_headers(h?: HeadersInit): Record<string, string> {
if (!h) return {};
if (Array.isArray(h)) return Object.fromEntries(h.map(([k, v]) => [k, v]));
@@ -78,6 +80,59 @@ function collect_strings(root: unknown, accept: (s: string) => boolean, limit: n
return out;
}
function collect_generated_image_urls(root: unknown, limit: number = 4): string[] {
return collect_strings(root, (s) => s.startsWith(GENERATED_IMAGE_URL_PREFIX), limit);
}
function parse_response_part_body(part: unknown): unknown[] | null {
if (!Array.isArray(part)) return null;
const part_body = get_nested_value<string | null>(part, [2], null);
if (!part_body) return null;
try {
const part_json = JSON.parse(part_body) as unknown;
return Array.isArray(part_json) ? part_json : null;
} catch {
return null;
}
}
function find_generated_image_part(
response_json: unknown[],
body_index: number,
candidate_index: number,
limit: number = 4,
): { body: unknown[]; urls: string[] } | null {
for (let part_index = body_index; part_index < response_json.length; part_index++) {
const part_json = parse_response_part_body(response_json[part_index]);
if (!part_json) continue;
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
if (!cand) continue;
const urls = collect_generated_image_urls(cand, limit);
if (urls.length > 0) return { body: part_json, urls };
}
return null;
}
export function collect_generated_image_urls_from_response_parts(
response_json: unknown[],
body_index: number,
candidate_index: number,
limit: number = 4,
): string[] {
return find_generated_image_part(response_json, body_index, candidate_index, limit)?.urls ?? [];
}
function push_generated_images(
generated_images: GeneratedImage[],
urls: string[],
proxy: string | null,
cookies: Record<string, string>,
): void {
for (const url of urls) {
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', proxy, cookies));
}
}
export class GeminiClient extends GemMixin {
public cookies: Record<string, string> = {};
public proxy: string | null = null;
@@ -404,24 +459,8 @@ export class GeminiClient extends GemMixin {
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text);
if (wants_generated) {
let img_body: unknown[] | null = null;
for (let part_index = body_index; part_index < (response_json as unknown[]).length; part_index++) {
const part = (response_json as unknown[])[part_index];
if (!Array.isArray(part)) continue;
const part_body = get_nested_value<string | null>(part, [2], null);
if (!part_body) continue;
try {
const part_json = JSON.parse(part_body) as unknown[];
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
if (!cand) continue;
const urls = collect_strings(cand, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 1);
if (urls.length > 0) {
img_body = part_json;
break;
}
} catch {}
}
const image_part = find_generated_image_part(response_json as unknown[], body_index, candidate_index, 1);
const img_body = image_part?.body ?? null;
if (!img_body) {
throw new ImageGenerationError(
@@ -452,13 +491,22 @@ export class GeminiClient extends GemMixin {
}
if (generated_images.length === 0) {
const urls = collect_strings(img_candidate, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 4);
for (const url of urls) {
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', this.proxy, this.cookies));
}
push_generated_images(generated_images, collect_generated_image_urls(img_candidate), this.proxy, this.cookies);
}
}
// Fallback: unconditionally scan all response parts for generated image URLs.
// The `wants_generated` detection above relies on `candidate[12][7][0]` and an old
// `googleusercontent.com/image_generation_content/` URL pattern, both of which no
// longer appear in the current Gemini Web API response format.
// When Gemini does generate images, their URLs now start with
// `https://lh3.googleusercontent.com/gg-dl/` and are present somewhere in the
// response parts — this fallback finds them so images are not silently dropped.
if (generated_images.length === 0) {
const urls = collect_generated_image_urls_from_response_parts(response_json as unknown[], body_index, candidate_index);
push_generated_images(generated_images, urls, this.proxy, this.cookies);
}
out.push(new Candidate({ rcid, text, thoughts, web_images, generated_images }));
}