feat: multi-pass AI research with draft approval and hero image generation#173
Conversation
62038b2 to
5bedba7
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces a multi-pass AI research system and hero image generation using Gemini. Key additions include a new research-v2 endpoint with draft approval, image generation capabilities, and a jitter utility for scheduled jobs to prevent bot detection. Feedback focuses on improving the robustness of file extension handling for generated images, refactoring duplicated API key retrieval logic, and enhancing security by moving API keys from URL parameters to request headers while adding necessary fetch timeouts.
| } | ||
|
|
||
| try { | ||
| const imageBuffer = Buffer.from(imageData, 'base64'); |
There was a problem hiding this comment.
The logic for determining the file extension based on the MIME type is incomplete. Gemini models can return images in image/webp format, which would currently default to a .png extension. This can lead to file type mismatches in storage or when serving the asset.
| const imageBuffer = Buffer.from(imageData, 'base64'); | |
| const extension = mimeType === 'image/jpeg' ? 'jpg' : (mimeType === 'image/webp' ? 'webp' : 'png'); |
| let apiKey = process.env.GEMINI_API_KEY; | ||
| if (!apiKey) { | ||
| const apiKeyQuery = await pool.query( | ||
| "SELECT value FROM admin_settings WHERE key = 'gemini_api_key'" | ||
| ); | ||
| if (!apiKeyQuery.rows.length || !apiKeyQuery.rows[0].value) { | ||
| throw new Error('Gemini API key not configured.'); | ||
| } | ||
| apiKey = apiKeyQuery.rows[0].value; | ||
| } |
There was a problem hiding this comment.
|
|
||
| // Use REST API directly for image generation (SDK may not support image output modality) | ||
| // Fix: use x-goog-api-key header instead of URL query param to avoid key leakage in logs (PR #173 review) | ||
| const modelName = GEMINI_IMAGE_MODEL; |
There was a problem hiding this comment.
Passing the API key as a query parameter in the URL is less secure than using a request header, as URLs are frequently logged by proxies and servers. Additionally, the fetch call lacks a timeout, which could cause the request to hang indefinitely if the API is unresponsive.
const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': apiKey
},
signal: controller.signal,7c6f2f4 to
c3e0b9a
Compare
…ation (#102, #99) Replace single-pass Gemini research with two-pass workflow (Pass 1: metadata + brief description, Pass 2: historical description) with URL-first grounding, admin context input, and draft approval step. Add 1800s-style hero image auto-generation via Gemini image model that runs concurrently after research completes. Includes robust JSON parser for truncated Gemini responses and editable draft fields with per-field accept/reject. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
c3e0b9a to
0fb3191
Compare
Documents the withJitter() anti-bot detection feature that was implemented and merged in PR #173 (commit 0fb3191). Spec written after implementation to maintain documentation completeness per constitution requirements. Feature adds random 1-60 second delays to cron-scheduled jobs (news collection, trail status, moderation sweep, backups) to avoid predictable timing patterns that trigger bot detection. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Summary
research_contextdatabase column with migration/ai/research-v2,/ai/generate-hero-image,/ai/accept-hero-imageCloses #102
Closes #99
Test plan
🤖 Generated with Claude Code