+

+
image2api
**多供应商 AI 生图 / 生视频网关 —— 一套 OpenAI 兼容 API,聚合七大平台,开箱即用的运营系统**
@@ -14,7 +16,7 @@
[](https://vuejs.org)
[](#-部署)
[](#-openai-兼容-api)
-[](#方式一docker-一键推荐)
+[](#-部署)
[](#-支持的模型--供应商)
[](#-部署)
[](#-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` 时用自签证书(本地测试)。
-
-
-方式二:手动安装 — 自建 PostgreSQL / Redis / RustFS / Nginx(点击展开)
-
-
-
-自备 **PostgreSQL · Redis · RustFS(或任意 S3)· Nginx**,后端用 **Go 1.26+**,前端用 **Node 18+**。
+也可**从源码手动构建**,自备 **PostgreSQL · Redis · RustFS(或任意 S3)· 反向代理**:
```bash
# 1. 创建空库(后端启动自动建表)
@@ -216,15 +202,13 @@ server {
> 完整环境变量见 `backend/.env.example`。
-
-
## 🧱 技术栈
| 层 | 技术 |
|---|---|
| 后端 | 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
diff --git a/docker-compose.yml b/docker-compose.yml
index 32a42a0..e063a48 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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://
/ (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://:${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:
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 43263fd..e5392f2 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -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
diff --git a/frontend/default.conf.template b/frontend/default.conf.template
index 397523f..c5e7a55 100644
--- a/frontend/default.conf.template
+++ b/frontend/default.conf.template
@@ -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;
diff --git a/frontend/docker-entrypoint.d/10-selfsigned.sh b/frontend/docker-entrypoint.d/10-selfsigned.sh
deleted file mode 100644
index 4e2540e..0000000
--- a/frontend/docker-entrypoint.d/10-selfsigned.sh
+++ /dev/null
@@ -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
diff --git a/frontend/docker-entrypoint.d/30-cert-watch.sh b/frontend/docker-entrypoint.d/30-cert-watch.sh
deleted file mode 100644
index ebed017..0000000
--- a/frontend/docker-entrypoint.d/30-cert-watch.sh
+++ /dev/null
@@ -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
-) &
diff --git a/frontend/src/views/DocsView.vue b/frontend/src/views/DocsView.vue
index 844c361..ca6126c 100644
--- a/frontend/src/views/DocsView.vue
+++ b/frontend/src/views/DocsView.vue
@@ -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 该传什么
+ 图像分辨率对照表 · size 该传什么
左边选比例,上面选分辨率档,交叉格里就是 size 要传的值(直接复制)。
- 没有 quality 参数,分辨率只看 size 的长边。
+ 没有 quality 参数,图像分辨率只看 size 的长边。
档位必须是该模型支持的(见上方「可用模型」的分辨率列),不支持会自动回退到该模型最低档。
@@ -359,6 +371,33 @@ async function copy(text) {
例:想要
2K 的 16:9 横图 →
"size": "2048x1152"。
留空 size = 默认
1:1 · 2K。
+
+
+
视频分辨率对照表 · size 该传什么
+
+ 视频用 720p / 1080p 两档,只看 size 的短边(短边 ≥1080 = 1080p,否则 720p)。
+ 档位必须是该视频模型支持的(如 grok-video 仅 720p),不支持会被拒。
+
+
+
+
+ | 比例 |
+ 720p |
+ 1080p |
+
+
+
+ | {{ row.ratio }} |
+ {{ row.p720 }} |
+ {{ row.p1080 }} |
+
+
+
+
+
+ 例:想要 720p 的 16:9 横版视频 → "size": "1280x720";
+ 竖版 9:16 → "720x1280"。
+
diff --git a/install.sh b/install.sh
deleted file mode 100644
index 9723684..0000000
--- a/install.sh
+++ /dev/null
@@ -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"