diff --git a/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/client.test.ts b/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/client.test.ts new file mode 100644 index 0000000..817c2b8 --- /dev/null +++ b/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/client.test.ts @@ -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]); +}); diff --git a/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/client.ts b/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/client.ts index a7afd4b..c308ae3 100644 --- a/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/client.ts +++ b/skills/baoyu-danger-gemini-web/scripts/gemini-webapi/client.ts @@ -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 { 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(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(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, +): void { + for (const url of urls) { + generated_images.push(new GeneratedImage(url, '[Generated Image]', '', proxy, cookies)); + } +} + export class GeminiClient extends GemMixin { public cookies: Record = {}; 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(part, [2], null); - if (!part_body) continue; - try { - const part_json = JSON.parse(part_body) as unknown[]; - const cand = get_nested_value(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,10 +491,7 @@ 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); } } @@ -467,22 +503,8 @@ export class GeminiClient extends GemMixin { // `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) { - 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(part, [2], null); - if (!part_body) continue; - try { - const part_json = JSON.parse(part_body) as unknown[]; - const cand = get_nested_value(part_json, [4, candidate_index], null); - if (!cand) continue; - const urls = collect_strings(cand, (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)); - } - if (generated_images.length > 0) break; - } catch {} - } + 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 }));