docker: 前端 nginx HTTP :2000(外部反代接管域名/TLS,不再内置证书);README + logo

- docker-compose.yml:去 acme,web 端口 2000(WEB_PORT),CORS_ORIGINS/COOKIE_SECURE 直配,清理证书 volumes
- frontend nginx 模板/Dockerfile:纯 HTTP :2000,删自签证书 + cert-watch 入口脚本
- 删除 install.sh、.env.docker.example(compose 直接 up,不再需要一键脚本/模板)
- README(中英):部署改为 Docker(2000端口反代)+ 源码两法;去 acme/自动 HTTPS 表述;结构/徽章同步;顶部加 Vivid logo(favicon.svg);视频 720p/1080p 分辨率对照表(DocsView)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:58:59 +08:00
co-authored by Claude Opus 4.8
parent 4cd5454a27
commit 6acc428e74
10 changed files with 89 additions and 270 deletions
-28
View File
@@ -1,28 +0,0 @@
# Copy to .env (next to docker-compose.yml), edit, then:
# docker compose up -d --build
# Domain nginx binds (server_name) and acme.sh issues the cert for. Must resolve
# to this host with ports 80 + 443 reachable for the Let's Encrypt http-01 check.
# Use "localhost" for local testing — then ACME is skipped and a self-signed cert
# is used (443 still works, browser will warn).
DOMAIN=vividai.run
# Email for the Let's Encrypt / ACME account (renewal notices).
ACME_EMAIL=admin@example.com
# Host ports nginx listens on (80 = ACME challenge + redirect, 443 = TLS).
HTTP_PORT=80
HTTPS_PORT=443
# Session cookie Secure flag — keep true when serving over HTTPS (the default).
COOKIE_SECURE=true
APP_TITLE=image2api
# --- Postgres ---
POSTGRES_PASSWORD=change-me-postgres
# --- S3 / object storage (RustFS) ---
S3_BUCKET=vivid-ai
S3_ACCESS_KEY=vividai
S3_SECRET_KEY=change-me-s3-secret
+11 -27
View File
@@ -1,5 +1,7 @@
<div align="center">
<img src="frontend/public/favicon.svg" width="84" alt="Vivid AI" />
<h1>image2api</h1>
**Multi-provider AI image / video generation gateway — one OpenAI-compatible API, seven platforms aggregated, a ready-to-run operations system**
@@ -14,7 +16,7 @@
[![Vue 3](https://img.shields.io/badge/Vue-3-42b883?logo=vuedotjs&logoColor=white)](https://vuejs.org)
[![Docker](https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white)](#-deployment)
[![OpenAI Compatible](https://img.shields.io/badge/OpenAI-compatible-412991?logo=openai&logoColor=white)](#-openai-compatible-api)
[![HTTPS](https://img.shields.io/badge/HTTPS-acme.sh%20auto--issue-success)](#option-1-docker-one-command-recommended)
[![HTTPS](https://img.shields.io/badge/HTTPS-your--proxy-lightgrey)](#-deployment)
[![Providers](https://img.shields.io/badge/providers-7-orange)](#-supported-models--providers)
[![Self-hosted](https://img.shields.io/badge/self--hosted-yes-success)](#-deployment)
[![License](https://img.shields.io/badge/license-MIT-blue)](#-license)
@@ -51,7 +53,7 @@ It's more than an API proxy: it ships with **credit billing, CDK top-ups, referr
> 💡 Both frontend and backend are **fully open-source** (MIT) — Go + Vue 3, free to fork and self-host.
**At a glance** 🔌 OpenAI-compatible · 🤖 7 platforms, 10+ models · 🔁 auto failover / token keep-alive · 💳 credits + agent pricing · 🎨 generation frontend + admin console · 🐳 one-command deploy + auto HTTPS
**At a glance** 🔌 OpenAI-compatible · 🤖 7 platforms, 10+ models · 🔁 auto failover / token keep-alive · 💳 credits + agent pricing · 🎨 generation frontend + admin console · 🐳 one-command deploy (bring your own TLS proxy)
## 🖼️ Screenshots
@@ -100,7 +102,7 @@ It's more than an API proxy: it ships with **credit billing, CDK top-ups, referr
- Overview dashboard (trends / DAU / top failures / top spenders)
- Model management (normal + agent price) · account management (bulk import / dedup / quota) · **concurrency groups** · **order management** (filter / search / paginate) · site-wide logs · user management (set as agent / assign concurrency group / view cumulative top-up) · CDK · showcase · **announcements** · site config (incl. epay)
**🧰 Engineering highlights**: tls-client (Chrome JA3/JA4 fingerprint) reliably passes Cloudflare · media stored in S3/RustFS, served through an authenticated proxy with retention cleanup · self-healing maintenance loop (quota recovery / credential refresh / orphan-job cleanup with refunds) · one-command Docker deploy with acme.sh auto HTTPS.
**🧰 Engineering highlights**: tls-client (Chrome JA3/JA4 fingerprint) reliably passes Cloudflare · media stored in S3/RustFS, served through an authenticated proxy with retention cleanup · self-healing maintenance loop (quota recovery / credential refresh / orphan-job cleanup with refunds) · one-command Docker deploy (TLS via your own reverse proxy).
## 🤖 Supported Models / Providers
@@ -139,27 +141,11 @@ Images return OpenAI-style `{ "created": ..., "data": [{ "b64_json": "..." }] }`
## 🚀 Deployment
Both frontend and backend are open-source. Docker one-command is recommended; you can also build from source with **Go 1.26+**.
> Domain + HTTPS are handled by your own reverse proxy (this project issues no certificates).
> Prerequisite: a domain A-record pointing to this host, with **ports 80 / 443 open to the internet** (required for Let's Encrypt verification).
**Docker (recommended)**: `docker compose up -d --build` brings up PostgreSQL + Redis + RustFS + backend + frontend (nginx serving **HTTP on container port 2000**); point your reverse proxy at `http://<host>:2000` (port via `WEB_PORT`; set passwords / keys / `CORS_ORIGINS` in a `.env` beside the compose file, `COOKIE_SECURE=true` when your proxy serves HTTPS).
### Option 1: Docker, one command (recommended)
Requires Docker + Docker Compose. A single command brings up Postgres + Redis + RustFS + backend + frontend, and **auto-issues / renews the HTTPS certificate** (built-in acme.sh).
```bash
cp .env.docker.example .env # fill in DOMAIN / ACME_EMAIL / POSTGRES_PASSWORD / S3_SECRET_KEY
sh install.sh # = docker compose up -d --build
```
Open `https://<your-domain>/`; watch cert progress with `docker compose logs -f acme`. With `DOMAIN=localhost` a self-signed cert is used (local testing).
<details>
<summary><b>Option 2: Manual install</b> — bring your own PostgreSQL / Redis / RustFS / Nginx (click to expand)</summary>
<br/>
Provide your own **PostgreSQL · Redis · RustFS (or any S3) · Nginx**, **Go 1.26+** for the backend and **Node 18+** for the frontend.
Or **build from source** — bring your own **PostgreSQL · Redis · RustFS (or any S3) · reverse proxy**:
```bash
# 1. Create an empty database (the backend auto-migrates on start)
@@ -216,7 +202,7 @@ server {
|---|---|
| Backend | Go · gin · gorm (PostgreSQL) · go-redis · tls-client (Chrome fingerprint) |
| Frontend | Vue 3 · Vue Router · Vite · Tailwind CSS v4 |
| Infrastructure | PostgreSQL · Redis · RustFS (S3-compatible) · Nginx · acme.sh |
| Infrastructure | PostgreSQL · Redis · RustFS (S3-compatible) · Nginx |
## 📦 Repository Layout
@@ -256,12 +242,10 @@ frontend/ Frontend source (Vue 3 + Vite)
│ ├── layouts/ Public / admin layouts
│ ├── utils/ Utility functions
│ └── api.js · auth.js … API client, auth, theme, credits, etc.
├── Dockerfile Nginx static hosting + cert watcher
├── Dockerfile Nginx static hosting (HTTP :2000) + API proxy
└── default.conf.template Nginx site template (reverse proxy + caching)
docker-compose.yml Docker orchestration (Postgres / Redis / RustFS / backend / frontend / acme)
install.sh One-command deploy script (= docker compose up -d --build)
.env.docker.example Deployment env-var template
docker-compose.yml Docker orchestration (Postgres / Redis / RustFS / backend / frontend)
```
## 🗺️ Roadmap
+12 -29
View File
@@ -1,5 +1,7 @@
<div align="center">
<img src="frontend/public/favicon.svg" width="84" alt="Vivid AI" />
<h1>image2api</h1>
**多供应商 AI 生图 / 生视频网关 —— 一套 OpenAI 兼容 API,聚合七大平台,开箱即用的运营系统**
@@ -14,7 +16,7 @@
[![Vue 3](https://img.shields.io/badge/Vue-3-42b883?logo=vuedotjs&logoColor=white)](https://vuejs.org)
[![Docker](https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white)](#-部署)
[![OpenAI Compatible](https://img.shields.io/badge/OpenAI-compatible-412991?logo=openai&logoColor=white)](#-openai-兼容-api)
[![HTTPS](https://img.shields.io/badge/HTTPS-acme.sh%20自动签发-success)](#方式一docker-一键推荐)
[![HTTPS](https://img.shields.io/badge/HTTPS-反代自理-lightgrey)](#-部署)
[![Providers](https://img.shields.io/badge/供应商-7%20平台-orange)](#-支持的模型--供应商)
[![Self-hosted](https://img.shields.io/badge/self--hosted-yes-success)](#-部署)
[![License](https://img.shields.io/badge/license-MIT-blue)](#-license)
@@ -51,7 +53,7 @@
> 💡 前后端**完全开源**(MIT),Go + Vue 3,可自由二开 / 自部署。
**一句话亮点** 🔌 OpenAI 兼容 · 🤖 7 平台十余模型 · 🔁 自动换号 / Token 保活 · 💳 积分 + 在线充值(易支付)+ 代理价 · 🧩 并发分组 · 🎨 画图前端 + 管理后台 · 🐳 一键部署 + 自动 HTTPS
**一句话亮点** 🔌 OpenAI 兼容 · 🤖 7 平台十余模型 · 🔁 自动换号 / Token 保活 · 💳 积分 + 在线充值(易支付)+ 代理价 · 🧩 并发分组 · 🎨 画图前端 + 管理后台 · 🐳 一键部署(TLS 反代自理)
## 🖼️ 界面预览
@@ -107,7 +109,7 @@
- 概览看板(趋势 / DAU / 失败 Top / 消费榜)
- 模型管理(普通价 + 代理价) · 账号管理(批量导入 / 去重 / 额度) · **并发分组** · **订单管理**(筛选 / 搜索 / 分页) · 全站日志 · 用户管理(设为代理 / 分配并发组 / 看累计充值) · CDK · 展示位 · **站点公告** · 站点配置(含易支付)
**🧰 工程亮点**:tls-client(Chrome JA3/JA4 指纹)稳定穿透 Cloudflare · 媒体存 S3/RustFS 经鉴权代理分发 + 保留期清理 · 自愈式维护轮询(恢复额度 / 刷新凭据 / 清理僵死任务并退款) · 一条命令 Docker 部署 + acme.sh 自动 HTTPS
**🧰 工程亮点**:tls-client(Chrome JA3/JA4 指纹)稳定穿透 Cloudflare · 媒体存 S3/RustFS 经鉴权代理分发 + 保留期清理 · 自愈式维护轮询(恢复额度 / 刷新凭据 / 清理僵死任务并退款) · 一条命令 Docker 部署(TLS 交给你的反代)
## 🤖 支持的模型 / 供应商
@@ -147,27 +149,11 @@ curl https://你的域名/v1/images/edits \
## 🚀 部署
前后端均开源。推荐 Docker 一键;也可用 **Go 1.26+** 从源码构建
> 域名 + HTTPS 由你自己的反向代理处理(本项目不内置证书签发)
> 前置:域名 A 记录指向本机,**80 / 443 对公网开放**(Let's Encrypt 验证需要)。
**Docker(推荐)**:`docker compose up -d --build` 一条命令拉起 PostgreSQL + Redis + RustFS + 后端 + 前端(nginx **HTTP 监听容器 2000 端口**),把你的反向代理指到 `http://<本机>:2000`(端口用 `WEB_PORT` 改;要自定义密码 / 密钥 / `CORS_ORIGINS` 就在 compose 同目录建 `.env` 配,反代走 HTTPS 时设 `COOKIE_SECURE=true`)。
### 方式一:Docker 一键(推荐)
需要 Docker + Docker Compose。一条命令拉起 Postgres + Redis + RustFS + 后端 + 前端,并**自动签发 / 续期 HTTPS 证书**(内置 acme.sh)。
```bash
cp .env.docker.example .env # 填 DOMAIN / ACME_EMAIL / POSTGRES_PASSWORD / S3_SECRET_KEY
sh install.sh # = docker compose up -d --build
```
打开 `https://<你的域名>/`;证书进度 `docker compose logs -f acme``DOMAIN=localhost` 时用自签证书(本地测试)。
<details>
<summary><b>方式二:手动安装</b> — 自建 PostgreSQL / Redis / RustFS / Nginx(点击展开)</summary>
<br/>
自备 **PostgreSQL · Redis · RustFS(或任意 S3)· Nginx**,后端用 **Go 1.26+**,前端用 **Node 18+**
也可**从源码手动构建**,自备 **PostgreSQL · Redis · RustFS(或任意 S3)· 反向代理**:
```bash
# 1. 创建空库(后端启动自动建表)
@@ -216,15 +202,13 @@ server {
> 完整环境变量见 `backend/.env.example`。
</details>
## 🧱 技术栈
| 层 | 技术 |
|---|---|
| 后端 | Go · gin · gorm(PostgreSQL)· go-redis · tls-client(Chrome 指纹) |
| 前端 | Vue 3 · Vue Router · Vite · Tailwind CSS v4 |
| 基础设施 | PostgreSQL · Redis · RustFS(S3 兼容)· Nginx · acme.sh |
| 基础设施 | PostgreSQL · Redis · RustFS(S3 兼容)· Nginx |
## 📦 仓库结构
@@ -264,12 +248,11 @@ frontend/ 前端源码(Vue 3 + Vite)
│ ├── layouts/ 公共 / 后台布局
│ ├── utils/ 工具函数
│ └── api.js · auth.js … 接口封装、鉴权、主题、积分等
├── Dockerfile Nginx 静态托管 + 证书监听
├── Dockerfile Nginx 静态托管(HTTP :2000)+ API 反代
└── default.conf.template Nginx 站点模板(反代 + 缓存策略)
docker-compose.yml Docker 编排(Postgres / Redis / RustFS / 后端 / 前端 / acme)
install.sh 一键部署脚本(= docker compose up -d --build)
.env.docker.example 部署环境变量模板
docker-compose.yml Docker 编排(Postgres / Redis / RustFS / 后端 / 前端)
.env.example → backend/.env 后端环境变量模板
```
## 🗺️ Roadmap
+13 -62
View File
@@ -1,9 +1,10 @@
# image2api — one-command stack: PostgreSQL + Redis + RustFS (S3) + backend
# (pre-built binary, closed-source — backend/bin/api ships with the repo) +
# frontend/nginx (open-source, built here).
# 1) cp .env.docker.example .env # 填 DOMAIN / ACME_EMAIL / 密码 / 密钥
# 2) docker compose up -d --build (或 sh install.sh)
# Then open https://<DOMAIN>/ (nginx serves the SPA, proxies the API, TLS via acme.sh).
# (pre-built binary) + frontend/nginx (built here, HTTP only on port 2000).
# TLS + domain are NOT handled here — put your own reverse proxy in front of the
# web container (host port ${WEB_PORT:-2000}) and terminate TLS there.
# 1) cp .env.docker.example .env # 填 密码 / 密钥 / CORS_ORIGINS
# 2) docker compose up -d --build
# Then point your reverse proxy at http://<host>:${WEB_PORT:-2000}.
services:
postgres:
image: postgres:16-alpine
@@ -77,8 +78,10 @@ services:
RUSTFS_BUCKET: ${S3_BUCKET:-vivid-ai}
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-vividai}
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-vividai-secret-change-me}
CORS_ORIGINS: https://${DOMAIN:-localhost}
COOKIE_SECURE: ${COOKIE_SECURE:-true}
# Your site origin(s), comma-separated (the URL your reverse proxy serves).
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:2000}
# Keep true only if your reverse proxy serves HTTPS; false for plain HTTP.
COOKIE_SECURE: ${COOKIE_SECURE:-false}
volumes:
- generated:/app/data/generated
depends_on:
@@ -98,68 +101,16 @@ services:
web:
build:
context: ./frontend
environment:
DOMAIN: ${DOMAIN:-localhost}
# Only substitute ${DOMAIN} in the nginx template — leave $host/$uri/etc.
NGINX_ENVSUBST_FILTER: DOMAIN
# nginx serves the SPA + proxies the API on container port 2000 (HTTP only).
# Point your own reverse proxy at host:${WEB_PORT:-2000}.
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- certs:/etc/nginx/certs
- acme_webroot:/var/www/acme
- "${WEB_PORT:-2000}:2000"
depends_on:
- backend
restart: unless-stopped
# acme.sh — issues + auto-renews the Let's Encrypt cert via http-01 (webroot
# shared with nginx) into the shared certs volume. nginx's cert-watch reloads
# it. Requires DOMAIN to resolve to this host and ports 80/443 reachable.
acme:
image: neilpang/acme.sh:latest
depends_on:
- web
environment:
DOMAIN: ${DOMAIN:-localhost}
ACME_EMAIL: ${ACME_EMAIL:-admin@example.com}
# Persist account + cert state here (a volume on /acme.sh would shadow the
# installed binary, so use a separate config-home).
LE_CONFIG_HOME: /acme-data
volumes:
- certs:/etc/nginx/certs
- acme_webroot:/var/www/acme
- acmedata:/acme-data
entrypoint: ["/bin/sh", "-c"]
command:
- |
set -e
D="$${DOMAIN:-localhost}"
# Skip ACME for non-public domains (localhost / bare IP) — the self-signed
# bootstrap cert keeps 443 working for local testing.
case "$$D" in
localhost|127.0.0.1|"") echo "acme: DOMAIN=$$D not public, skipping issuance"; exec tail -f /dev/null ;;
esac
mkdir -p /etc/nginx/certs/live/$$D /var/www/acme /acme-data
acme.sh --set-default-ca --server letsencrypt
acme.sh --register-account -m "$${ACME_EMAIL}" || true
# Retry the first issue until nginx :80 is reachable for the http-01 check.
i=0
while [ $$i -lt 10 ]; do
if acme.sh --issue -d "$$D" -w /var/www/acme --keylength ec-256; then break; fi
i=$$((i+1)); echo "acme: issue attempt $$i failed, retry in 30s"; sleep 30
done
acme.sh --install-cert -d "$$D" --ecc \
--key-file /etc/nginx/certs/live/$$D/privkey.pem \
--fullchain-file /etc/nginx/certs/live/$$D/fullchain.pem || true
# daemon: renew checks daily; renewals auto-reinstall to the paths above.
while true; do sleep 12h; acme.sh --cron || true; done
restart: unless-stopped
volumes:
pgdata:
redisdata:
rustfsdata:
generated:
certs:
acme_webroot:
acmedata:
+5 -10
View File
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Frontend is open-source: built from source inside the image, then served by
# nginx which also reverse-proxies the API and terminates TLS (certs from the
# acme.sh sidecar via a shared volume).
# nginx (HTTP only on :2000) which also reverse-proxies the API. TLS + domain are
# handled by the user's own reverse proxy in front of this container.
# ---- build stage ----
FROM node:22-alpine AS build
WORKDIR /app
@@ -12,12 +12,7 @@ RUN npm run build
# ---- serve stage ----
FROM nginx:1.27-alpine
# openssl: self-signed bootstrap cert. The nginx image substitutes ${DOMAIN} in
# /etc/nginx/templates/*.template at startup (limited to DOMAIN via the filter).
RUN apk add --no-cache openssl
COPY --from=build /app/dist /usr/share/nginx/html
COPY default.conf.template /etc/nginx/templates/default.conf.template
COPY docker-entrypoint.d/10-selfsigned.sh /docker-entrypoint.d/10-selfsigned.sh
COPY docker-entrypoint.d/30-cert-watch.sh /docker-entrypoint.d/30-cert-watch.sh
RUN chmod +x /docker-entrypoint.d/10-selfsigned.sh /docker-entrypoint.d/30-cert-watch.sh
EXPOSE 80 443
# Plain config (no envsubst / no ${DOMAIN}) — copied straight into conf.d.
COPY default.conf.template /etc/nginx/conf.d/default.conf
EXPOSE 2000
+7 -36
View File
@@ -1,43 +1,16 @@
# nginx for the docker stack. ${DOMAIN} is filled at container start (envsubst,
# limited to DOMAIN via NGINX_ENVSUBST_FILTER). Port 80 serves the ACME
# http-01 challenge and redirects everything else to HTTPS; port 443 serves the
# SPA + reverse-proxies the API. Certs come from the shared volume, issued/renewed
# by the acme.sh sidecar (a self-signed cert bootstraps 443 before the real one).
# ---- 80: ACME challenge + redirect to HTTPS ----
# nginx for the docker stack — HTTP only on :2000. TLS + domain are handled by
# the user's OWN reverse proxy placed in front of this container; this just serves
# the SPA and reverse-proxies the API to the backend.
server {
listen 80;
listen [::]:80;
server_name ${DOMAIN};
# acme.sh writes http-01 challenge files here (shared volume).
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
# ---- 443: the app ----
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ${DOMAIN};
ssl_certificate /etc/nginx/certs/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/${DOMAIN}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
listen 2000;
listen [::]:2000;
server_name _;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 50m;
# Proxy headers (inherited by all proxy_pass locations below).
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -61,12 +34,10 @@ server {
}
# ---- API / media / health -> backend ----
# 动态 API no-store,防止 CDN 缓存 GET 响应(如 managed-models)→ 改价不生效。
location ^~ /admin/api/ { proxy_pass http://backend:6666; add_header Cache-Control "no-store" always; }
location ^~ /images/ { proxy_pass http://backend:6666; }
location = /health { proxy_pass http://backend:6666; }
# /v1 is per-API-key authenticated — must NOT be cached by any CDN/proxy.
# /v1 is per-API-key authenticated — never cache.
location ^~ /v1/ {
proxy_pass http://backend:6666;
add_header Cache-Control "no-store" always;
@@ -1,14 +0,0 @@
#!/bin/sh
# Bootstrap a self-signed cert so nginx's 443 server block can start BEFORE
# acme.sh has issued the real certificate. acme.sh later overwrites these files
# in the shared volume; 30-cert-watch.sh reloads nginx when that happens.
set -e
D="${DOMAIN:-localhost}"
CERT_DIR="/etc/nginx/certs/live/$D"
mkdir -p "$CERT_DIR" /var/www/acme
if [ ! -s "$CERT_DIR/fullchain.pem" ] || [ ! -s "$CERT_DIR/privkey.pem" ]; then
echo "nginx: generating self-signed bootstrap cert for $D"
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout "$CERT_DIR/privkey.pem" -out "$CERT_DIR/fullchain.pem" \
-subj "/CN=$D" >/dev/null 2>&1
fi
@@ -1,20 +0,0 @@
#!/bin/sh
# Reload nginx whenever the certificate file changes — i.e. once acme.sh has
# issued/renewed the real cert into the shared volume, nginx picks it up within
# ~a minute without a container restart. Runs in the background so it doesn't
# block startup.
D="${DOMAIN:-localhost}"
CERT="/etc/nginx/certs/live/$D/fullchain.pem"
(
last=""
while true; do
sleep 60
cur="$(stat -c %Y "$CERT" 2>/dev/null || echo '')"
if [ -n "$cur" ] && [ "$cur" != "$last" ]; then
# Skip the very first observation (the self-signed bootstrap); only reload
# on a genuine change (acme.sh overwrote the cert).
[ -n "$last" ] && nginx -s reload 2>/dev/null || true
last="$cur"
fi
done
) &
+41 -2
View File
@@ -78,6 +78,18 @@ const sizeTable = [
{ ratio: '1:3 · 竖', k1: '512x1536', k2: '768x2304', k4: '1280x3840' },
]
// ---- 视频 size → 比例 × 分辨率(720p / 1080p)----
// 视频按「短边」判档:短边 <1080 → 720p,≥1080 → 1080p;宽高比映射比例。
const videoSizeTable = [
{ ratio: '16:9 · 横', p720: '1280x720', p1080: '1920x1080' },
{ ratio: '9:16 · 竖', p720: '720x1280', p1080: '1080x1920' },
{ ratio: '1:1 · 方', p720: '720x720', p1080: '1080x1080' },
{ ratio: '4:3 · 横', p720: '960x720', p1080: '1440x1080' },
{ ratio: '3:4 · 竖', p720: '720x960', p1080: '1080x1440' },
{ ratio: '3:2 · 横', p720: '1080x720', p1080: '1620x1080' },
{ ratio: '2:3 · 竖', p720: '720x1080', p1080: '1080x1620' },
]
// ---- examples (built in script so refs resolve correctly) ----
const examples = computed(() => [
{
@@ -331,10 +343,10 @@ async function copy(text) {
<!-- size 对照表(课时表) 解决"传错分辨率" -->
<section>
<h2 class="text-lg font-semibold mb-1">分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
<h2 class="text-lg font-semibold mb-1">图像分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
<p class="text-xs text-white/45 mb-3">
左边选比例,上面选分辨率档,交叉格里就是 <code class="text-white/70">size</code> 要传的值(直接复制)
没有 <code class="text-white/70">quality</code> 参数,分辨率只看 <code class="text-white/70">size</code> 长边
没有 <code class="text-white/70">quality</code> 参数,图像分辨率只看 <code class="text-white/70">size</code> <strong class="text-white/70">长边</strong>
档位必须是该模型支持的(见上方可用模型的分辨率列),不支持会自动回退到该模型最低档
</p>
<div class="card overflow-hidden">
@@ -359,6 +371,33 @@ async function copy(text) {
:想要 <strong class="text-white/70">2K 16:9 横图</strong> <code class="text-white/70">"size": "2048x1152"</code>
留空 size = 默认 <strong class="text-white/70">1:1 · 2K</strong>
</p>
<!-- 视频分辨率(720p / 1080p,按短边判) -->
<h2 class="text-lg font-semibold mb-1 mt-8">视频分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
<p class="text-xs text-white/45 mb-3">
视频用 <code class="text-white/70">720p</code> / <code class="text-white/70">1080p</code> 两档,只看 <code class="text-white/70">size</code> <strong class="text-white/70">短边</strong>(短边 1080 = 1080p,否则 720p)
档位必须是该视频模型支持的( grok-video 720p),不支持会被拒
</p>
<div class="card overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
<th class="px-4 py-2.5 font-medium">比例</th>
<th class="px-4 py-2.5 font-medium">720p</th>
<th class="px-4 py-2.5 font-medium">1080p</th>
</tr></thead>
<tbody>
<tr v-for="row in videoSizeTable" :key="row.ratio" class="border-b border-white/[0.04] last:border-0">
<td class="px-4 py-2.5 text-white/75">{{ row.ratio }}</td>
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p720 }}</td>
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p1080 }}</td>
</tr>
</tbody>
</table>
</div>
<p class="text-xs text-white/40 mt-2">
:想要 <strong class="text-white/70">720p 16:9 横版视频</strong> <code class="text-white/70">"size": "1280x720"</code>;
竖版 9:16 <code class="text-white/70">"720x1280"</code>
</p>
</section>
<!-- examples -->
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env sh
# image2api — one-command install (Docker). Run from the repo root:
# sh install.sh
# Brings up Postgres + Redis + RustFS + backend + frontend, and auto-issues a
# Let's Encrypt HTTPS certificate via the built-in acme.sh service.
set -e
cd "$(dirname "$0")"
# --- docker present? ---
if ! command -v docker >/dev/null 2>&1; then
echo "ERROR: 未安装 Docker。请先安装 Docker + Docker Compose。"
exit 1
fi
# --- env file ---
if [ ! -f .env ]; then
echo "==> 生成 .env(从 .env.docker.example),请按提示编辑后重跑"
cp .env.docker.example .env
echo
echo " 必填:DOMAIN(你的域名)、ACME_EMAIL(证书邮箱)、POSTGRES_PASSWORD、S3_SECRET_KEY"
echo " 编辑好后再次执行: sh install.sh"
exit 0
fi
# --- backend binary (closed-source, shipped prebuilt) ---
if [ ! -f backend/bin/api ]; then
echo "ERROR: 缺少 backend/bin/api(后端二进制)。"
echo " 请从 Releases 下载 linux/amd64 的 api 放到 backend/bin/api 后重试。"
exit 1
fi
chmod +x backend/bin/api 2>/dev/null || true
# --- up ---
echo "==> docker compose up -d --build"
docker compose up -d --build
DOMAIN_VAL="$(grep -E '^DOMAIN=' .env | head -1 | cut -d= -f2-)"
echo
echo "完成 ✅ 打开 https://${DOMAIN_VAL:-<你的域名>}/"
echo "证书签发进度: docker compose logs -f acme"
echo "后端日志: docker compose logs -f backend"
echo "停止: docker compose down"