Compare commits
4 Commits
ora_main
...
332d46cde7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
332d46cde7 | ||
|
|
ac3417a964 | ||
|
|
9c34e9619c | ||
|
|
f431b2e2ff |
5
.ci/Dockerfile
Normal file
5
.ci/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
WORKDIR /app
|
||||
COPY sub2api-linux /app/sub2api
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/sub2api"]
|
||||
13
.ci/README.md
Normal file
13
.ci/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# ai.puro.im CI artifacts
|
||||
|
||||
Drone CI (see `.drone.yml`) builds a statically-linked `sub2api-linux` binary and bakes it into this distroless image.
|
||||
|
||||
Host-side state (NOT in repo):
|
||||
- `/opt/sub2api/docker-compose.yml` — sub2api + sub2api-pg + sub2api-redis services + PG password
|
||||
- `/opt/sub2api/app-data/config.yaml` — wizard-generated runtime config
|
||||
- `/opt/sub2api/{pg-data,redis-data,app-data,logs}` — persistent volumes
|
||||
|
||||
Deploy flow:
|
||||
1. Drone builds frontend (pnpm) + backend (go, linux/amd64)
|
||||
2. CI copies `backend/sub2api-linux` + `.ci/Dockerfile` to `/opt/sub2api/`
|
||||
3. CI runs `docker compose up -d --build sub2api` — rebuilds only sub2api service, leaves PG/Redis untouched
|
||||
75
.drone.yml
Normal file
75
.drone.yml
Normal file
@@ -0,0 +1,75 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
trigger:
|
||||
branch: [main]
|
||||
event: [push]
|
||||
|
||||
steps:
|
||||
- name: build-frontend
|
||||
image: node:18-alpine
|
||||
commands:
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@10.33.0 --activate
|
||||
- cd frontend
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm run build
|
||||
volumes:
|
||||
- name: pnpm-store
|
||||
path: /root/.local/share/pnpm/store
|
||||
|
||||
- name: build-backend
|
||||
image: golang:1.23-alpine
|
||||
environment:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
GOTOOLCHAIN: auto
|
||||
GOFLAGS: "-buildvcs=false"
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- cd backend
|
||||
- go build -tags embed -ldflags='-s -w' -o sub2api-linux ./cmd/server
|
||||
volumes:
|
||||
- name: go-cache
|
||||
path: /root/.cache/go-build
|
||||
- name: go-mod
|
||||
path: /go/pkg/mod
|
||||
depends_on:
|
||||
- build-frontend
|
||||
|
||||
- name: deploy
|
||||
image: docker:cli
|
||||
commands:
|
||||
- cp backend/sub2api-linux /opt/sub2api/sub2api-linux
|
||||
- cp .ci/Dockerfile /opt/sub2api/Dockerfile
|
||||
- cd /opt/sub2api && docker compose up -d --build sub2api
|
||||
- sleep 8
|
||||
- docker ps --filter 'name=^sub2api$' --filter 'status=running' --format '{{.Names}}' | grep -qx sub2api
|
||||
- docker inspect sub2api --format='{{.State.Health.Status}} {{.State.Status}}' 2>/dev/null || true
|
||||
- echo "deploy ok"
|
||||
volumes:
|
||||
- name: docker-sock
|
||||
path: /var/run/docker.sock
|
||||
- name: opt-sub2api
|
||||
path: /opt/sub2api
|
||||
depends_on:
|
||||
- build-backend
|
||||
|
||||
volumes:
|
||||
- name: pnpm-store
|
||||
host:
|
||||
path: /opt/drone/cache/pnpm-store
|
||||
- name: go-cache
|
||||
host:
|
||||
path: /opt/drone/cache/go-build
|
||||
- name: go-mod
|
||||
host:
|
||||
path: /opt/drone/cache/go-mod
|
||||
- name: docker-sock
|
||||
host:
|
||||
path: /var/run/docker.sock
|
||||
- name: opt-sub2api
|
||||
host:
|
||||
path: /opt/sub2api
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -129,6 +129,8 @@ vite.config.js
|
||||
docs/*
|
||||
!docs/PAYMENT.md
|
||||
!docs/PAYMENT_CN.md
|
||||
!docs/superpowers/
|
||||
.superpowers/
|
||||
.serena/
|
||||
.codex/
|
||||
frontend/coverage/
|
||||
|
||||
541
VPS_DEPLOYMENT_NOTES.md
Normal file
541
VPS_DEPLOYMENT_NOTES.md
Normal file
@@ -0,0 +1,541 @@
|
||||
# Sub2API VPS 部署 + CI/CD 记录
|
||||
|
||||
> 2026-04-19 @ `ai.puro.im` · 217.216.32.230 · Ubuntu 22.04 x86_64
|
||||
|
||||
本文档对应 `LOCAL_SETUP_NOTES.md`(Mac 本地开发环境)的线上版本,覆盖:从零到站点可访问、接入 Codex CLI、建立 Drone CI 自动部署的完整流程。
|
||||
|
||||
---
|
||||
|
||||
## 概览
|
||||
|
||||
```
|
||||
外网
|
||||
│ HTTPS (Let's Encrypt, Caddy auto-TLS)
|
||||
▼
|
||||
ai.puro.im :443
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ Caddy │ host, /etc/caddy/conf.d/sub2api.conf
|
||||
│ reverse_ │ → 127.0.0.1:8081
|
||||
│ proxy │
|
||||
└──────┬──────┘
|
||||
│
|
||||
docker port map 127.0.0.1:8081 → 容器 8080
|
||||
│
|
||||
┌────────▼──────────────┐
|
||||
│ sub2api-net (compose) │
|
||||
│ sub2api (Go) │ /opt/sub2api/ (持久化)
|
||||
│ sub2api-pg (PG15) │ ├─ app-data/ (config, .installed, pricing, logs)
|
||||
│ sub2api-redis │ ├─ pg-data/
|
||||
│ (redis:7-alpine) │ └─ redis-data/
|
||||
└───────────────────────┘
|
||||
│ VPS 出口
|
||||
▼
|
||||
api.anthropic.com / chatgpt.com / api.openai.com
|
||||
```
|
||||
|
||||
### CI 流程
|
||||
|
||||
```
|
||||
Mac (本地仓库) ─ git push gitea main ─▶ Gitea (git.puro.im) ──webhook──▶ Drone (devops.puro.im)
|
||||
│
|
||||
┌───────────┴────────────┐
|
||||
│ build-frontend (pnpm) │
|
||||
│ build-backend (go) │
|
||||
│ deploy │
|
||||
│ - cp bin + Dockerfile│
|
||||
│ - docker compose up │
|
||||
│ --build sub2api │
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、VPS 环境
|
||||
|
||||
| 项 | 值 | 备注 |
|
||||
|---|---|---|
|
||||
| 主机 | 217.216.32.230(新加坡) | `ssh vps` |
|
||||
| OS | Ubuntu 22.04 LTS x86_64 | |
|
||||
| Caddy | host 安装,`/etc/caddy/conf.d/*.conf` auto-include | 已跑着 puro.im / git.puro.im / devops.puro.im / erp.puro.im / ... |
|
||||
| Docker | host 装 + `devops-net` 网络 | 各应用容器都在 devops-net 里 |
|
||||
| Gitea | `git.puro.im:2222` (SSH), 3000 (HTTP) | purovps/sub2api 仓库 |
|
||||
| Drone | `devops.puro.im`(OAuth via Gitea) | drone-server + drone-runner-docker |
|
||||
| Sub2API 专用 docker-compose | `sub2api-net` 独立 bridge 网络(**不加 devops-net**) | 隔离 PG/Redis 生命周期 |
|
||||
| DNS | Cloudflare **DNS only**(非代理模式) | `ai.puro.im` A 记录 → VPS IP |
|
||||
| 端口冲突 | 宿主 `:8080` 被 drone-server 占 | Sub2API 映射到宿主 `127.0.0.1:8081` |
|
||||
|
||||
---
|
||||
|
||||
## 二、初次部署流程(手工路径,首次跑通)
|
||||
|
||||
> CI 跑通后这段主要作为"首次引导 / 灾难恢复参考"。日常改代码走 CI(下一节)。
|
||||
|
||||
### 1. 本机交叉编译 linux/amd64 二进制
|
||||
|
||||
```bash
|
||||
cd /Users/mini/Work/dev/sub2api/backend
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -tags embed -ldflags='-s -w' -o sub2api-linux ./cmd/server
|
||||
# 产物 ~75MB, static ELF x86-64
|
||||
```
|
||||
|
||||
注意:本机 macOS arm64,VPS 是 x86_64,**必须交叉编译**。`-tags embed` 把前端 dist 打进二进制(配合先跑 `pnpm run build`)。
|
||||
|
||||
### 2. VPS 建目录 + scp 文件
|
||||
|
||||
```bash
|
||||
ssh vps "mkdir -p /opt/sub2api/{app-data,pg-data,redis-data}"
|
||||
|
||||
# 准备 deploy 文件(本地 staging 目录)
|
||||
cp backend/sub2api-linux /Users/mini/Work/dev/sub2api-deploy/
|
||||
# Dockerfile + docker-compose.yml 见下节内容
|
||||
|
||||
scp /Users/mini/Work/dev/sub2api-deploy/{sub2api-linux,Dockerfile,docker-compose.yml} \
|
||||
vps:/opt/sub2api/
|
||||
|
||||
# distroless:nonroot 需要 UID 65532 可写
|
||||
ssh vps "chown -R 65532:65532 /opt/sub2api/app-data"
|
||||
```
|
||||
|
||||
### 3. Dockerfile(`.ci/Dockerfile` 也是同一份)
|
||||
|
||||
```dockerfile
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
WORKDIR /app
|
||||
COPY sub2api-linux /app/sub2api
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/sub2api"]
|
||||
```
|
||||
|
||||
### 4. docker-compose.yml
|
||||
|
||||
关键点:
|
||||
- `sub2api` 监听 `127.0.0.1:8081:8080`(避开宿主 8080 被 drone 占)
|
||||
- `/app/data` 整个挂到 `./app-data`(含 config.yaml / install.lock / pricing / logs)
|
||||
- PG 密码通过 `POSTGRES_PASSWORD` env(不进仓库)
|
||||
- 独立 `sub2api-net` bridge,不混 devops-net
|
||||
|
||||
生产文件位置:`/opt/sub2api/docker-compose.yml`(**不签入 git**,含 PG 密码)。模板结构:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: sub2api-pg
|
||||
environment:
|
||||
POSTGRES_DB: sub2api
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: "<32-hex 强口令>"
|
||||
volumes: [./pg-data:/var/lib/postgresql/data]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d sub2api"]
|
||||
restart: unless-stopped
|
||||
networks: [sub2api-net]
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: sub2api-redis
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes: [./redis-data:/data]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
restart: unless-stopped
|
||||
networks: [sub2api-net]
|
||||
|
||||
sub2api:
|
||||
build: .
|
||||
image: sub2api:local
|
||||
container_name: sub2api
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
redis: { condition: service_healthy }
|
||||
volumes:
|
||||
- ./app-data:/app/data
|
||||
ports: ["127.0.0.1:8081:8080"]
|
||||
restart: unless-stopped
|
||||
networks: [sub2api-net]
|
||||
|
||||
networks:
|
||||
sub2api-net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### 5. 起 stack
|
||||
|
||||
```bash
|
||||
ssh vps "cd /opt/sub2api && docker compose up -d --build"
|
||||
# 等 PG/Redis healthy 后 sub2api 才会起(depends_on + healthcheck 保证)
|
||||
```
|
||||
|
||||
### 6. Caddy 反代
|
||||
|
||||
`/etc/caddy/conf.d/sub2api.conf`:
|
||||
|
||||
```caddy
|
||||
ai.puro.im {
|
||||
reverse_proxy 127.0.0.1:8081 {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
encode gzip zstd
|
||||
log {
|
||||
output file /var/log/caddy/ai.puro.im.log
|
||||
format json
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh vps "systemctl reload caddy"
|
||||
```
|
||||
|
||||
### 7. 首次访问 → Setup Wizard
|
||||
|
||||
**不预先写 `/opt/sub2api/app-data/config.yaml`**——让 sub2api 以 wizard 模式启动,自己生成 config。走:
|
||||
|
||||
1. 浏览器开 `https://ai.puro.im` → 自动进 Setup Wizard
|
||||
2. Step 1 数据库:host=`postgres` / port=`5432` / user=`postgres` / password=<compose 里那个> / dbname=`sub2api` / sslmode=`disable`
|
||||
3. Step 2 Redis:host=`redis` / port=`6379` / password 空 / db=`0`
|
||||
4. Step 3 Admin:邮箱 + ≥8 位密码(自己选好记下来)
|
||||
5. Step 4 提交 → wizard 写 `app-data/config.yaml` + `app-data/.installed` + DB 创建 admin → 进程自杀、compose 重启后以正常模式跑
|
||||
|
||||
### 8. 加 OpenAI OAuth 账号(账号池)
|
||||
|
||||
从本机 `~/.codex/auth.json` 抽 `tokens.refresh_token`,粘到:
|
||||
- 后台 → 账号管理 → 添加账号 → Platform: OpenAI / Type: OAuth → "粘贴 Refresh Token" → 验证
|
||||
- 弹出 ChatGPT 邮箱+Plan 即成功
|
||||
|
||||
### 9. 三件必调(否则 503 "no available OpenAI accounts")
|
||||
|
||||
见 [§四 · 坑 3](#坑-3-新建-oauth-账号一上来无法调度),后台 UI 里:
|
||||
- **开启调度开关**
|
||||
- **去掉 auto_pause_on_expired**
|
||||
- **清掉 expires_at**(或设远期)
|
||||
|
||||
---
|
||||
|
||||
## 三、CI/CD 流水线(Drone)
|
||||
|
||||
### 仓库
|
||||
|
||||
| 位置 | 用途 |
|
||||
|---|---|
|
||||
| GitHub: `Wei-Shaw/sub2api` | 上游,origin remote |
|
||||
| Gitea: `git.puro.im/purovps/sub2api` | 我们的 fork,**Drone trigger source**,remote 名 `gitea` |
|
||||
|
||||
本地开发流程:
|
||||
|
||||
```bash
|
||||
# 日常
|
||||
git checkout main
|
||||
# 改代码...
|
||||
git commit
|
||||
git push gitea main # 触发 CI
|
||||
# 可选同步上游
|
||||
git fetch origin
|
||||
git merge origin/main # 手动决定是否合上游
|
||||
```
|
||||
|
||||
### Drone 激活
|
||||
|
||||
首次激活:https://devops.puro.im → Sync → 点亮 `purovps/sub2api` → Settings → 开 **Trusted**(pipeline 要挂 host socket 和 `/opt/sub2api`)。
|
||||
|
||||
激活后 Gitea 会自动配 webhook(`POST https://devops.puro.im/hook`,事件 `push, pull_request, ...`)。
|
||||
|
||||
### `.drone.yml` 三段 pipeline
|
||||
|
||||
```yaml
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
trigger: { branch: [main], event: [push] }
|
||||
|
||||
steps:
|
||||
- name: build-frontend # node:18-alpine + pnpm 10.33
|
||||
# pnpm install --frozen-lockfile && pnpm run build
|
||||
# 产物 → backend/internal/web/dist/
|
||||
|
||||
- name: build-backend # golang:1.23-alpine + GOTOOLCHAIN=auto
|
||||
# 自动拉 Go 1.26.2 toolchain
|
||||
# CGO_ENABLED=0 GOOS=linux GOARCH=amd64 → sub2api-linux
|
||||
|
||||
- name: deploy # docker:cli
|
||||
# cp backend/sub2api-linux /opt/sub2api/sub2api-linux
|
||||
# cp .ci/Dockerfile /opt/sub2api/Dockerfile
|
||||
# cd /opt/sub2api && docker compose up -d --build sub2api
|
||||
# 验证 sub2api 容器 running
|
||||
```
|
||||
|
||||
host 挂载 3 类卷:
|
||||
- `/var/run/docker.sock` — 用宿主 docker daemon 做构建+重启
|
||||
- `/opt/sub2api` — 更新 binary/Dockerfile
|
||||
- `/opt/drone/cache/{pnpm-store,go-build,go-mod}` — 加速后续构建
|
||||
|
||||
### 首次构建时长
|
||||
|
||||
| Step | 首次 | 缓存命中后 |
|
||||
|---|---|---|
|
||||
| build-frontend | ~90s | ~30s |
|
||||
| build-backend | ~60s(+首次下 Go 1.26.2 toolchain) | ~40s |
|
||||
| deploy | ~20s | ~20s |
|
||||
| **合计** | **~3 min** | **~90s** |
|
||||
|
||||
### CI 不碰的内容(secrets / 数据)
|
||||
|
||||
| 文件/目录 | 位置 | 为什么 |
|
||||
|---|---|---|
|
||||
| `docker-compose.yml` | `/opt/sub2api/` | 含 PG 密码 |
|
||||
| `app-data/config.yaml` | `/opt/sub2api/app-data/` | JWT secret / 运行态 |
|
||||
| `pg-data/` | `/opt/sub2api/` | PG 数据 |
|
||||
| `redis-data/` | `/opt/sub2api/` | Redis 数据 |
|
||||
|
||||
CI 只:
|
||||
- 覆写 `/opt/sub2api/sub2api-linux`(二进制)
|
||||
- 覆写 `/opt/sub2api/Dockerfile`
|
||||
- `docker compose up -d --build sub2api` — 只重建 `sub2api` service(PG/Redis 不动)
|
||||
|
||||
### Skip CI
|
||||
|
||||
文档/无代码变动想跳过构建:commit 消息加 `[CI SKIP]`(Drone 官方约定)。
|
||||
|
||||
---
|
||||
|
||||
## 四、踩坑与解法
|
||||
|
||||
### 坑 1:Setup Wizard 默认写 `server.port: 443`
|
||||
|
||||
**现象**:Wizard 跑完 → 容器重启 → `127.0.0.1:8081` 无响应(app 启动了但监听在 container 内的 :443)。
|
||||
|
||||
**根因**:Wizard 默认把 `server.port` 写成 443(面向"容器直接暴露 HTTPS"的用法,不适合我们"容器内 8080 + Caddy 外 443"的模式)。
|
||||
|
||||
**解法**:
|
||||
```bash
|
||||
ssh vps "sed -i 's/port: 443/port: 8080/' /opt/sub2api/app-data/config.yaml && docker restart sub2api"
|
||||
```
|
||||
|
||||
后续 CI 不重写 config.yaml,这个修复是一次性的。
|
||||
|
||||
---
|
||||
|
||||
### 坑 2:distroless:nonroot 日志目录权限
|
||||
|
||||
**现象**:容器启动日志刷 `write error: can't open new logfile: open /app/data/logs/sub2api.log: permission denied`。
|
||||
|
||||
**根因**:`gcr.io/distroless/static-debian12:nonroot` 进程 UID=65532,但 `/opt/sub2api/app-data/logs` 宿主目录 owner=root。
|
||||
|
||||
**解法**:
|
||||
```bash
|
||||
ssh vps "chown -R 65532:65532 /opt/sub2api/app-data"
|
||||
```
|
||||
|
||||
一次性。CI 不碰 app-data,所以不复发。
|
||||
|
||||
---
|
||||
|
||||
### 坑 3:新建 OAuth 账号一上来"无法调度"
|
||||
|
||||
**现象**:refresh_token 粘好、验证通过、UI 显示"正常",curl `/responses` 报 `503 "Service temporarily unavailable"`,日志 `openai.account_select_failed: no available OpenAI accounts`。
|
||||
|
||||
**根因(三个叠加)**:
|
||||
1. UI 默认 `schedulable=false`
|
||||
2. Wizard/后端给 `expires_at` 写了 **access_token 的短期过期**(~7 分钟后),而 `auto_pause_on_expired=true` 默认开
|
||||
3. 每次 sub2api 重启 → `[AccountExpiry] Auto paused 1 expired accounts` → schedulable 被打回 false
|
||||
|
||||
**解法**(SQL 或 UI 都可):
|
||||
```sql
|
||||
UPDATE accounts
|
||||
SET schedulable=true, expires_at=NULL, auto_pause_on_expired=false
|
||||
WHERE id=<n>;
|
||||
```
|
||||
|
||||
**已存 memory**:`feedback_sub2api_account_pitfalls.md`。长期方案:修 fork 里的账号创建逻辑,OAuth 账号的 `expires_at` 应指 refresh_token 过期或订阅到期,不是 access_token。
|
||||
|
||||
---
|
||||
|
||||
### 坑 4:`run_mode: simple` 隐藏 SaaS 菜单
|
||||
|
||||
**现象**:为跳过 `INSUFFICIENT_BALANCE` 临时切到 `run_mode: simple`,重登后台发现**用户管理 / 分组管理 / 渠道管理 / 订阅管理 / 兑换码 / 优惠码**全消失。
|
||||
|
||||
**根因**:**设计行为**,不是 bug。`simple` 模式是给 "单用户/团队内部工具" 用,刻意隐藏 SaaS 管理面板;前端 `stores/auth.ts` 里 `isSimpleMode = computed(() => runMode === 'simple')` 控制路由可见性。
|
||||
|
||||
**解法**:
|
||||
- 如果需要管理 group/订阅/计费/兑换码 → 保持 `run_mode: standard`
|
||||
- `INSUFFICIENT_BALANCE` 的替代方案:给 admin 塞大额 balance 即可(见坑 5)
|
||||
|
||||
---
|
||||
|
||||
### 坑 5:`standard` 模式 + admin balance=0 → 403
|
||||
|
||||
**现象**:切回 `standard` 后 curl `/responses` 报 `INSUFFICIENT_BALANCE`。
|
||||
|
||||
**根因**:中间件 `api_key_auth.go:198` 检查 `apiKey.User.Balance <= 0` → 403。admin 默认余额为 0。
|
||||
|
||||
**解法**:
|
||||
```sql
|
||||
UPDATE users SET balance = 1000000000 WHERE id=1;
|
||||
```
|
||||
|
||||
10 亿够跑很久。接 iShare 后改用 iShare 订阅模式(sub2api 标记为 "subscription mode",走订阅限额而不查 balance)。
|
||||
|
||||
---
|
||||
|
||||
### 坑 6:Redis L2 cache stale
|
||||
|
||||
**现象**:跑 [坑 5] 的 SQL 之后、docker restart sub2api 之后,curl 仍报 `INSUFFICIENT_BALANCE`。
|
||||
|
||||
**根因**:Sub2API 的 API key 鉴权有两级缓存:
|
||||
- L1 进程内 LRU(TTL 15s)
|
||||
- L2 Redis(key `sub2api:apikey:*`,**TTL 300s**)
|
||||
|
||||
docker restart 只清 L1,L2 是独立 Redis 容器,保留着旧的 `balance=0` 缓存条目。
|
||||
|
||||
**解法**:
|
||||
```bash
|
||||
ssh vps "docker exec sub2api-redis redis-cli FLUSHDB && docker restart sub2api"
|
||||
```
|
||||
|
||||
**一般经验**:改 DB 里的 user / api_key / account 后需重启 sub2api **+** 清 Redis。只重启 sub2api 不够。
|
||||
|
||||
---
|
||||
|
||||
### 坑 7:`/setup/*` 路由按需注册
|
||||
|
||||
**现象**:首次手工写了 config.yaml 让容器跑起来,后来想走 Wizard 重新配置;前端进了 `/setup` 页,填完点"测试连接" → `Request failed with status code 404`。
|
||||
|
||||
**根因**:`cmd/server/main.go` 里只有 `NeedsSetup() == true` 分支(走 `runSetupServer()`)才 `setup.RegisterRoutes(r)`。config.yaml 已存在时走 `runMainServer()`,setup 路由不注册 → 前端 `/setup/test-db` → 404。
|
||||
|
||||
**解法**:让 `NeedsSetup()` 返回 true,即 `/app/data/config.yaml` **和** `/app/data/.installed` 都不存在。具体做法:
|
||||
```bash
|
||||
ssh vps "cd /opt/sub2api && mv app-data/config.yaml app-data/config.yaml.bak; \
|
||||
mv app-data/.installed app-data/.installed.bak 2>/dev/null; \
|
||||
docker restart sub2api"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、当前状态 snapshot(2026-04-19)
|
||||
|
||||
```
|
||||
Domain https://ai.puro.im (HTTP/2, Let's Encrypt via Caddy)
|
||||
VPS 217.216.32.230 (Singapore, Ubuntu 22.04)
|
||||
Image sub2api:local (Drone CI-built, distroless + static Go binary)
|
||||
|
||||
Containers on VPS:
|
||||
sub2api ← Drone 每次 push 重建
|
||||
sub2api-pg postgres:15 (devops-net 无关,独立 sub2api-net)
|
||||
sub2api-redis redis:7-alpine
|
||||
|
||||
Accounts:
|
||||
admin@puro.im (id=1, role=admin, balance=1e9)
|
||||
test_myopenai (OpenAI OAuth, group=test_codex, schedulable=true)
|
||||
|
||||
API keys:
|
||||
sk-d2132de2f0b4c1ab64ef7241a16d254cab483f1f8afd47ad4a89e39cf6e2345a
|
||||
(user=admin, group=test_codex)
|
||||
|
||||
Config:
|
||||
run_mode: standard
|
||||
server.port: 8080 (容器内)
|
||||
database.host: postgres (compose DNS)
|
||||
redis.host: redis (compose DNS)
|
||||
|
||||
CI:
|
||||
Drone job trigger = git push gitea main
|
||||
Gitea webhook → Drone webhook (active)
|
||||
最近一次 build: commit 9c34e961 ✓ (前端 ~90s + 后端 ~60s + deploy ~20s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、常用运维命令
|
||||
|
||||
### 日常
|
||||
```bash
|
||||
# 状态
|
||||
ssh vps "cd /opt/sub2api && docker compose ps"
|
||||
|
||||
# 日志
|
||||
ssh vps "docker logs sub2api --tail 100 -f"
|
||||
ssh vps "tail -f /var/log/caddy/ai.puro.im.log"
|
||||
|
||||
# 强制重启 sub2api(不动 PG/Redis)
|
||||
ssh vps "docker restart sub2api"
|
||||
|
||||
# 重载 Caddy
|
||||
ssh vps "systemctl reload caddy"
|
||||
|
||||
# 清 Redis 缓存(改 DB 后要做)
|
||||
ssh vps "docker exec sub2api-redis redis-cli FLUSHDB"
|
||||
```
|
||||
|
||||
### 触发 CI 重建
|
||||
```bash
|
||||
git commit --allow-empty -m "ci: rebuild" && git push gitea main
|
||||
# 跟 build 进度(可选)
|
||||
ssh vps "docker ps --filter 'name=^drone-'"
|
||||
```
|
||||
|
||||
### 数据备份
|
||||
```bash
|
||||
# PG dump
|
||||
ssh vps "docker exec sub2api-pg pg_dump -U postgres sub2api" \
|
||||
> ~/backups/sub2api-$(date +%Y%m%d).sql
|
||||
|
||||
# 整个 /opt/sub2api/
|
||||
ssh vps "tar -czf /tmp/sub2api-full-$(date +%Y%m%d).tgz -C /opt sub2api"
|
||||
scp vps:/tmp/sub2api-full-*.tgz ~/backups/
|
||||
```
|
||||
|
||||
### 查关键 DB 信息
|
||||
```sql
|
||||
-- 账号池
|
||||
SELECT id, name, platform, type, status, schedulable, expires_at FROM accounts;
|
||||
|
||||
-- API keys
|
||||
SELECT id, user_id, name, status, group_id, quota, quota_used FROM api_keys;
|
||||
|
||||
-- 用户 + 余额
|
||||
SELECT id, email, role, balance FROM users;
|
||||
|
||||
-- 今日用量
|
||||
SELECT model_name, SUM(total_tokens), SUM(total_cost)
|
||||
FROM usage_logs
|
||||
WHERE created_at > NOW() - INTERVAL '24 hours'
|
||||
GROUP BY model_name
|
||||
ORDER BY 2 DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、清理/从头重来
|
||||
|
||||
```bash
|
||||
# 停 + 删所有容器
|
||||
ssh vps "cd /opt/sub2api && docker compose down"
|
||||
|
||||
# 清数据(会彻底删 wizard 数据、用户、账号池!)
|
||||
ssh vps "rm -rf /opt/sub2api/{app-data,pg-data,redis-data}"
|
||||
ssh vps "mkdir -p /opt/sub2api/{app-data,pg-data,redis-data}"
|
||||
ssh vps "chown -R 65532:65532 /opt/sub2api/app-data"
|
||||
|
||||
# 重新起(走 Wizard)
|
||||
ssh vps "cd /opt/sub2api && docker compose up -d"
|
||||
|
||||
# Caddy 保留,不用动
|
||||
```
|
||||
|
||||
**不清数据的"纯重建"**(CI 跑一遍等价):
|
||||
```bash
|
||||
git commit --allow-empty -m "ci: rebuild" && git push gitea main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考
|
||||
|
||||
- 本地开发:`LOCAL_SETUP_NOTES.md`
|
||||
- CI 配置:`.drone.yml`、`.ci/Dockerfile`、`.ci/README.md`
|
||||
- 上游:https://github.com/Wei-Shaw/sub2api
|
||||
- Fork:https://git.puro.im/purovps/sub2api
|
||||
348
docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md
Normal file
348
docs/superpowers/specs/2026-04-19-puro-ai-landing-auth-design.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# PURO AI · Landing + Auth 重设计
|
||||
|
||||
> 2026-04-19 · 分支 `feat/design-landing-auth`
|
||||
|
||||
本文档是「PURO AI 公开页面重设计」的设计 spec:
|
||||
- **Stage 1(已完成 – brainstorm)**:信息架构 + 中文文案 + 风格方向 + 布局选型
|
||||
- **Stage 2(待执行 – user)**:拿本文档去 https://claude.ai/design 产出高保真视觉稿
|
||||
- **Stage 3(待执行 – Claude Code)**:把视觉稿翻译成 Vue 3 组件,对齐 Tailwind / router / i18n
|
||||
- **Stage 4(待执行 – CI)**:merge → Drone CI → ai.puro.im 实机验证
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| **公开品牌名** | **PURO AI** |
|
||||
| **内部代码名** | sub2api(Wei-Shaw/sub2api fork,不改) |
|
||||
| **域名** | https://ai.puro.im |
|
||||
| **现状** | 登录后是 Vue 3 + Tailwind 后台;无公开首页;登录页用浅色 `AuthLayout` |
|
||||
| **目标受众** | 个人开发者 / 小团队 — 已有 ChatGPT Plus / Claude Pro / Codex / Gemini 订阅,想程序化调用而不付 API 费率 |
|
||||
| **核心叙事** | "你的 AI 订阅,已经付过钱了"——把已付订阅复用为 API |
|
||||
|
||||
---
|
||||
|
||||
## 2. 风格方向
|
||||
|
||||
**暗黑科技**(Dark Tech)—— 对标 Linear / Vercel / Railway / Supabase / Cloudflare Workers。
|
||||
|
||||
### 配色(建议)
|
||||
| 角色 | 色值 | 用途 |
|
||||
|---|---|---|
|
||||
| 主底 | `#0a0e1a` ~ `#0f172a` | 页面背景,slate-950 区间 |
|
||||
| 卡片底 | `#0f172a` | 表单卡片、特性卡 |
|
||||
| 边框 | `#334155` | 次要边框 |
|
||||
| 主文 | `#f8fafc` | 标题 |
|
||||
| 副文 | `#94a3b8` ~ `#cbd5e1` | 描述、菜单 |
|
||||
| **主品牌色** | `#22d3ee`(cyan-400) | Logo、CTA、链接 |
|
||||
| **辅品牌色** | `#a855f7`(purple-500) | 渐变叠加、装饰光晕 |
|
||||
| 警示 | `#fbbf24`(amber-400) | "💡"标签、数字对比 |
|
||||
|
||||
### 视觉语汇
|
||||
- 暗底上**圆形 radial gradient 光晕**(青/紫双色)
|
||||
- 等宽字体(ui-monospace / SF Mono)用于 code demo
|
||||
- 主体字体:sans-serif(Inter / SF Pro / 系统默认)
|
||||
- 边框 1px 实线 / 关键分割用 dashed
|
||||
- CTA 圆角 8px;卡片圆角 12px
|
||||
- 不要拟物、不要软阴影、不要 Bootstrap 4 那种 gradient 按钮
|
||||
|
||||
### 排版氛围
|
||||
- 大量留白
|
||||
- 标题大、字距稍紧(letter-spacing -0.02em)
|
||||
- 内容居中收敛(max-width ~1100px)
|
||||
|
||||
---
|
||||
|
||||
## 3. Landing 页(路由 `/`,未登录态)
|
||||
|
||||
### 3.1 信息架构
|
||||
|
||||
```
|
||||
NAV · ⬢ PURO AI · 产品 · 文档 · [登录][免费试用]
|
||||
① HERO · 主标 + 副标 + CTA×2 + 微文案
|
||||
② 模型墙 · 4 个支持的 AI 平台(Claude / ChatGPT / Codex / Gemini)
|
||||
③ 三特性 · ⚡ 一个 key 多模型 · 🔄 账号池高可用 · 📊 用量看板
|
||||
④ Code Demo · codex config 片段 + curl 示例
|
||||
⑤ Dashboard · 真实截图 + 文案
|
||||
⑥ Footer · 4 列(品牌 / 产品 / 资源 / 联系)
|
||||
```
|
||||
|
||||
### 3.2 完整中文文案
|
||||
|
||||
#### NAV
|
||||
- Logo: `⬢ PURO AI`
|
||||
- 菜单: 产品 · 文档 · 定价(暂隐)
|
||||
- 右侧: `[登录]` `[免费试用 →]`
|
||||
|
||||
#### ① HERO
|
||||
- **主标**: 你的 AI 订阅,**已经付过钱了。**
|
||||
- **副标**: Claude Pro · ChatGPT Plus · Codex · Gemini 订阅<br>聚合成统一 API,零改动接入 OpenAI / Anthropic SDK
|
||||
- **CTA**: `立即开始 →` `查看文档`
|
||||
- **微文案**: 无需信用卡 · 用你已有的订阅 · 5 分钟跑通
|
||||
|
||||
#### ② 模型墙
|
||||
- **小标题**: 支持的 AI 平台
|
||||
- **副标**: 通过 OAuth 直接复用你的订阅,无需申请官方 API key
|
||||
- **Logos**:
|
||||
- ⚪ Claude Pro / Max
|
||||
- 🟢 ChatGPT Plus / Pro
|
||||
- 🟡 Codex CLI
|
||||
- 🔵 Gemini Code Assist
|
||||
- ⚫ 更多(规划中,灰显)
|
||||
|
||||
#### ③ 三大特性
|
||||
| 图标 | 标题 | 描述 |
|
||||
|---|---|---|
|
||||
| ⚡ | 一个 key 接所有模型 | 不再为每个 provider 申请 API key、配置 base_url。统一 sk- 走 Claude / GPT / Gemini,按 model 自动路由到对应账号池。|
|
||||
| 🔄 | 账号池高可用 | 多账号自动调度。某个 ChatGPT Plus 触发限流,自动 failover 到下一个。重启、刷新 token 全自动。|
|
||||
| 📊 | 用量看板 | 每条请求的 tokens、费用、上游账号、延迟全可视化。模型分布饼图 + 趋势曲线 + Top 排行。|
|
||||
|
||||
#### ④ Code Demo
|
||||
- **标题**: 把 base_url 一改,就能用
|
||||
- **副**: 兼容 OpenAI / Anthropic / Gemini SDK,**零代码改动**
|
||||
- **代码块**:
|
||||
```toml
|
||||
# Codex CLI
|
||||
# ~/.codex/config.toml
|
||||
[model_providers.OpenAI]
|
||||
base_url = "https://ai.puro.im"
|
||||
wire_api = "responses"
|
||||
```
|
||||
```bash
|
||||
# 或直接 curl
|
||||
$ curl https://ai.puro.im/responses \
|
||||
-H "Authorization: Bearer sk-xxx" \
|
||||
-d '{"model":"gpt-5.4","input":"hello"}'
|
||||
```
|
||||
- **底注**: 支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE & WebSocket
|
||||
|
||||
#### ⑤ Dashboard
|
||||
- **标题**: 每条请求都看得见
|
||||
- **副**: 不像第三方 API 池子那种"扣了多少不告诉你"。你能看到每次调用:扣了哪个账号、跑了哪个模型、用了多少 tokens、花了多少钱、上游响应几秒。
|
||||
- **图**: 截图占位 → 实施时取 ai.puro.im 实拍(模型分布饼图 + Token 趋势 + Top 模型 + 性能指标)
|
||||
|
||||
#### ⑥ Footer
|
||||
| 列 | 内容 |
|
||||
|---|---|
|
||||
| 品牌 | ⬢ PURO AI<br>Self-hosted on puro.im<br>© 2026 puro.im · MIT License<br>fork of Wei-Shaw/sub2api |
|
||||
| 产品 | 文档 · 套餐(暂隐)· 更新日志 |
|
||||
| 资源 | GitHub · API 状态 · Codex 配置示例 |
|
||||
| 联系 | admin@puro.im · git.puro.im |
|
||||
|
||||
---
|
||||
|
||||
## 4. Auth 页(登录 / 注册)
|
||||
|
||||
### 4.1 布局选型:左右分栏
|
||||
|
||||
```
|
||||
┌─────────────────────────┬─────────────────────┐
|
||||
│ 左:品牌叙事区 │ 右:表单区 │
|
||||
│ - Logo │ - 标题(登录/注册) │
|
||||
│ - 主标语(5→1 对比) │ - 副标 │
|
||||
│ - 副文(双卖点排比) │ - email / password │
|
||||
│ - 装饰光晕 cyan/purple │ - CTA │
|
||||
│ - 底栏小字(支持平台) │ - 切换链接 │
|
||||
└─────────────────────────┴─────────────────────┘
|
||||
```
|
||||
|
||||
移动端:左侧叙事降级为顶部小 banner 或完全隐藏,单列表单。
|
||||
|
||||
### 4.2 左侧叙事文案
|
||||
|
||||
- **Logo**: `⬢ PURO AI`
|
||||
- **主标语**:
|
||||
> **5** 个订阅<br>
|
||||
> → **1** 个 key
|
||||
|
||||
数字 `5` 用 amber/orange 强调;`1` 用主品牌色 cyan 强调。
|
||||
- **副文**(三句排比):
|
||||
> 省去切换账号的繁琐,<br>
|
||||
> 省去为多个高昂订阅重复买单。<br>
|
||||
> <small style="color:#64748b">PURO(纯粹)—— 让 AI 调用回归本质。</small>
|
||||
- **底栏小字**: `Claude · ChatGPT · Codex · Gemini`
|
||||
|
||||
### 4.3 右侧表单
|
||||
|
||||
#### 登录页(`/login`)
|
||||
- 标题: 登录
|
||||
- 副: 用你的 PURO AI 账户继续
|
||||
- 字段:
|
||||
- 📧 邮箱(input, type=email, required)
|
||||
- 🔒 密码(input, type=password, required, 带眼睛切换显示)
|
||||
- 选项:
|
||||
- 忘记密码?(router-link)
|
||||
- Turnstile captcha(条件显示)
|
||||
- CTA: `登录 →`
|
||||
- 分隔: `或`
|
||||
- OAuth 按钮(条件显示):
|
||||
- 使用 LinuxDO 登录
|
||||
- 使用 OIDC 登录
|
||||
- 底部链接: 没有账户?**注册**
|
||||
|
||||
#### 注册页(`/register`)
|
||||
- 标题: 创建账户
|
||||
- 副: 5 分钟开始用 PURO AI
|
||||
- 字段:
|
||||
- 📧 邮箱
|
||||
- 🔒 密码
|
||||
- 🔒 确认密码
|
||||
- (可选)邮箱验证码(条件显示,配置 `email_verify_required` 时)
|
||||
- Turnstile captcha(条件)
|
||||
- CTA: `创建账户 →`
|
||||
- 底部链接: 已有账户?**登录**
|
||||
|
||||
#### 其他保留页(不重设计本期)
|
||||
- `/forgot-password`
|
||||
- `/reset-password`
|
||||
- `/verify-email`
|
||||
- OAuth 回调页
|
||||
|
||||
---
|
||||
|
||||
## 5. 给 claude.ai/design 的 brief(Stage 2 输入)
|
||||
|
||||
复制下方文字到 https://claude.ai/design:
|
||||
|
||||
````
|
||||
我要做两个网页设计,请帮我生成高保真 HTML/React 视觉稿。
|
||||
|
||||
## 品牌
|
||||
名字:PURO AI(拉丁语「纯粹」)
|
||||
Logo:六边形 ⬢ + 文字
|
||||
域名:ai.puro.im
|
||||
定位:把多个 AI 订阅(Claude Pro / ChatGPT Plus / Codex / Gemini)聚合成统一 API
|
||||
核心叙事:你的 AI 订阅,已经付过钱了
|
||||
|
||||
## 风格
|
||||
暗黑科技风,对标 Linear / Vercel / Railway。
|
||||
配色:
|
||||
- 主底 #0a0e1a / #0f172a(slate-950 区间)
|
||||
- 主品牌色 #22d3ee(cyan-400)
|
||||
- 辅品牌色 #a855f7(purple-500)
|
||||
- 强调色 #fbbf24(amber-400,仅用于数字对比)
|
||||
- 主文 #f8fafc,副文 #94a3b8 ~ #cbd5e1
|
||||
- 卡片/表单底 #0f172a,边框 #334155
|
||||
|
||||
视觉元素:
|
||||
- 暗底上 radial gradient 光晕(青/紫双色,60% 透明度,blur)
|
||||
- 大量留白,max-width 1100px
|
||||
- 圆角:CTA 8px,卡片 12px
|
||||
- 字体:Inter 或 SF Pro(sans-serif),代码用 ui-monospace
|
||||
- 不要拟物、不要软阴影、不要 gradient 按钮
|
||||
|
||||
## 页面 1:Landing(路由 /,未登录)
|
||||
6 个 section + 顶部 nav,全部中文。
|
||||
|
||||
NAV
|
||||
- 左:⬢ PURO AI
|
||||
- 中:产品、文档(定价灰显)
|
||||
- 右:[登录](边框)[免费试用 →](cyan 实底)
|
||||
|
||||
① HERO(居中,垂直 padding 大)
|
||||
主标:你的 AI 订阅,**已经付过钱了。**
|
||||
("已经付过钱了" 用 cyan 高亮)
|
||||
副标:Claude Pro · ChatGPT Plus · Codex · Gemini 订阅
|
||||
聚合成统一 API,零改动接入 OpenAI / Anthropic SDK
|
||||
CTA:[立即开始 →][查看文档]
|
||||
微文案(小灰字):无需信用卡 · 用你已有的订阅 · 5 分钟跑通
|
||||
|
||||
② 模型墙
|
||||
小标题:支持的 AI 平台
|
||||
副:通过 OAuth 直接复用你的订阅,无需申请官方 API key
|
||||
横排 5 个 logo 卡片:Claude Pro/Max · ChatGPT Plus/Pro · Codex CLI · Gemini Code Assist · 更多(灰显)
|
||||
|
||||
③ 三特性(3 列卡片)
|
||||
卡片 1:⚡ 一个 key 接所有模型 / 不再为每个 provider 申请 API key、配置 base_url。统一 sk- 走 Claude / GPT / Gemini,按 model 自动路由到对应账号池。
|
||||
卡片 2:🔄 账号池高可用 / 多账号自动调度。某个 ChatGPT Plus 触发限流,自动 failover 到下一个。重启、刷新 token 全自动。
|
||||
卡片 3:📊 用量看板 / 每条请求的 tokens、费用、上游账号、延迟全可视化。模型分布饼图 + 趋势曲线 + Top 排行。
|
||||
|
||||
④ Code Demo
|
||||
标题:把 base_url 一改,就能用
|
||||
副:兼容 OpenAI / Anthropic / Gemini SDK,零代码改动
|
||||
代码块(深色 terminal 配色,syntax highlight):
|
||||
- 上方一段 toml(codex config)
|
||||
- 下方一段 bash(curl 示例)
|
||||
底注小字:支持 OpenAI Responses API · Anthropic Messages API · Gemini generateContent · 流式 SSE & WebSocket
|
||||
|
||||
⑤ Dashboard
|
||||
标题:每条请求都看得见
|
||||
副:不像第三方 API 池子那种"扣了多少不告诉你"。你能看到每次调用:扣了哪个账号、跑了哪个模型、用了多少 tokens、花了多少钱、上游响应几秒。
|
||||
图:dashboard 截图占位(深色饼图 + 折线图 + 表格)
|
||||
|
||||
⑥ Footer
|
||||
4 列:
|
||||
- 品牌列:⬢ PURO AI / Self-hosted on puro.im / © 2026 puro.im · MIT / fork of Wei-Shaw/sub2api
|
||||
- 产品列:文档 · 套餐(暂隐)· 更新日志
|
||||
- 资源列:GitHub · API 状态 · Codex 配置示例
|
||||
- 联系列:admin@puro.im · git.puro.im
|
||||
|
||||
## 页面 2:登录页(路由 /login)
|
||||
左右分栏,桌面端 50/50 分。
|
||||
|
||||
左侧(叙事区):
|
||||
- 顶部 Logo:⬢ PURO AI
|
||||
- 中部主标:5 个订阅 → 1 个 key
|
||||
(5 用 amber #fbbf24 强调,1 用 cyan #22d3ee 强调,字号 36-48px,weight 800)
|
||||
- 主标下副文(三句排比):
|
||||
省去切换账号的繁琐,
|
||||
省去为多个高昂订阅重复买单。
|
||||
PURO(纯粹)—— 让 AI 调用回归本质。
|
||||
- 底部小字:Claude · ChatGPT · Codex · Gemini
|
||||
- 背景:linear-gradient(135deg, #0a0e1a, #1e1b4b) + 角落 radial gradient 光晕(cyan + purple)
|
||||
|
||||
右侧(表单区):
|
||||
- 标题:登录
|
||||
- 副:用你的 PURO AI 账户继续
|
||||
- 邮箱输入(带 📧 icon)
|
||||
- 密码输入(带 🔒 icon + 眼睛切换显示)
|
||||
- 行:忘记密码?(右对齐链接)
|
||||
- 主 CTA:登录 →(cyan 实底)
|
||||
- 分隔:—— 或 ——
|
||||
- OAuth 按钮:使用 LinuxDO 登录(边框样式)
|
||||
- 底部:没有账户?注册(链接)
|
||||
|
||||
移动端:左侧叙事区收为顶部小 banner(只保留 Logo + 短主标),表单全宽。
|
||||
|
||||
## 页面 3:注册页(路由 /register)
|
||||
和登录页同布局,右侧表单字段:
|
||||
- 标题:创建账户 / 副:5 分钟开始用 PURO AI
|
||||
- 邮箱、密码、确认密码
|
||||
- 主 CTA:创建账户 →
|
||||
- 底部:已有账户?登录
|
||||
|
||||
请生成完整可预览的 HTML(含 inline CSS)或 React 组件。
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## 6. Stage 3 实施约束(给未来的我看)
|
||||
|
||||
技术栈(不改动):
|
||||
- Vue 3.4+ Composition API + TypeScript
|
||||
- Tailwind CSS(已配 dark mode、`primary-*` 色板)
|
||||
- Vite 5
|
||||
- Vue Router 4 / Pinia / vue-i18n
|
||||
- 现有组件库(`@/components/common`、`@/components/layout/AuthLayout`)
|
||||
|
||||
实施要点:
|
||||
1. **Landing 页是新页**:新建 `frontend/src/views/landing/HomeView.vue`;改 `router/index.ts`,未登录访问 `/` 显示 landing,已登录跳 `/dashboard`
|
||||
2. **Auth 页改造**:改 `frontend/src/components/layout/AuthLayout.vue` 为左右分栏;改 `LoginView.vue` / `RegisterView.vue` 适配新 layout,**保留所有现有逻辑**(OAuth、Turnstile、2FA、表单校验)
|
||||
3. **i18n**:新文案进 `frontend/src/i18n/locales/zh.ts`;本期只补中文(默认语言),英文 key 留空 / 复用现有
|
||||
4. **Tailwind 配色**:如果新色值不在 `tailwind.config` 里,按需扩展 `colors.brand` / `colors.accent`
|
||||
5. **Dashboard 截图**:实施时手动截 ai.puro.im 后台 → 放 `frontend/public/landing/dashboard.png`
|
||||
6. **不动的**:Setup Wizard / 后台所有页面 / API 层 / store
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
- [ ] 未登录访问 https://ai.puro.im/ → 看到 PURO AI Landing 页
|
||||
- [ ] 已登录访问 https://ai.puro.im/ → 跳到 /dashboard(保持现有行为)
|
||||
- [ ] Landing 6 个 section 内容全部呈现,移动端可堆叠
|
||||
- [ ] /login 是左右分栏布局,文案与 spec 一致
|
||||
- [ ] /register 同上
|
||||
- [ ] 所有现有 auth 功能(OAuth、Turnstile、2FA、密码重置)仍可用
|
||||
- [ ] 后台所有页面(dashboard、账号管理、API key 等)外观不变
|
||||
- [ ] CI 构建通过,部署后 ai.puro.im 加载正常
|
||||
Reference in New Issue
Block a user