[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-79988":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":16,"stars7d":17,"stars30d":18,"stars90d":15,"forks30d":15,"starsTrendScore":19,"compositeScore":20,"rankGlobal":10,"rankLanguage":10,"license":21,"archived":22,"fork":22,"defaultBranch":23,"hasWiki":24,"hasPages":22,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":26,"readmeContent":27,"aiSummary":28,"trendingCount":15,"starSnapshotCount":15,"syncStatus":29,"lastSyncTime":30,"discoverSource":31},79988,"gpt-image-linux","Z1rconium\u002Fgpt-image-linux","Z1rconium","Self-hosted web panel for GPT-compatible image generation APIs — generate, edit, and manage your images in one place.","https:\u002F\u002Fgpt-image-linux.onrender.com",null,"Python",80,18,72,0,1,4,9,3,48.74,"Other",false,"main",true,[],"2026-06-12 04:01:26","# GPT Image Panel\n\n![FastAPI](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FFastAPI-0.109-009688?logo=fastapi)\n![SvelteKit](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FSvelteKit-2-FF3E00?logo=svelte)\n![Python](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FPython-3.11+-3776AB?logo=python)\n![SQLite](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FSQLite-3-003B57?logo=sqlite)\n![Docker](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FDocker-24.0+-2496ED?logo=docker)\n\nWeb panel for GPT Image 2 API image generation and editing.\n\nEnglish | [中文](#中文文档)\n\n## Overview\n\nGPT Image Panel is a lightweight FastAPI web UI for image generation and image editing. It is designed as a self-hosted panel that connects to an external GPT-compatible image API and stores generated images locally.\n\n## Disclaimer\n\nThis project is only a self-hosted web visualization panel for initiating image generation and image editing requests. It does not provide, wrap, proxy, resell, or modify any upstream model or API service. All generation capability, content policy enforcement, account permissions, billing, and model behavior come from the upstream API provider configured by the user.\n\nThis project does not encourage or endorse generating pornographic, political, illegal, infringing, violent, hateful, or otherwise sensitive or policy-violating content. Users are solely responsible for their configured upstream service, prompts, uploaded images, generated results, storage, sharing, and any legal or platform-policy consequences. Any content generated through an upstream API is unrelated to this project and is not produced, reviewed, hosted, or approved by the project maintainers.\n\nKey characteristics:\n\n- SvelteKit + TypeScript frontend in `frontend\u002F`\n- FastAPI backend in `backend\u002Fapp\u002F`; use `backend.app.main:app` as the ASGI entrypoint\n- public API paths, methods, status codes, SSE event names, cookies, and response shapes are contract-tested and kept stable\n- API presets persisted to SQLite at `data\u002Fapp.sqlite3`\n- background generation\u002Fedit jobs executed with `asyncio.create_task`\n- local image storage under `images\u002F`\n- gallery metadata stored in SQLite at `data\u002Fapp.sqlite3`\n- Docker and Docker Compose deployment support with frontend static build baked into the image\n- pytest API contract tests under `backend\u002Ftests\u002F`\n\n## Features\n\n- API preset management: base URL\u002Fpath\u002Fkey, per-preset default model and response format, global SOCKS5 upstream proxy, and global Webhook URL\n- prompt helper tags, server-side prompt optimization, independent prompt snippets, and gallery prompt\u002Fparameter reuse\n- generation and image-editing (`\u002Fv1\u002Fimages\u002Fedits`) with size\u002Fquality\u002Fformat\u002Fcompression\u002Fquantity controls and up to 16 edit reference images\n- preview + job history with SSE progress, multi-image result previews, `completed_at`, elapsed time, per-job stage timings, loading states, detailed terminal statuses, an errors-only history filter, clear-all for persisted history, cancel for queued\u002Frunning jobs, and reuse\u002Fretry from persisted history\n- shared queue and concurrency limits for generation\u002Fedit jobs\n- optional global Webhook URL with HTTPS-only validation, SSRF checks, signing, retry, and masked settings responses\n- gallery with filters (FTS-backed prompt search, model, preset, size, date range, favorite), URL-synced page\u002Fnon-prompt-filter\u002Flightbox\u002Fjob-history state, direct page-number jump, lightbox previous\u002Fnext navigation and left\u002Fright keyboard shortcuts, “Edit this image”, download, custom delete confirmations with 5-second undo for single images, batch actions with partial-success feedback, delete\u002Fdelete-all, prompt\u002Fimage-url copy, and on-demand total-size metadata\n- prompt snippets drawer for reusable prompt templates, stored separately from gallery images in SQLite\n- ZIP export\u002Fimport (`metadata.json`) with streaming upload, safety validation, low-memory export path, skipped-entry metadata for partial batch downloads, and visible import\u002Fexport\u002Fdownload progress states\n- Cloudflare R2 gallery backup sync: configurable in `.env` or Web Settings, health-probed from Settings, and manually synced from Gallery without changing local gallery storage as the source of truth\n- access-key gate, Host\u002Fpublic-origin allowlist, IP allowlist\u002Fproxy-header support, GitHub version badge, and CSP nonce injection\n- observability hooks for job stage timings, slow `\u002Fapi\u002Fgallery` query logging, queue\u002Ffailure metrics, and optional JSON\u002FPrometheus metrics endpoints\n\n## Architecture\n\n### Backend\n\nThe backend is a FastAPI application under `backend\u002Fapp\u002F`.\n\nResponsibilities are split into a few modules:\n\n- `backend\u002Fapp\u002Fmain.py` — thin ASGI entrypoint\n- `backend\u002Fapp\u002Fapi\u002Fcontract_app.py` — frozen public API surface and current route wiring\n- `backend\u002Fapp\u002Fschemas\u002F` — Pydantic request\u002Fresponse DTOs\n- `backend\u002Fapp\u002Frepositories\u002F` — SQLite, gallery, image file, settings, and job persistence\n- `backend\u002Fapp\u002Fintegrations\u002F` — upstream GPT-compatible image API client\n- `backend\u002Fapp\u002Fcore\u002F` — settings, access tokens, IP allowlist, proxy headers, and URL validators\n- `backend\u002Fapp\u002Fservices\u002F` — webhook signing, retry, and async delivery\n\nWhen serving the static frontend, the backend injects a per-response script nonce into `frontend\u002Fbuild\u002Findex.html` and sends a matching Content Security Policy. Asset and index serving are covered by the backend contract tests.\n\n### Frontend\n\nThe frontend is a SvelteKit static application in `frontend\u002F`.\nThe backend serves only `frontend\u002Fbuild`; run a frontend build before starting the production backend.\n\nIt uses:\n\n- Tailwind CSS\n- `src\u002Flib\u002Fapi\u002Fclient.ts` for same-origin fetch calls to existing `\u002Fapi\u002F*` endpoints\n- `src\u002Flib\u002Fapi\u002Fevents.ts` for SSE wrappers\n- stores split across access, settings, prompt snippets, gallery, jobs, preview, and UI state\n- components for access, header, settings drawer, prompt snippets drawer, prompt helper, job history drawer, preview, gallery, lightbox, and size selection\n\nFrontend build:\n\n```bash\nnpm --prefix frontend install\nnpm --prefix frontend run build\n```\n\n### Storage\n\nRuntime persistent storage is minimal:\n\n- generated images are saved in the `images\u002F` directory\n- gallery metadata, image byte sizes, FTS prompt-search index, and API presets are stored in SQLite at `data\u002Fapp.sqlite3`, including `completed_at`, Beijing completion time, and generation duration\n- prompt snippets are stored in SQLite at `data\u002Fapp.sqlite3` independently from gallery metadata\n- optional R2 backups are incremental object uploads only; local `images\u002F` files and SQLite gallery rows remain the only gallery source used for serving, thumbnails, ZIP export, and import\n- generation\u002Fedit job status, errors, timing, `completed_at`, and result metadata are stored in SQLite at `data\u002Fapp.sqlite3`; successful multi-image jobs persist the full `images` result list while keeping the first result in `image_id`\u002F`image_url` for compatibility\n- active `asyncio.Task` handles live only in process memory; queued\u002Frunning jobs from a previous process are marked interrupted on startup\n\n### Generation flow\n\n1. frontend calls `\u002Fapi\u002Fgenerate`\n2. backend validates config, creates a SQLite-backed job, then schedules async execution\n3. shared queue\u002Fconcurrency limits are enforced; progress stages stream via SSE\n4. generation requests with `n > 1` stay as one public job, but count as `n` queue units and fan out internally through a global upstream-request semaphore; `\u002Fv1\u002Fimages\u002Fgenerations` child payloads always use `n=1`\n5. upstream image data is decoded\u002Fdownloaded, validated, and saved; gallery metadata is updated\n6. job history is queryable\u002Fstreamed (`\u002Fapi\u002Fgenerate\u002Fjobs*`), clearable (`DELETE \u002Fapi\u002Fgenerate\u002Fjobs\u002Fhistory`), cancellable (`DELETE \u002Fapi\u002Fgenerate\u002F{job_id}`), and can trigger optional signed webhook callbacks\n\n### Edit flow\n\n1. frontend selects source images (one or more uploads, optionally combined with one gallery image) and calls `\u002Fapi\u002Fedits` or `\u002Fapi\u002Fedits\u002Ffrom-gallery\u002F{image_id}`\n2. backend creates a job and calls upstream `\u002Fv1\u002Fimages\u002Fedits` using multipart form data\n3. source images plus supported edit params are forwarded; multiple references are sent upstream as repeated `image[]` fields, and unsupported source formats (for example SVG) are rejected\n4. progress stages stream via SSE; returned image data is decoded\u002Fdownloaded, validated, and saved\n5. edited results appear in preview\u002Fgallery and follow the same queue, history, and cancellation model as generation\n\n## Tech stack\n\n- Python 3.11+\n- FastAPI\n- Uvicorn\n- aiohttp\n- SQLite\n- Pydantic v2\n- SvelteKit\n- TypeScript\n- Tailwind CSS\n\n## Project structure\n\n```text\nLICENSE\nREADME.md\nDockerfile\ndocker-compose.yml\n.env.example\nVERSION\nrequirements.txt\npackage.json\nbackend\u002F\n  requirements-dev.txt\n  app\u002F\n    main.py\n    api\u002F\n    core\u002F\n    schemas\u002F\n    repositories\u002F\n    integrations\u002F\n    services\u002F\n  tests\u002F\nfrontend\u002F\n  package.json\n  svelte.config.js\n  vite.config.ts\n  src\u002F\n    routes\u002F\n    lib\u002F\n      api\u002F\n      stores\u002F\n      components\u002F\n      utils\u002F\ndeploy\u002F\n  nginx.conf\nimages\u002F\ndata\u002F\n```\n\n## Getting started\n\n### Prerequisites\n\nYou need one of the following:\n\n- Python 3.11 or newer\n- Docker\n- Docker Compose\n\nAn external GPT-compatible image API is required for actual image generation.\n\n### Docker\n\n```bash\ndocker build -t gpt-image-panel .\ndocker run -d --name gpt-image-panel \\\n  -p 127.0.0.1:9090:9090 \\\n  -v $(pwd)\u002Fimages:\u002Fapp\u002Fimages \\\n  -v $(pwd)\u002Fdata:\u002Fapp\u002Fdata \\\n  gpt-image-panel\n```\n\nIf Docker Hub times out while resolving `python:3.11-slim`, use a reachable mirror image:\n\n```bash\ndocker build \\\n  --build-arg PYTHON_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fpython:3.11-slim \\\n  --build-arg NODE_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fnode:24-alpine \\\n  -t gpt-image-panel .\n```\n\n### Docker Compose\n\n```bash\ncp .env.example .env\n# edit .env if needed\n# ACCESS_KEY is required by default unless ALLOW_UNAUTHENTICATED=true\ndocker-compose up -d --build --force-recreate\n```\n\nFor Docker Hub timeout issues with Compose, set this in `.env` before building:\n\n```bash\nPYTHON_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fpython:3.11-slim\nNODE_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fnode:24-alpine\n```\n\n### Local development\n\n```bash\npip install -r backend\u002Frequirements-dev.txt\nnpm --prefix frontend install\nnpm run backend:dev\n```\n\nIn another terminal:\n\n```bash\nnpm run frontend:dev\n```\n\nThen open `http:\u002F\u002Flocalhost:5173`. The Vite dev server proxies `\u002Fapi` and `\u002Fhealth` to FastAPI at `127.0.0.1:9090`, so browser requests stay same-origin in development.\nIt binds to `127.0.0.1` by default; for LAN\u002Fexternal debugging, run `npm run frontend:dev -- --host 0.0.0.0`.\n\nFor a single-process local smoke test, build the frontend first and run FastAPI:\n\n```bash\nnpm run frontend:build\nuvicorn backend.app.main:app --host 0.0.0.0 --port 9090 --reload\n```\n\nThen open `http:\u002F\u002Flocalhost:9090`.\n\nIf you want to run without access auth during local dev, set `ALLOW_UNAUTHENTICATED=true`.\n\n### Health check\n\n```bash\ncurl http:\u002F\u002Flocalhost:9090\u002Fhealth\n```\n\n## Usage\n\n1. open the site\n2. optionally use the top-left language switch to toggle English \u002F Simplified Chinese\n3. click the settings gear icon\n4. choose an existing preset or click New\n5. enter the API base URL\n6. choose the API path\n7. enter the preset default model; the Generate\u002FEdit form's Model field defaults to the active preset's value\n8. choose the preset default Response Format; the Generate\u002FEdit form's Response format field defaults to the active preset's value\n9. enter the API key, or an env ref such as `${OPENAI_API_KEY}`; literal keys require `ALLOW_PLAINTEXT_SECRETS=true`\n10. optionally enter a global SOCKS5 proxy or env ref such as `${UPSTREAM_PROXY_URL}`\n11. optionally enter a global Webhook URL or env ref such as `${WEBHOOK_URL}` for completed generation\u002Fedit jobs\n12. optionally configure R2 Backup with endpoint URL, bucket, prefix, and access key env refs such as `${R2_ACCESS_KEY_ID}` \u002F `${R2_SECRET_ACCESS_KEY}`, then click Test R2\n13. optionally configure Prompt Optimizer with an endpoint URL, model, timeout seconds, and API key\u002Fenv ref; literal keys require `ALLOW_PLAINTEXT_SECRETS=true`\n14. optionally click Edit System Prompt in the Prompt Optimizer settings to edit the optimizer system prompt stored at `DATA_DIR\u002Fprompt_optimizer_system_prompt.md`\n15. optionally run Health check for the saved preset\n16. click Save Preset\n17. enter a prompt\n18. click Prompt Helper tags to append common modifiers\n19. click Optimize to rewrite the prompt through the server-side optimizer\n20. open Prompts in the header to save or reuse prompt snippets; using a snippet replaces the current prompt\n21. choose generation options, including API path for per-request upstream routing\n22. click Generate\n23. optionally upload one or more edit reference images, pick \"Edit this image\" in Gallery\u002FLightbox, or combine both; uploads append to the current edit sources and Clear removes all edit sources\n24. click Edits to run image-to-image\n25. use Gallery\u002FLightbox \"Use prompt\" or \"Use all\" to reuse historical prompt text or full parameters\n26. view preview and gallery; click Gallery Sync to upload local gallery images missing from the configured R2 bucket prefix\n\n## API paths\n\nThe panel supports these upstream paths. The API base URL may either omit or include `\u002Fv1`; for example, both `https:\u002F\u002Fapi.example.com` and `https:\u002F\u002Fapi.example.com\u002Fv1` are accepted.\n\n### `\u002Fv1\u002Fimages\u002Fgenerations`\n\n- sends generation requests to the Images API\n- reads image data from `data[]`\n\n### `\u002Fv1\u002Fresponses`\n\n- sends generation requests to the Responses API\n- sends only `prompt` and `model` in the upstream request body; the UI model default comes from the active preset\n- reads base64 image data from `output[]` items of type `image_generation_call`\n- size, quality, format, compression, quantity, and response format controls are disabled in the UI for this path\n\n### `\u002Fv1\u002Fchat\u002Fcompletions`\n\n- sends OpenAI Chat Completions-compatible generation requests\n- sends only `model`, `messages`, and `stream: false` in the upstream request body\n- supports `grok-imagine-image-lite` and other image models that return image URLs or base64 data through chat completion messages\n- reads image output from JSON chat completions or `data:` SSE chunks, including Markdown image links such as `![image](https:\u002F\u002F...)`\n- size, quality, format, compression, quantity, and response format controls are disabled in the UI for this path\n\n### `\u002Fv1\u002Fimages\u002Fedits`\n\n- used by the Edits button after image upload(s), gallery-image selection, or both\n- always calls `\u002Fv1\u002Fimages\u002Fedits` on the configured API base URL\n- sends multipart\u002Fform-data with source image fields plus supported edit parameters; single uploads use `image`, while multiple references are forwarded upstream as repeated `image[]`\n- supports up to 16 edit reference images total; local uploads append to the current source list and can be combined with one gallery source\n- uploaded source files must be supported raster image formats; SVG uploads are rejected\n- if the upstream returns `404`, `405`, or `501`, the UI reports that `\u002Fv1\u002Fimages\u002Fedits` is not supported and stops the edit request\n\n## API preset health checks\n\n- `POST \u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}\u002Fhealth` validates the saved preset without sending a generation request\n- checks include API path allowability, HTTPS URL\u002Fhostname validation, upstream host allowlist and SSRF DNS\u002Fprivate-IP validation, API key\u002Fenv-ref presence, and a low-cost `OPTIONS`\u002F`HEAD` upstream probe\n- returned shape is `{ status, checks: [{ name, status, message }] }`, where each status is `ok`, `warning`, or `error`\n- API key env refs use the exact `${ENV_VAR_NAME}` form; the database stores the reference string and generation\u002Fedit calls resolve it from the server environment at request time\n- literal API keys are rejected by default; set `ALLOW_PLAINTEXT_SECRETS=true` only if you intentionally want plaintext SQLite storage\n\n## Upstream SOCKS5 proxy\n\n- The Settings drawer has one global `SOCKS5 proxy` field, independent of API presets.\n- Leave it empty for direct upstream API calls.\n- Use an env ref such as `${UPSTREAM_PROXY_URL}` by default. Literal values like `socks5:\u002F\u002Fhost:port` or `socks5:\u002F\u002Fuser:pass@host:port` require `ALLOW_PLAINTEXT_SECRETS=true`; stored proxy passwords are masked in API responses and the UI.\n- The proxy boundary is intentionally narrow: only generation\u002Fedit upstream API `POST` calls use it. Preset health checks, webhooks, version checks, frontend `\u002Fapi\u002F*` requests, and image URL downloads stay direct.\n\n## Global Webhook URL\n\n- The Settings drawer has one global `Webhook URL` field directly below `SOCKS5 proxy`; it is independent of API presets.\n- Leave it empty to disable job callbacks.\n- Use an env ref such as `${WEBHOOK_URL}` by default. Literal webhook URLs require `ALLOW_PLAINTEXT_SECRETS=true`.\n- When configured, completed generation\u002Fedit jobs send signed webhook callbacks to that HTTPS URL.\n- `WEBHOOK_SIGNING_SECRET` is required when a Webhook URL is configured. Stored webhook URLs are masked in API responses and the UI.\n\n## R2 gallery backup sync\n\n- R2 Backup is an incremental backup path, not remote gallery storage.\n- Configure it in `.env` with `R2_BACKUP_ENABLED=true` plus the other `R2_*` variables, or in Web Settings. When SQLite has no saved R2 settings yet, startup persists the current `.env` R2 values so they appear in Web Settings. Later Web Settings saves take precedence. Web Settings accepts `${ENV_VAR_NAME}` refs for credentials; literal stored credentials require `ALLOW_PLAINTEXT_SECRETS=true`.\n- Test R2 in Settings runs `HeadBucket`, a prefix-scoped `ListObjectsV2`, and a small probe object write; probe cleanup failure is reported as a warning.\n- The Gallery Sync button uploads only local gallery filenames missing from `R2_KEY_PREFIX`; existing bucket objects are skipped, and bucket-only objects are never deleted or overwritten.\n\n## Image size modes\n\n- `auto` — default; let the model choose the output size\n- ratio presets — 1K, 2K, or 4K with ratios `1:1`, `4:3`, `3:4`, `16:9`, `9:16`, or `21:9`\n- custom width and height — values are normalized to multiples of 16, max side `3840px`, aspect ratio up to `3:1`, and total pixels between `655360` and `8294400`\n\n## Generation options\n\n- Quality: `auto`, `low`, `medium`, or `high`\n- Format: `PNG`, `JPEG`, or `WebP`\n- Compression: disabled for `PNG`; `0-100` for `JPEG` and `WebP`\n- Quantity: integer from `1` to `10`; the field can be cleared while editing, and Generate\u002FEdit will restore an empty value to `1` on submit\n- Response Format: defaults to the active preset's value (`url` by default), with `none` and `b64_json` still available; `none` omits the `response_format` parameter\n\n## Import and upload limits\n\n- each uploaded source image is limited by `MAX_FILE_SIZE_MB`, must pass full Pillow decode validation plus pixel-bomb limits, and must be a supported raster format (`.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.avif`, `.bmp`, `.heic`, `.heif`, `.ico`, `.tif`, `.tiff`); SVG is rejected\n- `\u002Fapi\u002Fimport` accepts ZIP archives created by `\u002Fapi\u002Fdownload-all`\n- import archives must include `metadata.json`\n- selected-image ZIP downloads include `metadata.skipped` when requested gallery rows or image files are missing\n- import archives are validated for uploaded size, file count, total uncompressed size, metadata size, member-path safety, and compression ratio\n- imported image entries must pass file extension, content-type\u002Fmagic-byte, full decoder, and pixel-count validation before they are stored\n\n## Environment variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `DEFAULT_API_URL` | empty | Pre-fill API base URL; may omit or include `\u002Fv1` |\n| `DEFAULT_API_KEY` | empty | Pre-fill API key; use an env ref such as `${OPENAI_API_KEY}` by default. Literal keys require `ALLOW_PLAINTEXT_SECRETS=true` |\n| `DEFAULT_API_PATH` | `\u002Fv1\u002Fimages\u002Fgenerations` | Default upstream path; supported values are `\u002Fv1\u002Fimages\u002Fgenerations`, `\u002Fv1\u002Fresponses`, and `\u002Fv1\u002Fchat\u002Fcompletions` |\n| `DEFAULT_RESPONSES_MODEL` | `gpt-5.4` | Fallback top-level model used for `\u002Fv1\u002Fresponses` when no request\u002Fpreset model is provided |\n| `DEFAULT_UPSTREAM_SOCKS5_PROXY` | empty | Optional default global SOCKS5 proxy or env ref for generation\u002Fedit upstream API calls |\n| `ALLOW_PLAINTEXT_SECRETS` | `false` | Allow literal API keys \u002F SOCKS5 proxy URLs \u002F Webhook URLs \u002F R2 credentials to be persisted in SQLite instead of requiring `${ENV_VAR_NAME}` refs |\n| `PROMPT_OPTIMIZER_ENABLED` | `false` | Enable the server-side prompt optimizer |\n| `PROMPT_OPTIMIZER_API_URL` | empty | Full Chat Completions-compatible endpoint URL used by the prompt optimizer |\n| `PROMPT_OPTIMIZER_API_KEY` | empty | Prompt optimizer API key; use an env ref such as `${OPENAI_API_KEY}` by default. Literal keys require `ALLOW_PLAINTEXT_SECRETS=true` |\n| `PROMPT_OPTIMIZER_MODEL` | `gpt-4o-mini` | Prompt optimizer model |\n| `PROMPT_OPTIMIZER_TIMEOUT_SECONDS` | `60` | Prompt optimizer request timeout in seconds |\n| `PROMPT_OPTIMIZER_MAX_OUTPUT_CHARS` | `4000` | Max optimized prompt length returned to the textarea |\n| `PROMPT_OPTIMIZER_MAX_RESPONSE_MB` | `8` | Max optimizer upstream response body size in MB before JSON parsing |\n| `PROMPT_OPTIMIZER_HOST_ALLOWLIST` | empty | Optional comma-separated hostname allowlist for the optimizer endpoint |\n| `R2_BACKUP_ENABLED` | `false` | Enable Gallery Sync backup when using environment-based R2 configuration |\n| `R2_ENDPOINT_URL` | empty | Cloudflare R2 S3-compatible endpoint URL, for example `https:\u002F\u002FACCOUNT_ID.r2.cloudflarestorage.com` |\n| `R2_BUCKET_NAME` | empty | Bucket used by Gallery Sync backup |\n| `R2_REGION` | `auto` | S3 client region name for R2 |\n| `R2_KEY_PREFIX` | `gallery\u002F` | Object key prefix for gallery backups; use a dedicated prefix such as `gallery-test\u002F` for validation |\n| `R2_ACCESS_KEY_ID` | empty | R2 access key ID used by env-ref credentials |\n| `R2_SECRET_ACCESS_KEY` | empty | R2 secret access key used by env-ref credentials |\n| `APP_VERSION` | `VERSION` file | Override the app version shown in the UI and returned by `\u002Fapi\u002Fversion`; read on each request |\n| `GITHUB_REPO` | `Z1rconium\u002Fgpt-image-linux` | GitHub `owner\u002Frepo` used for latest-release update detection; set empty to disable latest-version checks |\n| `ENABLE_VERSION_CHECK` | `true` | Enable per-request GitHub latest-version checks for the header `New` badge |\n| `VERSION_CHECK_TIMEOUT_SECONDS` | `3` | Timeout for each GitHub latest release or branch `VERSION` request |\n| `VERSION_CHECK_BRANCH` | `main` | Branch used for the fallback raw `VERSION` file check |\n| `ENABLE_METRICS` | `false` | Enable `\u002Fapi\u002Fmetrics` JSON counters\u002Fgauges\u002Frates\u002Flatency summaries and Prometheus text output |\n| `SLOW_GALLERY_QUERY_MS` | `200` | Log `\u002Fapi\u002Fgallery` requests at or above this threshold with prompt presence\u002Flength\u002Fhash, other filters, page, total, and DB query time |\n| `ACCESS_KEY` | empty | Required by default; all non-health routes require unlock when set |\n| `ALLOW_UNAUTHENTICATED` | `false` | Set `true` to explicitly allow startup without `ACCESS_KEY` |\n| `ACCESS_KEY_COOKIE_NAME` | `gpt_image_access` | Browser cookie name used for the access-key session |\n| `ACCESS_COOKIE_SECURE` | `true` | Mark the access cookie Secure; set `false` only for plain-HTTP local\u002Fprivate deployments |\n| `ACCESS_MAX_FAILURES` | `5` | Failed access-key attempts before temporary lockout |\n| `ACCESS_LOCKOUT_SECONDS` | `300` | Lockout duration after too many failed access attempts |\n| `IP_ALLOWLIST` | empty | Comma-separated allowed IPs\u002FCIDRs |\n| `TRUST_PROXY_HEADERS` | `false` | Read `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto`, or `X-Forwarded-Host` from a trusted reverse proxy |\n| `TRUSTED_PROXY_IPS` | empty | Optional comma-separated proxy IPs\u002FCIDRs allowed to provide trusted proxy headers |\n| `PUBLIC_ORIGIN` | empty | Optional canonical browser origin such as `https:\u002F\u002Fpanel.example.com`; also contributes to the Host allowlist and CSRF expected origin |\n| `ALLOWED_HOSTS` | empty | Optional comma-separated allowed Host \u002F trusted `X-Forwarded-Host` values; accepts hostnames, `host:port`, or origins |\n| `CSRF_ORIGIN_CHECK_ENABLED` | `true` | Reject cross-origin `POST`, `PATCH`, and `DELETE` requests using `Origin` or `Referer` checks |\n| `UPSTREAM_HOST_ALLOWLIST` | empty | Optional comma-separated hostname allowlist for generation\u002Fedit upstream API URLs |\n| `WEBHOOK_HOST_ALLOWLIST` | empty | Optional comma-separated webhook hostname allowlist |\n| `MAX_FILE_SIZE_MB` | `50` | Max uploaded image size in MB for edit source images, imported image files, and downloaded upstream image URLs |\n| `MAX_JSON_BODY_MB` | `1` | Max JSON request body size in MB for non-upload API calls |\n| `MAX_UPSTREAM_JSON_MB` | `128` | Max upstream JSON\u002FSSE response body size in MB before parsing; prefer `response_format=url` for large or multi-image results |\n| `MAX_IMAGE_PIXELS` | `100000000` | Max decoded image pixels accepted by Pillow before decompression-bomb rejection |\n| `IMPORT_ARCHIVE_MAX_MB` | `1000` | Max uploaded ZIP size in MB for `\u002Fapi\u002Fimport` |\n| `IMPORT_MAX_FILES` | `500` | Max number of files allowed inside one import archive |\n| `IMPORT_MAX_UNCOMPRESSED_MB` | `1024` | Max total uncompressed size in MB across all files in an import archive |\n| `IMPORT_MAX_METADATA_BYTES` | `2097152` | Max `metadata.json` size in bytes for an import archive |\n| `IMPORT_MAX_COMPRESSION_RATIO` | `100` | Max allowed uncompressed\u002Fcompressed ratio for any imported file |\n| `MAX_ACTIVE_GENERATE_JOBS` | `2` | Max number of generation and edit jobs running concurrently |\n| `MAX_QUEUED_GENERATE_JOBS` | `20` | Max additional queued generation and edit jobs before new requests are rejected with `429` |\n| `MAX_PENDING_EDIT_SOURCE_MB` | `200` | Max total pending edit source image bytes in MB; set `0` to disable this byte cap |\n| `MAX_SSE_SUBSCRIBERS_GLOBAL` | `200` | Max active SSE subscribers across the process |\n| `MAX_SSE_SUBSCRIBERS_PER_IP` | `10` | Max active SSE subscribers per client IP |\n| `SSE_CONNECTION_TTL_SECONDS` | `3600` | Maximum lifetime for one SSE connection |\n| `IMAGES_DIR` | `.\u002Fimages` | Directory for saved images |\n| `THUMBNAILS_DIR` | `.\u002Fimages\u002Fthumbs` | Directory for generated gallery thumbnails |\n| `THUMBNAIL_MAX_SIDE` | `512` | Max thumbnail width\u002Fheight in pixels |\n| `DATA_DIR` | `.\u002Fdata` | Directory for SQLite runtime data |\n| `DATABASE_FILE` | `.\u002Fdata\u002Fapp.sqlite3` | SQLite database for gallery metadata and API presets |\n| `PYTHON_BASE_IMAGE` | `python:3.11-slim` | Docker build base image; override when Docker Hub is slow or blocked |\n| `NODE_BASE_IMAGE` | `node:24-alpine` | Docker frontend build base image; override when Docker Hub is slow or blocked |\n| `WEBHOOK_SIGNING_SECRET` | empty | Required when a global Webhook URL is configured; used to sign webhook payloads (`X-Webhook-Signature`) |\n| `WEBHOOK_TIMEOUT_SECONDS` | `5` | Webhook delivery timeout per attempt (seconds) |\n| `WEBHOOK_MAX_ATTEMPTS` | `3` | Max webhook delivery retry attempts |\n\n## Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `\u002F` | Frontend UI |\n| `GET` | `\u002Fhealth` | Health check |\n| `GET` | `\u002Fapi\u002Fversion` | Current app version, configured GitHub repo, and latest-release URL |\n| `GET` | `\u002Fapi\u002Faccess\u002Fstatus` | Check access-key session status |\n| `POST` | `\u002Fapi\u002Faccess` | Unlock access for 3 hours |\n| `POST` | `\u002Fapi\u002Fsettings` | Save the active API preset |\n| `GET` | `\u002Fapi\u002Fsettings` | Get current settings and presets |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fr2\u002Fhealth` | Validate a draft R2 Backup config and run bucket\u002Flist\u002Fwrite checks |\n| `POST` | `\u002Fapi\u002Fprompt\u002Foptimize` | Rewrite a prompt through the server-side optimizer |\n| `GET` | `\u002Fapi\u002Fprompt\u002Foptimizer-system-prompt` | Read the Prompt Optimizer system prompt, falling back to the built-in default |\n| `POST` | `\u002Fapi\u002Fprompt\u002Foptimizer-system-prompt` | Save the Prompt Optimizer system prompt to `DATA_DIR\u002Fprompt_optimizer_system_prompt.md` |\n| `GET` | `\u002Fapi\u002Fprompt-snippets` | List prompt snippets, optionally filtered by `query` |\n| `POST` | `\u002Fapi\u002Fprompt-snippets` | Create a prompt snippet |\n| `PATCH` | `\u002Fapi\u002Fprompt-snippets\u002F{snippet_id}` | Update a prompt snippet title, prompt, or favorite flag |\n| `DELETE` | `\u002Fapi\u002Fprompt-snippets\u002F{snippet_id}` | Delete a prompt snippet |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fpresets` | Create and activate an API preset |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}\u002Factivate` | Activate an API preset |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}\u002Fhealth` | Validate a saved API preset and run a low-cost upstream probe |\n| `DELETE` | `\u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}` | Delete an API preset |\n| `POST` | `\u002Fapi\u002Fgenerate` | Start an image generation job |\n| `POST` | `\u002Fapi\u002Fedits` | Start an image edit job with one or more multipart image uploads |\n| `POST` | `\u002Fapi\u002Fedits\u002Ffrom-gallery\u002F{image_id}` | Start an image edit job using an existing gallery image, optionally with uploaded references |\n| `GET` | `\u002Fapi\u002Fgenerate\u002Fjobs` | List queued\u002Frunning generation and edit jobs; pass `include_finished=true` with optional `limit`\u002F`offset` for paginated persisted history, and `failed_only=true` to return only `error`\u002F`upstream_error` history rows |\n| `GET` | `\u002Fapi\u002Fgenerate\u002Fjobs\u002Fevents` | Stream queued\u002Frunning generation and edit jobs over SSE |\n| `DELETE` | `\u002Fapi\u002Fgenerate\u002Fjobs\u002Fhistory` | Delete all persisted terminal generation\u002Fedit job history rows; queued\u002Frunning jobs and gallery images are kept |\n| `GET` | `\u002Fapi\u002Fgenerate\u002F{job_id}` | Get generation job status or result |\n| `GET` | `\u002Fapi\u002Fgenerate\u002F{job_id}\u002Fevents` | Stream generation job status\u002Fprogress over SSE |\n| `DELETE` | `\u002Fapi\u002Fgenerate\u002F{job_id}` | Cancel and remove a queued\u002Frunning generation or edit job |\n| `GET` | `\u002Fapi\u002Fgallery` | List gallery images with pagination and optional `prompt`, `model`, `preset`, `size`, `date_from`, `date_to`, `favorite`, and `include_total_bytes` filters |\n| `PATCH` | `\u002Fapi\u002Fgallery\u002F{id}\u002Ffavorite` | Set or clear a gallery favorite flag |\n| `GET` | `\u002Fapi\u002Fgallery\u002F{image_id}` | Get a single gallery entry by ID |\n| `GET` | `\u002Fapi\u002Fimage\u002F{filename}` | Serve image file |\n| `GET` | `\u002Fapi\u002Fthumb\u002F{filename}` | Serve or lazily create a WebP gallery thumbnail |\n| `GET` | `\u002Fapi\u002Fdownload\u002F{filename}` | Download image as attachment |\n| `DELETE` | `\u002Fapi\u002Fgallery\u002F{id}` | Delete gallery entry and its server image file |\n| `GET` | `\u002Fapi\u002Fdownload-all` | Download all gallery images plus `metadata.json` as a ZIP file |\n| `POST` | `\u002Fapi\u002Fgallery\u002Fexport-jobs` | Start a tracked gallery ZIP export job; optionally pass `ids` for selected images |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fexport-jobs\u002F{job_id}` | Get tracked ZIP export job status |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fexport-jobs\u002F{job_id}\u002Fevents` | Stream ZIP export pack progress over SSE |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fexport-jobs\u002F{job_id}\u002Fdownload` | Download a completed tracked ZIP export with `Content-Length` transfer progress |\n| `POST` | `\u002Fapi\u002Fgallery\u002Fsync-jobs` | Start a single active R2 gallery backup sync job |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fsync-jobs\u002F{job_id}` | Get R2 sync job status |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fsync-jobs\u002F{job_id}\u002Fevents` | Stream R2 sync progress over SSE |\n| `POST` | `\u002Fapi\u002Fimport` | Import a ZIP created by `\u002Fapi\u002Fdownload-all` |\n| `DELETE` | `\u002Fapi\u002Fgallery` | Delete all gallery entries and server image files |\n| `GET` | `\u002Fapi\u002Fmetrics` | Optional metrics snapshot; returns JSON by default or Prometheus exposition format with `Accept: text\u002Fplain`; only available when `ENABLE_METRICS=true` |\n| `GET` | `\u002Fapi\u002Fmetrics\u002Fprometheus` | Optional Prometheus exposition metrics; only available when `ENABLE_METRICS=true` |\n\n## Runtime behavior notes\n\n- app version comes from `APP_VERSION` then `VERSION`; both the local app version and optional GitHub remote check are evaluated on each web-triggered version request. The remote check reads the latest release first, falls back to the configured branch `VERSION`, and can show a `New` badge without blocking usage.\n- when `PUBLIC_ORIGIN` or `ALLOWED_HOSTS` is configured, unknown `Host` or trusted `X-Forwarded-Host` values are rejected before CSRF checks; `Sec-Fetch-Site: same-origin` does not bypass the Host allowlist\n- presets, prompt snippets, and gallery\u002Fjob data persist only in `DATABASE_FILE`\n- SQLite repository operations use short-lived connections with WAL enabled at startup; `DATA_DIR` is chmodded to `0700`, the SQLite DB\u002Fsidecars are chmodded to `0600`, and app shutdown\u002Ftests call the storage close hook so connection lifecycle stays explicit\n- generation and edit share one queue measured in image units (`MAX_ACTIVE_GENERATE_JOBS` + `MAX_QUEUED_GENERATE_JOBS`), all edit source images are staged under `DATA_DIR\u002Fedit-sources` and additionally capped by `MAX_PENDING_EDIT_SOURCE_MB`, support cancellation, and persist terminal history including `completed_at`\n- batch generation (`n > 1`) consumes `n` queue units; the parent job aggregates successful child results into `images[]`, Gallery metadata keeps the user-requested `n`, and actual upstream child calls are bounded by the global upstream-request semaphore\n- Prompt Optimizer uses its own server-side Chat Completions-compatible endpoint config and user-configurable request timeout\u002Fresponse-size cap, resolves API key env refs on the backend, stores its editable system prompt in `DATA_DIR\u002Fprompt_optimizer_system_prompt.md`, and does not consume generation\u002Fedit queue capacity.\n- R2 Backup uses boto3 against a Cloudflare R2 S3-compatible endpoint under `asyncio.to_thread`; sync jobs list only the configured prefix, upload missing local gallery filenames, and never serve, overwrite, or delete gallery images from R2.\n- SSE is the primary progress channel; `\u002Fapi\u002Fgenerate\u002Fjobs` provides list\u002Fhistory (`include_finished=true`, optional `limit`\u002F`offset`, optional `failed_only=true`), `\u002Fapi\u002Fgenerate\u002Fjobs\u002Fhistory` clears terminal history, and `\u002Fapi\u002Fgenerate\u002Fjobs\u002Fevents` streams debounced live job-list changes from memory\n- terminal job history includes `stage_timings` for `upstream_wait`, `download_decode`, `validate`, `thumbnail`, and `db_insert`; slow gallery queries are logged with prompt presence\u002Flength\u002Fhash plus other filters and totals and counted in metrics; optional metrics include queue depth, running jobs, failure ratios, job-stage latencies, and slow SQLite query counters; terminal job statuses distinguish `cancelled`, `interrupted`, and `upstream_error` in addition to the generic `error`\n- upstream JSON\u002FSSE bodies are read with a `MAX_UPSTREAM_JSON_MB` cap before parsing, JSON request bodies are capped by `MAX_JSON_BODY_MB`, and upstream image URL downloads are revalidated (SSRF-aware, no blind redirect follow), fully decoded with Pillow, pixel-limited by `MAX_IMAGE_PIXELS`, and bounded by `MAX_FILE_SIZE_MB`\n- `\u002Fapi\u002Fimport` enforces ZIP safety\u002Fsize\u002Fcount\u002Fcompression checks; `\u002Fapi\u002Fdownload-all` keeps the low-memory streaming path, while tracked export jobs write temp ZIP files so UI progress can cover both packing and transfer\n- gallery stores byte-size metadata and thumbnails (`THUMBNAILS_DIR`), with lazy thumbnail and opt-in byte-size backfill for older images\n- startup reconciliation removes gallery rows for missing files and marks previously running\u002Fqueued jobs as interrupted\n\n## Testing\n\n```bash\nnpm run frontend:check\nnpm run frontend:build\npython3 -m pytest backend\u002Ftests\u002Ftest_contract.py -q\nnpm run test:e2e\nRUN_PERFORMANCE_TESTS=true python3 -m pytest backend\u002Ftests\u002Ftest_performance.py -q\nnpm run test:e2e:perf\n```\n\nThe contract tests cover the frozen public API surface, including access cookies, settings, generation\u002Fedit job creation, job timing metrics, SSE response framing, gallery import\u002Fexport, frontend index\u002FCSP handling, static asset access, downloads, validation errors, and 500 error shape. Playwright covers access gate, settings drawer focus behavior, mocked generate\u002Fedit flows, gallery filtering\u002Fpage jumps\u002Fbatch actions, toast live regions, and lightbox keyboard close\u002Fnavigation.\n\n## Contributing\n\nContributions are welcome.\n\nHelpful guidelines:\n\n- keep backend changes simple and explicit\n- update `VERSION` when a user-visible change or release-worthy fix warrants a new `vMAJOR.MINOR.PATCH` version\n- use FastAPI response models from `backend\u002Fapp\u002Fschemas\u002F` where applicable\n- keep persistent storage operations centralized in `backend\u002Fapp\u002Frepositories\u002F`\n- keep upstream API interaction centralized in `backend\u002Fapp\u002Fintegrations\u002F`\n- keep browser requests on same-origin `\u002Fapi\u002F*` paths; do not introduce direct cross-origin frontend-to-backend calls\n- avoid storing real API keys in repository files\n- do not commit generated images or runtime gallery metadata unless explicitly requested\n- preserve the existing async generation flow and SSE progress model unless the change explicitly requires altering job lifecycle behavior\n\n## License\n\nThis project is licensed under `CC BY-NC 4.0` (`Creative Commons Attribution-NonCommercial 4.0 International`).\n\n- You can use, copy, modify, redistribute, and create derivative works.\n- You must provide attribution and keep the license notice.\n- You may not use this project or derivative works for commercial purposes.\n- If you need commercial use, you must obtain prior permission from the copyright holder.\n\nSee [LICENSE](.\u002FLICENSE) for the repository license text.\n\n---\n\n# 中文文档\n\n# GPT Image Panel\n\nGPT Image 2 API 图像生成和编辑 Web 面板。\n\n[English](#gpt-image-panel) | 中文\n\n## 概述\n\nGPT Image Panel 是一个轻量级 FastAPI Web 界面，用于图像生成和图像编辑。它被设计为自托管面板，连接外部 GPT 兼容图像 API，并在本地存储生成的图片。\n\n## 免责声明\n\n本项目仅是一个用于发起图像生成和图像编辑请求的自托管 Web 可视化面板。本项目不提供、不封装、不代理、不转售、也不修改任何上游模型或 API 服务。实际生成能力、内容政策执行、账号权限、计费规则和模型行为均来自用户自行配置的上游 API 服务商。\n\n本项目不鼓励、不支持生成色情、政治、违法、侵权、暴力、仇恨，或其他敏感及违反政策的内容。用户需要自行对其配置的上游服务、提示词、上传图片、生成结果、存储、传播，以及由此产生的法律或平台政策后果负责。任何通过上游 API 生成的内容均与本项目无关，亦不代表项目维护者生成、审核、托管或认可该内容。\n\n主要特点：\n\n- SvelteKit + TypeScript 前端位于 `frontend\u002F`\n- FastAPI 后端位于 `backend\u002Fapp\u002F`；ASGI 入口使用 `backend.app.main:app`\n- 公共 API 路径、方法、状态码、SSE 事件名、cookie 和响应结构通过契约测试冻结\n- API 预设持久化保存在 SQLite：`data\u002Fapp.sqlite3`\n- 生成\u002F编辑任务通过 `asyncio.create_task` 异步执行\n- 图片保存在 `images\u002F`\n- Gallery 元数据保存在 SQLite：`data\u002Fapp.sqlite3`\n- Docker 镜像会构建并内置 SvelteKit 静态前端\n- pytest API 契约测试位于 `backend\u002Ftests\u002F`\n\n## 功能\n\n- API 预设管理：base URL\u002Fpath\u002Fkey、每个预设的默认 model 和 Response Format、全局 SOCKS5 上游代理和全局 Webhook URL\n- 提示词助手标签、服务端提示词优化器、独立提示词收藏夹，以及 Gallery 提示词\u002F参数复用\n- 图像生成 + 图生图编辑（`\u002Fv1\u002Fimages\u002Fedits`），支持尺寸\u002F质量\u002F格式\u002F压缩率\u002F数量等参数，并支持最多 16 张编辑参考图\n- 预览 + 历史任务：SSE 进度、多图结果预览、`completed_at`、耗时、任务分段耗时、加载状态、细分终态状态、仅错误历史筛选、清空持久化历史、排队\u002F运行任务取消，以及从持久化历史复用\u002F重试\n- 生成与编辑共享并发和排队限制\n- 可选全局 Webhook URL：HTTPS 校验、SSRF 防护、签名、重试，以及设置响应打码\n- Gallery：筛选（FTS 提示词搜索、模型、预设、尺寸、日期区间、收藏）、URL 同步的 page\u002F非提示词筛选\u002Flightbox\u002Fjob history 状态、页码输入跳转、Lightbox 上一张\u002F下一张导航和左右方向键快捷键、”Edit this image”、下载\u002F删除、批量操作部分成功反馈、单图 5 秒撤销删除、复制提示词\u002F图片链接、按需总大小统计\n- 提示词收藏夹：可复用 prompt 模板，与 Gallery 图片分开存储和管理\n- ZIP 导出导入（含 `metadata.json`）+ 流式上传 + 安全校验 + 低内存导出路径 + 批量下载 skipped metadata + 可见导入\u002F导出\u002F下载进度状态\n- Cloudflare R2 Gallery 备份同步：可通过 `.env` 或 Web Settings 配置，可在 Settings 测试连通性，并可从 Gallery 手动增量同步；本地 Gallery 存储仍是唯一源数据\n- 访问密钥、Host\u002Fpublic origin 白名单、IP 白名单\u002F反向代理头、版本检测、CSP nonce\n- 观测能力：任务分段耗时、慢 `\u002Fapi\u002Fgallery` 查询日志、队列\u002F失败率指标、可选 JSON\u002FPrometheus metrics\n\n## 架构\n\n### 后端\n\n后端是 FastAPI 应用，位于 `backend\u002Fapp\u002F`。\n\n功能拆分为以下模块：\n\n- `backend\u002Fapp\u002Fmain.py` — 很薄的 ASGI 入口\n- `backend\u002Fapp\u002Fapi\u002Fcontract_app.py` — 冻结的公共 API 表面和当前路由组装\n- `backend\u002Fapp\u002Fschemas\u002F` — Pydantic 请求\u002F响应 DTO\n- `backend\u002Fapp\u002Frepositories\u002F` — SQLite、Gallery、图片文件、settings 和 jobs 持久化\n- `backend\u002Fapp\u002Fintegrations\u002F` — 上游 GPT 兼容图片 API 调用\n- `backend\u002Fapp\u002Fcore\u002F` — settings、访问 token、IP allowlist、proxy header 和 URL 校验\n- `backend\u002Fapp\u002Fservices\u002F` — webhook 签名、重试和异步投递\n\n后端服务静态前端时，会为 `frontend\u002Fbuild\u002Findex.html` 注入每次响应不同的 script nonce，并发送匹配的 Content Security Policy。前端入口和静态资源访问已纳入后端契约测试。\n\n### 前端\n\n前端是 SvelteKit 静态应用，位于 `frontend\u002F`。\n后端只服务 `frontend\u002Fbuild`；生产方式启动后端前需要先完成前端构建。\n\n使用技术：\n\n- Tailwind CSS\n- `src\u002Flib\u002Fapi\u002Fclient.ts` 封装同源 `\u002Fapi\u002F*` fetch\n- `src\u002Flib\u002Fapi\u002Fevents.ts` 封装 SSE\n- stores 拆分为 access、settings、prompt snippets、gallery、gallery actions、edit source、jobs、preview、lightbox、version 和 UI\n- 组件拆分为 access、header、settings drawer、prompt snippets drawer、prompt helper、job history drawer、preview、gallery、lightbox 和 size dialog\n\n前端构建命令：\n\n```bash\nnpm --prefix frontend install\nnpm --prefix frontend run build\n```\n\n### 存储\n\n运行时持久化存储非常简单：\n\n- 生成的图片保存在 `images\u002F` 目录\n- Gallery 元数据、图片字节数、FTS 提示词索引和 API 预设保存在 SQLite：`data\u002Fapp.sqlite3`，包含真实图片宽高、`completed_at` 完成时间、北京时间生成完成时间和生成耗时\n- 提示词收藏夹保存在 SQLite：`data\u002Fapp.sqlite3`，独立于 Gallery 元数据\n- 可选 R2 备份只做增量对象上传；本地 `images\u002F` 文件和 SQLite Gallery 行仍是图片服务、缩略图、ZIP 导出和导入流程使用的唯一 Gallery 源\n- 生成\u002F编辑任务的状态、错误、耗时、`completed_at`、请求参数和结果元数据保存在 SQLite：`data\u002Fapp.sqlite3`；多图任务会保留完整 `images` 结果列表，同时继续用第一张结果填充 `image_id`\u002F`image_url` 以兼容旧客户端\n- 运行中的 `asyncio.Task` 句柄仅保存在进程内存中；重启后，上个进程遗留的排队\u002F运行任务会被标记为 interrupted\n\n### 生成流程\n\n1. 前端调用 `\u002Fapi\u002Fgenerate`\n2. 后端校验配置并创建 SQLite 任务，再异步调度执行\n3. 执行前检查共享并发\u002F队列限制，执行中通过 SSE 推送细分进度\n4. `n > 1` 的生成请求仍只暴露一个父任务，但会按 `n` 计入队列容量，并通过全局上游请求 semaphore 拆成多次内部调用；`\u002Fv1\u002Fimages\u002Fgenerations` 子请求 payload 固定使用 `n=1`\n5. 上游返回数据解码\u002F下载、校验并落盘，同时更新 Gallery 元数据\n6. 任务历史可通过 `\u002Fapi\u002Fgenerate\u002Fjobs*` 查询\u002F订阅，可清空（`DELETE \u002Fapi\u002Fgenerate\u002Fjobs\u002Fhistory`）或取消运行中任务；可选触发签名 webhook 回调\n\n### 编辑流程\n\n1. 前端选择编辑源（上传一张或多张图片，也可以组合一张 Gallery 图片）并调用 `\u002Fapi\u002Fedits` 或 `\u002Fapi\u002Fedits\u002Ffrom-gallery\u002F{image_id}`\n2. 后端创建任务并以 multipart 调用上游 `\u002Fv1\u002Fimages\u002Fedits`\n3. 源图片和支持参数会被转发；多参考图会以重复的 `image[]` 字段发给上游，不支持格式（如 SVG）会被拒绝\n4. 通过 SSE 推送进度；返回数据解码\u002F下载、校验并落盘\n5. 编辑结果进入预览和 Gallery，沿用与生成一致的队列\u002F历史\u002F取消模型\n\n## 技术栈\n\n- Python 3.11+\n- FastAPI\n- Uvicorn\n- aiohttp\n- SQLite\n- Pydantic v2\n- SvelteKit\n- TypeScript\n- Tailwind CSS\n\n## 项目结构\n\n```text\nLICENSE\nREADME.md\nDockerfile\ndocker-compose.yml\n.env.example\nVERSION\nrequirements.txt\npackage.json\nbackend\u002F\n  requirements-dev.txt\n  app\u002F\n    main.py\n    api\u002F\n    core\u002F\n    schemas\u002F\n    repositories\u002F\n    integrations\u002F\n    services\u002F\n  tests\u002F\nfrontend\u002F\n  package.json\n  svelte.config.js\n  vite.config.ts\n  src\u002F\n    routes\u002F\n    lib\u002F\n      api\u002F\n      stores\u002F\n      components\u002F\n      utils\u002F\ndeploy\u002F\n  nginx.conf\nimages\u002F\ndata\u002F\n```\n\n## 快速开始\n\n### 前置条件\n\n需要以下条件之一：\n\n- Python 3.11 或更新版本\n- Docker\n- Docker Compose\n\n实际生成图像需要一个外部 GPT 兼容图像 API。\n\n### Docker\n\n```bash\ndocker build -t gpt-image-panel .\ndocker run -d --name gpt-image-panel \\\n  -p 127.0.0.1:9090:9090 \\\n  -v $(pwd)\u002Fimages:\u002Fapp\u002Fimages \\\n  -v $(pwd)\u002Fdata:\u002Fapp\u002Fdata \\\n  gpt-image-panel\n```\n\n如果解析 `python:3.11-slim` 时 Docker Hub 超时，可以改用可访问的镜像源：\n\n```bash\ndocker build \\\n  --build-arg PYTHON_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fpython:3.11-slim \\\n  --build-arg NODE_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fnode:24-alpine \\\n  -t gpt-image-panel .\n```\n\n### Docker Compose\n\n```bash\ncp .env.example .env\n# 按需修改 .env\n# 默认必须设置 ACCESS_KEY，除非显式设置 ALLOW_UNAUTHENTICATED=true\ndocker-compose up -d --build --force-recreate\n```\n\n如果 Compose 构建时 Docker Hub 超时，先在 `.env` 里设置：\n\n```bash\nPYTHON_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fpython:3.11-slim\nNODE_BASE_IMAGE=docker.m.daocloud.io\u002Flibrary\u002Fnode:24-alpine\n```\n\n### 本地开发\n\n```bash\npip install -r backend\u002Frequirements-dev.txt\nnpm --prefix frontend install\nnpm run backend:dev\n```\n\n另开一个终端：\n\n```bash\nnpm run frontend:dev\n```\n\n然后打开 `http:\u002F\u002Flocalhost:5173`。Vite dev server 会把 `\u002Fapi` 和 `\u002Fhealth` 代理到 `127.0.0.1:9090`，浏览器侧仍然是同源路径。\n默认只监听 `127.0.0.1`；如果要做局域网\u002F外网调试，使用 `npm run frontend:dev -- --host 0.0.0.0`。\n\n如果要单进程 smoke test：\n\n```bash\nnpm run frontend:build\nuvicorn backend.app.main:app --host 0.0.0.0 --port 9090 --reload\n```\n\n然后打开 `http:\u002F\u002Flocalhost:9090`。\n\n若本地开发需要无鉴权启动，请设置 `ALLOW_UNAUTHENTICATED=true`。\n\n### 健康检查\n\n```bash\ncurl http:\u002F\u002Flocalhost:9090\u002Fhealth\n```\n\n## 使用方法\n\n1. 打开网站\n2. 可用左上角语言按钮在英文\u002F简体中文之间切换\n3. 点击右上角齿轮图标\n4. 选择已有预设，或点击 New 新建预设\n5. 填写 API Base URL\n6. 选择 API Path\n7. 填写该预设的默认模型；Generate\u002FEdit 表单里的 Model 默认值会使用当前预设的值\n8. 选择该预设的默认 Response Format；Generate\u002FEdit 表单里的 Response format 默认值会使用当前预设的值\n9. 填写 API Key，或填写 `${OPENAI_API_KEY}` 这类环境变量引用；直接填写的 key 需要显式设置 `ALLOW_PLAINTEXT_SECRETS=true`\n10. 可选：填写全局 SOCKS5 代理，或填写 `${UPSTREAM_PROXY_URL}` 这类环境变量引用\n11. 可选：填写全局 Webhook URL，或填写 `${WEBHOOK_URL}` 这类环境变量引用，用于生成\u002F编辑任务完成回调\n12. 可选：配置 R2 备份的 endpoint URL、储存桶、prefix，以及 `${R2_ACCESS_KEY_ID}` \u002F `${R2_SECRET_ACCESS_KEY}` 这类环境变量引用，然后点击测试 R2\n13. 可选：配置提示词优化器的 endpoint URL、模型、超时时间和 API Key\u002F环境变量引用；直接填写的 key 需要显式设置 `ALLOW_PLAINTEXT_SECRETS=true`\n14. 可选：对已保存预设执行 Health check\n15. 点击 Save Preset\n16. 输入提示词\n17. 点击提示词助手标签追加常用修饰词\n18. 点击 Optimize 通过服务端优化器改写提示词\n19. 点击右上角提示词按钮保存或复用提示词片段；使用片段会替换当前提示词\n20. 选择生成参数；需要逐次复用不同上游路径时可直接选择 API Path\n21. 点击 Generate\n22. 也可以上传一张或多张编辑参考图、在 Gallery\u002FLightbox 中选择 “Edit this image”，或两者组合；上传会追加到当前编辑源，Clear 会清空全部编辑源\n23. 点击 Edits 执行图生图\n24. 在 Gallery\u002FLightbox 使用 “Use prompt” 或 “Use all” 复用历史提示词或完整参数\n25. 查看预览和 Gallery；点击 Gallery 的同步按钮可把本地 Gallery 中 R2 prefix 下缺失的图片上传到配置的储存桶\n\n## 支持的 API Path\n\n面板支持以下上游路径。API Base URL 可以不带 `\u002Fv1`，也可以带 `\u002Fv1`；例如 `https:\u002F\u002Fapi.example.com` 和 `https:\u002F\u002Fapi.example.com\u002Fv1` 都可以。\n\n### `\u002Fv1\u002Fimages\u002Fgenerations`\n\n- 向 Images API 发送生成请求\n- 从 `data[]` 读取图片数据\n\n### `\u002Fv1\u002Fresponses`\n\n- 向 Responses API 发送生成请求\n- 上游请求体只发送 `prompt` 和 `model`；UI 里的模型默认值来自当前预设\n- 从 `output[]` 中类型为 `image_generation_call` 的项目读取 base64 图片数据\n- 选择该路径时，界面中的尺寸、质量、格式、压缩率、数量和 response format 控件会被禁用\n\n### `\u002Fv1\u002Fchat\u002Fcompletions`\n\n- 发送兼容 OpenAI Chat Completions 的生成请求\n- 上游请求体只发送 `model`、`messages` 和 `stream: false`\n- 支持 `grok-imagine-image-lite` 以及其他通过 chat completion 消息返回图片 URL 或 base64 数据的图像模型\n- 可从 JSON chat completions 或 `data:` SSE chunk 中读取图片输出，包括 `![image](https:\u002F\u002F...)` 这类 Markdown 图片链接\n- 选择该路径时，界面中的尺寸、质量、格式、压缩率、数量和 response format 控件会被禁用\n\n### `\u002Fv1\u002Fimages\u002Fedits`\n\n- 上传图片后点击 Edits、在 Gallery 里选择 “Edit this image” 后点击 Edits，或两者组合使用\n- 始终在配置的 API Base URL 下调用 `\u002Fv1\u002Fimages\u002Fedits`\n- 使用 multipart\u002Fform-data 发送源图片字段和支持的编辑参数；单张上传使用 `image`，多参考图会以重复的 `image[]` 字段转发给上游\n- 最多支持 16 张编辑参考图；本地上传会追加到当前编辑源列表，并可与一张 Gallery 源图组合\n- 上传的源图必须是受支持的位图图片格式；SVG 上传会被拒绝\n- 如果上游返回 `404`、`405` 或 `501`，界面会提示 `\u002Fv1\u002Fimages\u002Fedits` 不受支持并停止编辑请求\n\n## API 预设健康检查\n\n- `POST \u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}\u002Fhealth` 会校验已保存预设，不会发送真实生成请求\n- 检查项包括 API Path 是否允许、HTTPS URL\u002Fhostname、上游 host allowlist、SSRF DNS\u002F内网 IP 校验、API Key\u002F环境变量引用是否可用，以及低成本 `OPTIONS`\u002F`HEAD` 上游探测\n- 返回结构为 `{ status, checks: [{ name, status, message }] }`，状态值为 `ok`、`warning` 或 `error`\n- API Key 环境变量引用必须使用完整的 `${ENV_VAR_NAME}` 格式；数据库只保存引用字符串，生成\u002F编辑请求会在执行时从服务端环境变量解析真实值\n- 默认拒绝直接把 API Key 明文持久化到 SQLite；只有显式设置 `ALLOW_PLAINTEXT_SECRETS=true` 才允许\n\n## 上游 SOCKS5 代理\n\n- Settings 抽屉提供一个全局 `SOCKS5 代理` 字段，不跟随 API 预设切换。\n- 留空时生成\u002F编辑上游 API 请求保持直连。\n- 默认使用 `${UPSTREAM_PROXY_URL}` 这类环境变量引用。若直接填写 `socks5:\u002F\u002Fhost:port` 或 `socks5:\u002F\u002Fuser:pass@host:port`，需要显式设置 `ALLOW_PLAINTEXT_SECRETS=true`；保存后的代理密码会在 API 响应和 UI 中打码。\n- 代理边界刻意收窄：只有生成\u002F编辑的上游 API `POST` 请求会使用 SOCKS5。Preset health check、Webhook、版本检查、前端 `\u002Fapi\u002F*` 请求和上游返回的图片 URL 下载都保持直连。\n\n## 全局 Webhook URL\n\n- Settings 抽屉在 `SOCKS5 代理` 下方提供一个全局 `Webhook URL` 字段，不跟随 API 预设切换。\n- 留空时不发送任务回调。\n- 默认使用 `${WEBHOOK_URL}` 这类环境变量引用；若直接填写 webhook URL，需要显式设置 `ALLOW_PLAINTEXT_SECRETS=true`。\n- 配置后，生成\u002F编辑任务完成时会向该 HTTPS URL 发送签名 webhook 回调。\n- 配置 Webhook URL 时需要设置 `WEBHOOK_SIGNING_SECRET`；保存后的 webhook URL 会在 API 响应和 UI 中打码。\n\n## R2 Gallery 备份同步\n\n- R2 Backup 是增量备份路径，不是远端 Gallery 存储。\n- 可以用 `.env` 的 `R2_BACKUP_ENABLED=true` 加其他 `R2_*` 变量配置，也可以在 Web Settings 中保存配置。当 SQLite 里还没有保存过 R2 settings 时，启动会把当前 `.env` R2 值持久化进去，所以 Web Settings 会直接显示这些值；之后 Web Settings 保存的值优先。Web Settings 的凭据字段支持 `${ENV_VAR_NAME}` 引用；直接保存明文凭据需要 `ALLOW_PLAINTEXT_SECRETS=true`。\n- Settings 里的测试 R2 会执行 `HeadBucket`、带 prefix 的 `ListObjectsV2`，并写入一个很小的 probe object；probe 清理失败会作为 warning 返回。\n- Gallery 的同步按钮只上传 `R2_KEY_PREFIX` 下缺失的本地 Gallery filename；已存在对象会跳过，bucket 中额外对象不会删除或覆盖。\n\n## 图像尺寸模式\n\n- `auto` — 默认值；让模型自动选择输出尺寸\n- 比例预设 — 1K \u002F 2K \u002F 4K，支持比例 `1:1`、`4:3`、`3:4`、`16:9`、`9:16`、`21:9`\n- 自定义宽高 — 会归一化到 16 的倍数，最大边 `3840px`，最大纵横比 `3:1`，像素总量在 `655360` 到 `8294400` 之间\n\n## 生成选项\n\n- Quality：`auto`、`low`、`medium`、`high`\n- Format：`PNG`、`JPEG`、`WebP`\n- Compression：`PNG` 不可用；`JPEG` 和 `WebP` 可设置 `0-100`\n- Quantity：`1` 到 `10`；编辑时可以先清空，提交 Generate\u002FEdit 时如果为空会自动回填为 `1`\n- Response Format：默认使用当前预设的值（默认 `url`），仍可选 `none` 和 `b64_json`；`none` 会省略 `response_format` 参数\n\n## 导入与上传限制\n\n- 每张上传的编辑源图大小受 `MAX_FILE_SIZE_MB` 限制，必须通过 Pillow 完整解码校验和像素炸弹限制，且必须是受支持的位图格式（`.png`、`.jpg`、`.jpeg`、`.webp`、`.gif`、`.avif`、`.bmp`、`.heic`、`.heif`、`.ico`、`.tif`、`.tiff`）；SVG 会被拒绝\n- `\u002Fapi\u002Fimport` 只接受由 `\u002Fapi\u002Fdownload-all` 导出的 ZIP 归档\n- 导入 ZIP 必须包含 `metadata.json`\n- 所选图片 ZIP 下载遇到缺失图库行或缺失图片文件时，会在 `metadata.skipped` 中记录跳过项\n- 导入 ZIP 会校验上传体积、文件数、解压总体积、metadata 大小、安全路径和压缩比\n- 导入图片条目在存储前必须通过扩展名、Content-Type\u002F文件魔数、完整解码和像素数量校验\n\n## 环境变量\n\n| 变量 | 默认值 | 说明 |\n|------|--------|------|\n| `DEFAULT_API_URL` | 空 | 预填 API Base URL；可以不带或带 `\u002Fv1` |\n| `DEFAULT_API_KEY` | 空 | 预填 API Key；优先使用 `${OPENAI_API_KEY}` 这类环境变量引用，直接填写的 key 会以明文保存到 SQLite |\n| `DEFAULT_API_PATH` | `\u002Fv1\u002Fimages\u002Fgenerations` | 默认上游路径；支持 `\u002Fv1\u002Fimages\u002Fgenerations`、`\u002Fv1\u002Fresponses` 和 `\u002Fv1\u002Fchat\u002Fcompletions` |\n| `DEFAULT_RESPONSES_MODEL` | `gpt-5.4` | 当请求\u002F预设没有提供模型时，`\u002Fv1\u002Fresponses` 使用的兜底顶层模型 |\n| `DEFAULT_UPSTREAM_SOCKS5_PROXY` | 空 | 可选的全局 SOCKS5 代理默认值，仅用于生成\u002F编辑的上游 API 请求 |\n| `PROMPT_OPTIMIZER_ENABLED` | `false` | 是否启用服务端提示词优化器 |\n| `PROMPT_OPTIMIZER_API_URL` | 空 | 提示词优化器使用的完整 Chat Completions 兼容 endpoint URL |\n| `PROMPT_OPTIMIZER_API_KEY` | 空 | 提示词优化器 API Key；优先使用 `${OPENAI_API_KEY}` 这类环境变量引用，直接填写的 key 会以明文保存到 SQLite |\n| `PROMPT_OPTIMIZER_MODEL` | `gpt-4o-mini` | 提示词优化器模型 |\n| `PROMPT_OPTIMIZER_TIMEOUT_SECONDS` | `60` | 提示词优化请求超时时间（秒） |\n| `PROMPT_OPTIMIZER_MAX_OUTPUT_CHARS` | `4000` | 回填到文本框的优化后提示词最大长度 |\n| `PROMPT_OPTIMIZER_MAX_RESPONSE_MB` | `8` | JSON 解析前允许的最大优化器上游响应体积（MB） |\n| `PROMPT_OPTIMIZER_HOST_ALLOWLIST` | 空 | 可选的优化器 endpoint 主机名白名单，逗号分隔 |\n| `R2_BACKUP_ENABLED` | `false` | 使用环境变量配置 R2 时，启用 Gallery 同步备份 |\n| `R2_ENDPOINT_URL` | 空 | Cloudflare R2 S3 兼容 endpoint URL，例如 `https:\u002F\u002FACCOUNT_ID.r2.cloudflarestorage.com` |\n| `R2_BUCKET_NAME` | 空 | Gallery 同步备份使用的储存桶 |\n| `R2_REGION` | `auto` | R2 使用的 S3 client region name |\n| `R2_KEY_PREFIX` | `gallery\u002F` | Gallery 备份对象 key prefix；手动验证建议使用 `gallery-test\u002F` 这类独立 prefix |\n| `R2_ACCESS_KEY_ID` | 空 | env-ref 凭据解析使用的 R2 access key ID |\n| `R2_SECRET_ACCESS_KEY` | 空 | env-ref 凭据解析使用的 R2 secret access key |\n| `APP_VERSION` | `VERSION` 文件 | 覆盖界面显示和 `\u002Fapi\u002Fversion` 返回的当前应用版本；每次请求实时读取 |\n| `GITHUB_REPO` | `Z1rconium\u002Fgpt-image-linux` | 用于检测 latest release 新版本的 GitHub `owner\u002Frepo`；设为空可禁用最新版本检查 |\n| `ENABLE_VERSION_CHECK` | `true` | 启用每次请求的 GitHub 最新版本检测，用于 Header 的 `New` 标记 |\n| `VERSION_CHECK_TIMEOUT_SECONDS` | `3` | 每次请求 GitHub latest release 或分支 `VERSION` 的超时时间 |\n| `VERSION_CHECK_BRANCH` | `main` | latest release 失败后，用于回退读取 raw `VERSION` 文件的分支 |\n| `ENABLE_METRICS` | `false` | 启用 `\u002Fapi\u002Fmetrics` JSON counters\u002Fgauges\u002Frates\u002F延迟摘要和 Prometheus 文本输出 |\n| `SLOW_GALLERY_QUERY_MS` | `200` | `\u002Fapi\u002Fgallery` 达到该阈值时记录筛选条件、页码、total 和 DB 查询耗时 |\n| `ACCESS_KEY` | 空 | 默认要求设置；设置后每个非健康路由均需解锁 |\n| `ALLOW_UNAUTHENTICATED` | `false` | 设置为 `true` 可显式允许在未设置 `ACCESS_KEY` 时启动 |\n| `ACCESS_KEY_COOKIE_NAME` | `gpt_image_access` | 访问会话使用的浏览器 cookie 名称 |\n| `ACCESS_COOKIE_SECURE` | `true` | 给访问 cookie 添加 Secure；仅在纯 HTTP 本地\u002F内网部署时设为 `false` |\n| `ACCESS_MAX_FAILURES` | `5` | 触发临时锁定前允许的访问密钥失败次数 |\n| `ACCESS_LOCKOUT_SECONDS` | `300` | 失败次数过多后的锁定时长（秒） |\n| `IP_ALLOWLIST` | 空 | 允许访问的 IP\u002FCIDR，逗号分隔 |\n| `TRUST_PROXY_HEADERS` | `false` | 是否读取受信任反向代理的 `X-Forwarded-For`、`X-Real-IP`、`X-Forwarded-Proto` 或 `X-Forwarded-Host` |\n| `TRUSTED_PROXY_IPS` | 空 | 允许提供可信代理头的代理 IP\u002FCIDR，逗号分隔 |\n| `PUBLIC_ORIGIN` | 空 | 可选的浏览器侧规范 origin，例如 `https:\u002F\u002Fpanel.example.com`；同时加入 Host 白名单并作为 CSRF expected origin |\n| `ALLOWED_HOSTS` | 空 | 可选的 Host \u002F 受信任 `X-Forwarded-Host` 白名单，逗号分隔；支持 hostname、`host:port` 或 origin |\n| `CSRF_ORIGIN_CHECK_ENABLED` | `true` | 是否通过 `Origin` 或 `Referer` 拒绝跨站 `POST`、`PATCH`、`DELETE` 请求 |\n| `UPSTREAM_HOST_ALLOWLIST` | 空 | 生成\u002F编辑上游 API URL 的可选主机名白名单，逗号分隔 |\n| `WEBHOOK_HOST_ALLOWLIST` | 空 | 可选 webhook 主机名白名单，逗号分隔 |\n| `MAX_FILE_SIZE_MB` | `50` | 上传为编辑源图的图片、导入图片文件和上游图片 URL 下载的最大体积（MB） |\n| `MAX_JSON_BODY_MB` | `1` | 非上传 API 调用的最大 JSON 请求体积（MB） |\n| `MAX_UPSTREAM_JSON_MB` | `128` | 解析前允许的最大上游 JSON\u002FSSE 响应体积（MB）；大图或多图建议使用 `response_format=url` |\n| `MAX_IMAGE_PIXELS` | `100000000` | Pillow 接受的最大解码图片像素数，超过后按 decompression bomb 拒绝 |\n| `IMPORT_ARCHIVE_MAX_MB` | `1000` | `\u002Fapi\u002Fimport` 可上传 ZIP 的最大体积（MB） |\n| `IMPORT_MAX_FILES` | `500` | 单个导入归档允许的最大文件数 |\n| `IMPORT_MAX_UNCOMPRESSED_MB` | `1024` | 导入归档内所有文件解压后的最大总体积（MB） |\n| `IMPORT_MAX_METADATA_BYTES` | `2097152` | 导入归档中 `metadata.json` 的最大字节数 |\n| `IMPORT_MAX_COMPRESSION_RATIO` | `100` | 单个导入文件允许的最大解压\u002F压缩体积比 |\n| `MAX_ACTIVE_GENERATE_JOBS` | `2` | 生成和编辑任务允许同时运行的最大数量 |\n| `MAX_QUEUED_GENERATE_JOBS` | `20` | 超出并发后允许继续排队的最大任务数；超过后新请求返回 `429` |\n| `MAX_PENDING_EDIT_SOURCE_MB` | `200` | 待处理编辑源图的总字节上限（MB）；设为 `0` 可关闭该字节上限 |\n| `MAX_SSE_SUBSCRIBERS_GLOBAL` | `200` | 全进程允许的最大活跃 SSE 订阅数 |\n| `MAX_SSE_SUBSCRIBERS_PER_IP` | `10` | 单个客户端 IP 允许的最大活跃 SSE 订阅数 |\n| `SSE_CONNECTION_TTL_SECONDS` | `3600` | 单条 SSE 连接的最长生命周期（秒） |\n| `IMAGES_DIR` | `.\u002Fimages` | 图片存储目录 |\n| `THUMBNAILS_DIR` | `.\u002Fimages\u002Fthumbs` | Gallery 缩略图生成目录 |\n| `THUMBNAIL_MAX_SIDE` | `512` | 缩略图最大宽\u002F高像素 |\n| `ALLOW_PLAINTEXT_SECRETS` | `false` | 是否允许把 API Key \u002F SOCKS5 代理 URL \u002F Webhook URL \u002F R2 凭据明文持久化到 SQLite；默认要求 `${ENV_VAR_NAME}` 引用 |\n| `DATA_DIR` | `.\u002Fdata` | SQLite 运行时数据目录；启动时会收紧到 `0700` |\n| `DATABASE_FILE` | `.\u002Fdata\u002Fapp.sqlite3` | 保存 Gallery 元数据和 API 预设的 SQLite 数据库；启动时会收紧到 `0600` |\n| `PYTHON_BASE_IMAGE` | `python:3.11-slim` | Docker 构建基础镜像；Docker Hub 慢或不可访问时可覆盖 |\n| `NODE_BASE_IMAGE` | `node:24-alpine` | Docker 前端构建基础镜像；Docker Hub 慢或不可访问时可覆盖 |\n| `WEBHOOK_SIGNING_SECRET` | 空 | 配置全局 Webhook URL 时需要；用于签名 webhook payload（`X-Webhook-Signature`） |\n| `WEBHOOK_TIMEOUT_SECONDS` | `5` | 单次 webhook 投递超时时间（秒） |\n| `WEBHOOK_MAX_ATTEMPTS` | `3` | webhook 最大重试次数 |\n\n## 接口列表\n\n| 方法 | 路径 | 说明 |\n|------|------|------|\n| `GET` | `\u002F` | 前端页面 |\n| `GET` | `\u002Fhealth` | 健康检查 |\n| `GET` | `\u002Fapi\u002Fversion` | 当前应用版本、配置的 GitHub 仓库和 latest release URL |\n| `GET` | `\u002Fapi\u002Faccess\u002Fstatus` | 访问密钥会话状态 |\n| `POST` | `\u002Fapi\u002Faccess` | 解锁访问 3 小时 |\n| `POST` | `\u002Fapi\u002Fsettings` | 保存当前 API 预设 |\n| `GET` | `\u002Fapi\u002Fsettings` | 获取当前设置和预设列表 |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fr2\u002Fhealth` | 校验草稿 R2 Backup 配置并执行 bucket\u002Flist\u002Fwrite 检查 |\n| `POST` | `\u002Fapi\u002Fprompt\u002Foptimize` | 通过服务端提示词优化器改写提示词 |\n| `GET` | `\u002Fapi\u002Fprompt-snippets` | 查询提示词片段，可选 `query` 筛选 |\n| `POST` | `\u002Fapi\u002Fprompt-snippets` | 创建提示词片段 |\n| `PATCH` | `\u002Fapi\u002Fprompt-snippets\u002F{snippet_id}` | 更新提示词片段标题、内容或收藏标记 |\n| `DELETE` | `\u002Fapi\u002Fprompt-snippets\u002F{snippet_id}` | 删除提示词片段 |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fpresets` | 新建并激活 API 预设 |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}\u002Factivate` | 激活 API 预设 |\n| `POST` | `\u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}\u002Fhealth` | 校验已保存 API 预设并执行低成本上游探测 |\n| `DELETE` | `\u002Fapi\u002Fsettings\u002Fpresets\u002F{preset_id}` | 删除 API 预设 |\n| `POST` | `\u002Fapi\u002Fgenerate` | 创建图像生成任务 |\n| `POST` | `\u002Fapi\u002Fedits` | 使用一张或多张 multipart 上传图片创建图像编辑任务 |\n| `POST` | `\u002Fapi\u002Fedits\u002Ffrom-gallery\u002F{image_id}` | 使用已有 Gallery 图片创建图像编辑任务，可附加上传参考图 |\n| `GET` | `\u002Fapi\u002Fgenerate\u002Fjobs` | 查询排队\u002F运行中的生成和编辑任务；传 `include_finished=true` 并可选 `limit`\u002F`offset` 可分页查询持久化历史，传 `failed_only=true` 仅返回 `error`\u002F`upstream_error` 历史行 |\n| `GET` | `\u002Fapi\u002Fgenerate\u002Fjobs\u002Fevents` | 通过 SSE 推送排队\u002F运行中的生成和编辑任务 |\n| `DELETE` | `\u002Fapi\u002Fgenerate\u002Fjobs\u002Fhistory` | 删除全部已结束的生成\u002F编辑任务历史记录；保留排队\u002F运行任务和 Gallery 图片 |\n| `GET` | `\u002Fapi\u002Fgenerate\u002F{job_id}` | 查询任务状态或结果 |\n| `GET` | `\u002Fapi\u002Fgenerate\u002F{job_id}\u002Fevents` | 通过 SSE 推送单个任务状态和进度 |\n| `DELETE` | `\u002Fapi\u002Fgenerate\u002F{job_id}` | 取消并移除排队\u002F运行中的生成或编辑任务 |\n| `GET` | `\u002Fapi\u002Fgallery` | 分页查询 Gallery 图片，可选 `prompt`、`model`、`preset`、`size`、`date_from`、`date_to`、`favorite`、`include_total_bytes` 筛选 |\n| `PATCH` | `\u002Fapi\u002Fgallery\u002F{id}\u002Ffavorite` | 设置或取消 Gallery 收藏标记 |\n| `GET` | `\u002Fapi\u002Fgallery\u002F{image_id}` | 按 ID 获取单个 Gallery 条目 |\n| `GET` | `\u002Fapi\u002Fimage\u002F{filename}` | 访问图片文件 |\n| `GET` | `\u002Fapi\u002Fthumb\u002F{filename}` | 访问或懒生成 WebP Gallery 缩略图 |\n| `GET` | `\u002Fapi\u002Fdownload\u002F{filename}` | 下载图片 |\n| `DELETE` | `\u002Fapi\u002Fgallery\u002F{id}` | 删除 Gallery 条目和对应服务器图片文件 |\n| `GET` | `\u002Fapi\u002Fdownload-all` | 下载 Gallery 所有图片和 `metadata.json` 为 ZIP 文件 |\n| `POST` | `\u002Fapi\u002Fgallery\u002Fexport-jobs` | 创建可跟踪进度的 Gallery ZIP 导出任务；可传 `ids` 导出所选图片 |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fexport-jobs\u002F{job_id}` | 查询 ZIP 导出任务状态 |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fexport-jobs\u002F{job_id}\u002Fevents` | 通过 SSE 推送 ZIP 打包进度 |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fexport-jobs\u002F{job_id}\u002Fdownload` | 下载已完成的 ZIP 导出，并通过 `Content-Length` 支持传输进度 |\n| `POST` | `\u002Fapi\u002Fgallery\u002Fsync-jobs` | 创建单活 R2 Gallery 备份同步任务 |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fsync-jobs\u002F{job_id}` | 查询 R2 同步任务状态 |\n| `GET` | `\u002Fapi\u002Fgallery\u002Fsync-jobs\u002F{job_id}\u002Fevents` | 通过 SSE 推送 R2 同步进度 |\n| `POST` | `\u002Fapi\u002Fimport` | 导入 `\u002Fapi\u002Fdownload-all` 创建的 ZIP |\n| `DELETE` | `\u002Fapi\u002Fgallery` | 删除所有 Gallery 条目和服务器图片文件 |\n| `GET` | `\u002Fapi\u002Fmetrics` | 可选指标快照；默认 JSON，带 `Accept: text\u002Fplain` 时返回 Prometheus exposition format；仅在 `ENABLE_METRICS=true` 时可用 |\n| `GET` | `\u002Fapi\u002Fmetrics\u002Fprometheus` | 可选 Prometheus exposition metrics；仅在 `ENABLE_METRICS=true` 时可用 |\n\n## 运行时注意事项\n\n- 版本读取顺序是 `APP_VERSION` -> `VERSION`；本地版本和可选 GitHub 远端检查都会在每次 Web 端触发版本请求时实时计算。远端检查会先读 latest release，再回退到配置分支的 `VERSION`，仅用于显示 `New`，不会阻塞使用。\n- 配置 `PUBLIC_ORIGIN` 或 `ALLOWED_HOSTS` 后，未知 `Host` 或受信任 `X-Forwarded-Host` 会在 CSRF 检查前被拒绝；`Sec-Fetch-Site: same-origin` 不能绕过 Host 白名单\n- 预设、提示词收藏夹和 Gallery\u002FJob 数据只保存在 `DATABASE_FILE`\n- SQLite 仓储操作使用短连接，并在启动时启用 WAL；`DATA_DIR` 会 chmod 为 `0700`，SQLite 数据库和 sidecar 文件会 chmod 为 `0600`；应用 shutdown 和测试 reset 会调用 storage close hook，连接生命周期保持显式\n- 生成与编辑共用按 image units 计量的队列（`MAX_ACTIVE_GENERATE_JOBS` + `MAX_QUEUED_GENERATE_JOBS`）；所有编辑源图先落到 `DATA_DIR\u002Fedit-sources` 并额外受 `MAX_PENDING_EDIT_SOURCE_MB` 总量限制；支持取消，并持久化终态历史（含 `completed_at`）\n- 批量生成（`n > 1`）会占用 `n` 个队列单位；父任务会把成功子结果聚合到 `images[]`，Gallery 元数据保留用户请求的 `n`，真实上游子调用受全局 upstream-request semaphore 限制\n- 提示词优化器使用独立的服务端 Chat Completions 兼容 endpoint 配置和用户可配置请求超时\u002F响应体积上限，在后端解析 API Key 环境变量引用，不占用生成\u002F编辑任务队列容量。\n- R2 Backup 通过 boto3 访问 Cloudflare R2 S3 兼容 endpoint，并用 `asyncio.to_thread` 隔离阻塞调用；同步任务只列出配置的 prefix，只上传缺失的本地 Gallery filename，不会从 R2 服务、覆盖或删除 Gallery 图片。\n- SSE 是主进度通道；`\u002Fapi\u002Fgenerate\u002Fjobs` 提供列表\u002F历史（`include_finished=true`，可选 `limit`\u002F`offset`，可选 `failed_only=true`），`\u002Fapi\u002Fgenerate\u002Fjobs\u002Fhistory` 清空终态历史，`\u002Fapi\u002Fgenerate\u002Fjobs\u002Fevents` 从内存推送 debounce 后的实时任务列表变化\n- 任务终态历史包含 `stage_timings`：`upstream_wait`、`download_decode`、`validate`、`thumbnail`、`db_insert`；慢 Gallery 查询日志只记录提示词是否存在\u002F长度\u002Fhash，以及其他筛选条件与 total，并计入 metrics；可选 metrics 包含队列深度、运行中任务数、失败率、任务分段耗时和慢 SQLite 查询数；终态状态区分 `cancelled`、`interrupted` 和 `upstream_error`，同时保留通用 `error`\n- 上游 JSON\u002FSSE 响应会在解析前受 `MAX_UPSTREAM_JSON_MB` 限制，JSON 请求体受 `MAX_JSON_BODY_MB` 限制；上游图片 URL 下载会做 SSRF\u002F重定向目标复核，并会经过 Pillow 完整解码、`MAX_IMAGE_PIXELS` 像素限制和 `MAX_FILE_SIZE_MB` 体积限制\n- `\u002Fapi\u002Fimport` 做 ZIP 安全与体积校验；`\u002Fapi\u002Fdownload-all` 保留低内存流式导出，带进度的导出任务会写入临时 ZIP，让 UI 同时展示打包和传输进度\n- Gallery 持久化图片字节数和缩略图（`THUMBNAILS_DIR`），旧图按需懒补缩略图\n- 启动时会清理缺失文件对应的 Gallery 记录，并把上次进程遗留的 running\u002Fqueued 任务标记为 interrupted\n\n## 测试\n\n```bash\nnpm run frontend:check\nnpm run frontend:build\npython3 -m pytest backend\u002Ftests\u002Ftest_contract.py -q\nnpm run test:e2e\nRUN_PERFORMANCE_TESTS=true python3 -m pytest backend\u002Ftests\u002Ftest_performance.py -q\nnpm run test:e2e:perf\n```\n\n契约测试覆盖冻结的公共 API 表面，包括访问 cookie、settings、generation\u002Fedit 任务创建、任务耗时指标、SSE 响应 framing、Gallery import\u002Fexport、前端入口\u002FCSP 处理、静态资源访问、下载、422 校验错误和 500 错误形状。Playwright 覆盖访问门禁、设置抽屉焦点行为、mock 生成\u002F编辑流程、Gallery 筛选\u002F页码跳转\u002F批量操作、toast live region 和 Lightbox 键盘关闭\u002F导航。\n\n## 贡献\n\n欢迎贡献。\n\n建议遵循以下原则：\n\n- 后端修改尽量保持简单和明确\n- 用户可见变更或值得发布的修复应同步更新 `VERSION`，格式为 `vMAJOR.MINOR.PATCH`\n- 尽量使用 `backend\u002Fapp\u002Fschemas\u002F` 中的 FastAPI 响应模型\n- 持久化存储操作集中在 `backend\u002Fapp\u002Frepositories\u002F`\n- 上游 API 调用集中在 `backend\u002Fapp\u002Fintegrations\u002F`\n- 浏览器请求保持同源 `\u002Fapi\u002F*` 路径，不要引入前端直连跨域后端\n- 不要在仓库文件中保存真实 API Key\n- 除非明确要求，否则不要提交生成图片或运行时 Gallery 元数据\n- 除非明确要求改变任务生命周期，否则保留现有异步生成与 SSE 进度机制\n\n## 许可证\n\n本项目采用 `CC BY-NC 4.0` 许可证，即 `Creative Commons Attribution-NonCommercial 4.0 International`。\n\n- 允许任何人使用、复制、修改、再分发以及二次创作。\n- 需要保留署名，并附带许可证说明。\n- 不允许将本项目或其衍生作品用于商业用途。\n- 如需商业使用，必须事先获得著作权人的许可。\n\n许可证全文见 [LICENSE](.\u002FLICENSE)。\n","GPT Image Panel 是一个自托管的网页面板，用于生成、编辑和管理基于GPT兼容API的图像。该项目采用FastAPI作为后端框架，SvelteKit与TypeScript构建前端界面，支持Docker部署，使用SQLite进行数据存储。核心功能包括API预设管理、提示优化、多图像编辑以及作业历史记录等，旨在为用户提供一站式的图像处理解决方案。适用于需要在本地环境中对AI生成的图像进行定制化管理和编辑的场景，如创意设计、内容创作等领域。",2,"2026-06-11 03:58:48","CREATED_QUERY"]