chore: squash merge customizations from develop-old-0.1.77

- 定制文档: CLAUDE.md, AGENTS.md
- UI定制: 微信客服按钮, 首页改造, 移除GitHub链接
- 部署运维: docker-compose.yml, 压测脚本
- CI/gitignore 小改动
This commit is contained in:
liuxiongfeng
2026-02-10 20:59:54 +08:00
parent 7d0a30fa8f
commit c4d6715443
10 changed files with 1832 additions and 79 deletions

View File

@@ -17,6 +17,7 @@ jobs:
go-version-file: backend/go.mod
check-latest: false
cache: true
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.25.7'
@@ -36,6 +37,7 @@ jobs:
go-version-file: backend/go.mod
check-latest: false
cache: true
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.25.7'

1
.gitignore vendored
View File

@@ -78,6 +78,7 @@ Desktop.ini
# ===================
tmp/
temp/
logs/
*.tmp
*.temp
*.log

723
AGENTS.md Normal file
View File

@@ -0,0 +1,723 @@
# Sub2API 开发说明
## 版本管理策略
### 版本号规则
我们在官方版本号后面添加自己的小版本号:
- 官方版本:`v0.1.68`
- 我们的版本:`v0.1.68.1``v0.1.68.2`(递增)
### 分支策略
| 分支 | 说明 |
|------|------|
| `main` | 我们的主分支,包含所有定制功能 |
| `release/custom-X.Y.Z` | 基于官方 `vX.Y.Z` 的发布分支 |
| `upstream/main` | 上游官方仓库 |
---
## 发布流程(基于新官方版本)
当官方发布新版本(如 `v0.1.69`)时:
### 1. 同步上游并创建发布分支
```bash
# 获取上游最新代码
git fetch upstream --tags
# 基于官方标签创建新的发布分支
git checkout v0.1.69 -b release/custom-0.1.69
# 合并我们的 main 分支(包含所有定制功能)
git merge main --no-edit
# 解决可能的冲突后继续
```
### 2. 更新版本号并打标签
```bash
# 更新版本号文件
echo "0.1.69.1" > backend/cmd/server/VERSION
git add backend/cmd/server/VERSION
git commit -m "chore: bump version to 0.1.69.1"
# 打上我们自己的标签
git tag v0.1.69.1
# 推送分支和标签
git push origin release/custom-0.1.69
git push origin v0.1.69.1
```
### 3. 更新 main 分支
```bash
# 将发布分支合并回 main保持 main 包含最新定制功能
git checkout main
git merge release/custom-0.1.69
git push origin main
```
---
## 热修复发布(在现有版本上修复)
当需要在当前版本上发布修复时:
```bash
# 在当前发布分支上修复
git checkout release/custom-0.1.68
# ... 进行修复 ...
git commit -m "fix: 修复描述"
# 递增小版本号
echo "0.1.68.2" > backend/cmd/server/VERSION
git add backend/cmd/server/VERSION
git commit -m "chore: bump version to 0.1.68.2"
# 打标签并推送
git tag v0.1.68.2
git push origin release/custom-0.1.68
git push origin v0.1.68.2
# 同步修复到 main
git checkout main
git cherry-pick <fix-commit-hash>
git push origin main
```
---
## 服务器部署流程
### 前置条件
- 本地已配置 SSH 别名 `clicodeplus` 连接到服务器
- 服务器部署目录:`/root/sub2api`(正式)、`/root/sub2api-beta`(测试)
- 服务器使用 Docker Compose 部署
### 部署环境说明
| 环境 | 目录 | 端口 | 数据库 | 容器名 |
|------|------|------|--------|--------|
| 正式 | `/root/sub2api` | 8080 | `sub2api` | `sub2api` |
| Beta | `/root/sub2api-beta` | 8084 | `beta` | `sub2api-beta` |
### 外部数据库
正式和 Beta 环境**共用外部 PostgreSQL 数据库**(非容器内数据库),配置在 `.env` 文件中:
- `DATABASE_HOST`:外部数据库地址
- `DATABASE_SSLMODE`SSL 模式(通常为 `require`
- `POSTGRES_USER` / `POSTGRES_DB`:用户名和数据库名
#### 数据库操作命令
通过 SSH 在服务器上执行数据库操作:
```bash
# 正式环境 - 查询迁移记录
ssh clicodeplus "source /root/sub2api/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c 'SELECT * FROM schema_migrations ORDER BY applied_at DESC LIMIT 5;'"
# Beta 环境 - 查询迁移记录
ssh clicodeplus "source /root/sub2api-beta/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c 'SELECT * FROM schema_migrations ORDER BY applied_at DESC LIMIT 5;'"
# Beta 环境 - 清除指定迁移记录(重新执行迁移)
ssh clicodeplus "source /root/sub2api-beta/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c \"DELETE FROM schema_migrations WHERE filename LIKE '%049%';\""
# Beta 环境 - 更新账号数据
ssh clicodeplus "source /root/sub2api-beta/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c \"UPDATE accounts SET credentials = credentials - 'model_mapping' WHERE platform = 'antigravity';\""
```
> **注意**:使用 `source .env` 加载环境变量,避免在命令行中暴露密码。
### 部署步骤
**重要:每次部署都必须递增版本号!**
#### 0. 递增版本号(本地操作)
每次部署前,先在本地递增小版本号:
```bash
# 查看当前版本号
cat backend/cmd/server/VERSION
# 假设当前是 0.1.69.1
# 递增版本号
echo "0.1.69.2" > backend/cmd/server/VERSION
git add backend/cmd/server/VERSION
git commit -m "chore: bump version to 0.1.69.2"
git push origin release/custom-0.1.69
```
#### 1. 服务器拉取代码
```bash
ssh clicodeplus "cd /root/sub2api && git fetch fork && git checkout -B release/custom-0.1.69 fork/release/custom-0.1.69"
```
#### 2. 服务器构建镜像
```bash
ssh clicodeplus "cd /root/sub2api && docker build --no-cache -t sub2api:latest -f Dockerfile ."
```
#### 3. 更新镜像标签并重启服务
```bash
ssh clicodeplus "docker tag sub2api:latest weishaw/sub2api:latest"
ssh clicodeplus "cd /root/sub2api/deploy && docker compose up -d --force-recreate sub2api"
```
#### 4. 验证部署
```bash
# 查看启动日志
ssh clicodeplus "docker logs sub2api --tail 20"
# 确认版本号(必须与步骤 0 中设置的版本号一致)
ssh clicodeplus "cat /root/sub2api/backend/cmd/server/VERSION"
# 检查容器状态
ssh clicodeplus "docker ps | grep sub2api"
```
---
## Beta 并行部署(不影响现网)
目标:在同一台服务器上并行启动一个 beta 实例(例如端口 `8084`**严禁改动/重启**现网实例(默认目录 `/root/sub2api`)。
### 设计原则
- **新目录**beta 使用独立目录,例如 `/root/sub2api-beta`
- **敏感信息只放 `.env`**beta 的数据库密码、JWT_SECRET 等只写入 `/root/sub2api-beta/deploy/.env`,不要提交到 git。
- **独立 Compose Project**:通过 `docker compose -p sub2api-beta ...` 启动,确保 network/volume 隔离。
- **独立端口**:通过 `.env``SERVER_PORT` 映射宿主机端口(例如 `8084:8080`)。
### 前置检查
```bash
# 1) 确保 8084 未被占用
ssh clicodeplus "ss -ltnp | grep :8084 || echo '8084 is free'"
# 2) 确认现网容器还在(只读检查)
ssh clicodeplus "docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' | sed -n '1,200p'"
```
### 首次部署步骤
```bash
# 0) 进入服务器
ssh clicodeplus
# 1) 克隆代码到新目录(示例使用你的 fork
cd /root
git clone https://github.com/touwaeriol/sub2api.git sub2api-beta
cd /root/sub2api-beta
git checkout release/custom-0.1.71
# 2) 准备 beta 的 .env敏感信息只写这里
cd /root/sub2api-beta/deploy
# 推荐:从现网 .env 复制,保证除 DB 名/用户/端口外完全一致
cp -f /root/sub2api/deploy/.env ./.env
# 仅修改以下三项(其他保持不变)
perl -pi -e 's/^SERVER_PORT=.*/SERVER_PORT=8084/' ./.env
perl -pi -e 's/^POSTGRES_USER=.*/POSTGRES_USER=beta/' ./.env
perl -pi -e 's/^POSTGRES_DB=.*/POSTGRES_DB=beta/' ./.env
# 3) 写 compose override避免与现网容器名冲突镜像使用本地构建的 sub2api:beta
cat > docker-compose.override.yml <<'YAML'
services:
sub2api:
image: sub2api:beta
container_name: sub2api-beta
redis:
container_name: sub2api-beta-redis
YAML
# 4) 构建 beta 镜像(基于当前代码)
cd /root/sub2api-beta
docker build -t sub2api:beta -f Dockerfile .
# 5) 启动 beta独立 project确保不影响现网
cd /root/sub2api-beta/deploy
docker compose -p sub2api-beta --env-file .env -f docker-compose.yml -f docker-compose.override.yml up -d
# 6) 验证 beta
curl -fsS http://127.0.0.1:8084/health
docker logs sub2api-beta --tail 50
```
### 数据库配置约定beta
- 数据库地址/SSL/密码:与现网一致(从现网 `.env` 复制即可)。
- 仅修改:
- `POSTGRES_USER=beta`
- `POSTGRES_DB=beta`
注意:需要数据库侧已存在 `beta` 用户与 `beta` 数据库,并授予权限;否则容器会启动失败并不断重启。
### 更新 beta拉代码 + 仅重建 beta 容器)
```bash
ssh clicodeplus "set -e; cd /root/sub2api-beta && git fetch --all --tags && git checkout -f release/custom-0.1.71 && git reset --hard origin/release/custom-0.1.71"
ssh clicodeplus "cd /root/sub2api-beta && docker build -t sub2api:beta -f Dockerfile ."
ssh clicodeplus "cd /root/sub2api-beta/deploy && docker compose -p sub2api-beta --env-file .env -f docker-compose.yml -f docker-compose.override.yml up -d --no-deps --force-recreate sub2api"
ssh clicodeplus "curl -fsS http://127.0.0.1:8084/health"
```
### 停止/回滚 beta只影响 beta
```bash
ssh clicodeplus "cd /root/sub2api-beta/deploy && docker compose -p sub2api-beta -f docker-compose.yml -f docker-compose.override.yml down"
```
---
## 服务器首次部署
### 1. 克隆代码并配置远程仓库
```bash
ssh clicodeplus
cd /root
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api
# 添加 fork 仓库
git remote add fork https://github.com/touwaeriol/sub2api.git
```
### 2. 切换到定制分支并配置环境
```bash
git fetch fork
git checkout -B release/custom-0.1.69 fork/release/custom-0.1.69
cd deploy
cp .env.example .env
vim .env # 配置 DATABASE_URL, REDIS_URL, JWT_SECRET 等
```
### 3. 构建并启动
```bash
cd /root/sub2api
docker build -t sub2api:latest -f Dockerfile .
docker tag sub2api:latest weishaw/sub2api:latest
cd deploy && docker compose up -d
```
### 6. 启动服务
```bash
# 进入 deploy 目录
cd deploy
# 启动所有服务PostgreSQL、Redis、sub2api
docker compose up -d
# 查看服务状态
docker compose ps
```
### 7. 验证部署
```bash
# 查看应用日志
docker logs sub2api --tail 50
# 检查健康状态
curl http://localhost:8080/health
# 确认版本号
cat /root/sub2api/backend/cmd/server/VERSION
```
### 8. 常用运维命令
```bash
# 查看实时日志
docker logs -f sub2api
# 重启服务
docker compose restart sub2api
# 停止所有服务
docker compose down
# 停止并删除数据卷(慎用!会删除数据库数据)
docker compose down -v
# 查看资源使用情况
docker stats sub2api
```
---
## 定制功能说明
当前定制分支包含以下功能(相对于官方版本):
### UI/UX 定制
| 功能 | 说明 |
|------|------|
| 首页优化 | 面向用户的价值主张设计 |
| 移除 GitHub 链接 | 用户菜单中不显示 GitHub 导航 |
| 微信客服按钮 | 首页悬浮微信客服入口 |
| 限流时间精确显示 | 账号限流时间显示精确到秒 |
### Antigravity 平台增强
| 功能 | 说明 |
|------|------|
| Scope 级别限流 | 按配额域claude/gemini_text/gemini_image独立限流避免整个账号被锁定 |
| 模型级别限流 | 按具体模型(如 claude-opus-4-5独立限流更精细的限流控制 |
| 限流预检查 | 调度时预检查账号/模型限流状态,避免选中已限流账号 |
| 秒级冷却时间 | 支持 429 响应的秒级精确冷却时间 |
| 身份注入优化 | 模型身份信息注入 + 静默边界防止身份泄露 |
| thoughtSignature 修复 | Gemini 3 函数调用 400 错误修复 |
| max_tokens 自动修正 | 自动修正 max_tokens <= budget_tokens 导致的 400 错误 |
### 调度算法优化
| 功能 | 说明 |
|------|------|
| 分层过滤选择 | 调度算法从全排序改为分层过滤,提升性能 |
| LRU 随机选择 | 相同 LRU 时间时随机选择,避免账号集中 |
| 限流等待阈值配置化 | 可配置的限流等待阈值 |
### 运维增强
| 功能 | 说明 |
|------|------|
| Scope 限流统计 | 运维界面展示 Antigravity 账号 scope 级别限流统计 |
| 账号限流状态显示 | 账号列表显示 scope 和模型级别限流状态 |
| 清除限流按钮增强 | 有 scope/模型限流时也显示清除限流按钮 |
### 其他修复
| 功能 | 说明 |
|------|------|
| .gitattributes | 确保迁移文件使用 LF 换行符(解决 Windows 下 SQL 摘要不一致) |
| 部署配置优化 | DATABASE_HOST 和 DATABASE_SSLMODE 可通过 .env 配置 |
---
## 注意事项
1. **前端必须打包进镜像**:使用 `docker build` 在服务器上构建Dockerfile 会自动编译前端并 embed 到后端二进制中
2. **镜像标签**docker-compose.yml 使用 `weishaw/sub2api:latest`,本地构建后需要 `docker tag` 覆盖
3. **Windows 换行符问题**:已通过 `.gitattributes` 解决,确保 `*.sql` 文件始终使用 LF
4. **版本号管理**:每次发布必须更新 `backend/cmd/server/VERSION` 并打标签
5. **合并冲突**:合并上游新版本时,重点关注以下文件可能的冲突:
- `backend/internal/service/antigravity_gateway_service.go`
- `backend/internal/service/gateway_service.go`
- `backend/internal/pkg/antigravity/request_transformer.go`
---
## Go 代码规范
### 1. 函数设计
#### 单一职责原则
- **函数行数**:单个函数常规不应超过 **30 行**,超过时应拆分为子函数。若某段逻辑确实不可拆分(如复杂的状态机、协议解析等),可以例外,但需添加注释说明原因
- **嵌套层级**:避免超过 3 层嵌套,使用 early return 减少嵌套
```go
// ❌ 不推荐:深层嵌套
func process(data []Item) {
for _, item := range data {
if item.Valid {
if item.Type == "A" {
if item.Status == "active" {
// 业务逻辑...
}
}
}
}
}
// ✅ 推荐early return
func process(data []Item) {
for _, item := range data {
if !item.Valid {
continue
}
if item.Type != "A" {
continue
}
if item.Status != "active" {
continue
}
// 业务逻辑...
}
}
```
#### 复杂逻辑提取
将复杂的条件判断或处理逻辑提取为独立函数:
```go
// ❌ 不推荐:内联复杂逻辑
if resp.StatusCode == 429 || resp.StatusCode == 503 {
// 80+ 行处理逻辑...
}
// ✅ 推荐:提取为独立函数
result := handleRateLimitResponse(resp, params)
switch result.action {
case actionRetry:
continue
case actionBreak:
return result.resp, nil
}
```
### 2. 重复代码消除
#### 配置获取模式
将重复的配置获取逻辑提取为方法:
```go
// ❌ 不推荐:重复代码
logBody := s.settingService != nil && s.settingService.cfg != nil && s.settingService.cfg.Gateway.LogUpstreamErrorBody
maxBytes := 2048
if s.settingService != nil && s.settingService.cfg != nil && s.settingService.cfg.Gateway.LogUpstreamErrorBodyMaxBytes > 0 {
maxBytes = s.settingService.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
}
// ✅ 推荐:提取为方法
func (s *Service) getLogConfig() (logBody bool, maxBytes int) {
maxBytes = 2048
if s.settingService == nil || s.settingService.cfg == nil {
return false, maxBytes
}
cfg := s.settingService.cfg.Gateway
if cfg.LogUpstreamErrorBodyMaxBytes > 0 {
maxBytes = cfg.LogUpstreamErrorBodyMaxBytes
}
return cfg.LogUpstreamErrorBody, maxBytes
}
```
### 3. 常量管理
#### 避免魔法数字
所有硬编码的数值都应定义为常量:
```go
// ❌ 不推荐
if retryDelay >= 10*time.Second {
resetAt := time.Now().Add(30 * time.Second)
}
// ✅ 推荐
const (
rateLimitThreshold = 10 * time.Second
defaultRateLimitDuration = 30 * time.Second
)
if retryDelay >= rateLimitThreshold {
resetAt := time.Now().Add(defaultRateLimitDuration)
}
```
#### 注释引用常量名
在注释中引用常量名而非硬编码值:
```go
// ❌ 不推荐
// < 10s: 等待后重试
// ✅ 推荐
// < rateLimitThreshold: 等待后重试
```
### 4. 错误处理
#### 使用结构化日志
优先使用 `slog` 进行结构化日志记录:
```go
// ❌ 不推荐
log.Printf("%s status=%d model_rate_limit_failed model=%s error=%v", prefix, statusCode, modelName, err)
// ✅ 推荐
slog.Error("failed to set model rate limit",
"prefix", prefix,
"status_code", statusCode,
"model", modelName,
"error", err,
)
```
### 5. 测试规范
#### Mock 函数签名同步
修改函数签名时,必须同步更新所有测试中的 mock 函数:
```go
// 如果修改了 handleError 签名
handleError func(..., groupID int64, sessionHash string) *Result
// 必须同步更新测试中的 mock
handleError: func(..., groupID int64, sessionHash string) *Result {
return nil
},
```
#### 测试构建标签
统一使用测试构建标签:
```go
//go:build unit
package service
```
### 6. 时间格式解析
#### 使用标准库
优先使用 `time.ParseDuration`,支持所有 Go duration 格式:
```go
// ❌ 不推荐:手动限制格式
if !strings.HasSuffix(delay, "s") || strings.Contains(delay, "m") {
continue
}
// ✅ 推荐:使用标准库
dur, err := time.ParseDuration(delay) // 支持 "0.5s", "4m50s", "1h30m" 等
```
### 7. 接口设计
#### 接口隔离原则
定义最小化接口,只包含必需的方法:
```go
// ❌ 不推荐:使用过于宽泛的接口
type AccountRepository interface {
// 20+ 个方法...
}
// ✅ 推荐:定义最小化接口
type ModelRateLimiter interface {
SetModelRateLimit(ctx context.Context, id int64, modelKey string, resetAt time.Time) error
}
```
### 8. 并发安全
#### 共享数据保护
访问可能被并发修改的数据时,确保线程安全:
```go
// 如果 Account.Extra 可能被并发修改
// 需要使用互斥锁或原子操作保护读取
func (a *Account) GetRateLimitRemainingTime(model string) time.Duration {
a.mu.RLock()
defer a.mu.RUnlock()
// 读取 Extra 字段...
}
```
### 9. 命名规范
#### 一致的命名风格
- 常量使用 camelCase`rateLimitThreshold`
- 类型使用 PascalCase`AntigravityQuotaScope`
- 同一概念使用统一命名:`Threshold``Limit`,不要混用
```go
// ❌ 不推荐:命名不一致
antigravitySmartRetryMinWait // 使用 Min
antigravityRateLimitThreshold // 使用 Threshold
// ✅ 推荐:统一风格
antigravityMinRetryWait
antigravityRateLimitThreshold
```
### 10. 代码审查清单
在提交代码前,检查以下项目:
- [ ] 函数是否超过 30 行?(不可拆分的逻辑除外,需注释说明)
- [ ] 嵌套是否超过 3 层?
- [ ] 是否有重复代码可以提取?
- [ ] 是否使用了魔法数字?
- [ ] Mock 函数签名是否与实际函数一致?
- [ ] 测试是否覆盖了新增逻辑?
- [ ] 日志是否包含足够的上下文信息?
- [ ] 是否考虑了并发安全?
---
## CI 检查与发布门禁
### GitHub Actions 检查项
本项目有 4 个 CI 任务,**任何代码推送或发布前都必须全部通过**
| Workflow | Job | 说明 | 本地验证命令 |
|----------|-----|------|-------------|
| CI | `test` | 单元测试 + 集成测试 | `cd backend && make test-unit && make test-integration` |
| CI | `golangci-lint` | Go 代码静态检查golangci-lint v2.7 | `cd backend && golangci-lint run --timeout=5m` |
| Security Scan | `backend-security` | govulncheck + gosec 安全扫描 | `cd backend && govulncheck ./... && gosec -severity high -confidence high ./...` |
| Security Scan | `frontend-security` | pnpm audit 前端依赖安全检查 | `cd frontend && pnpm audit --prod --audit-level=high` |
### 向上游提交 PR
PR 目标是上游官方仓库,**只包含通用功能改动**bug fix、新功能、性能优化等
**以下文件禁止出现在 PR 中**(属于我们 fork 的定制化内容):
- `CLAUDE.md``AGENTS.md` — 我们的开发文档
- `backend/cmd/server/VERSION` — 我们的版本号文件
- UI 定制改动GitHub 链接移除、微信客服按钮、首页定制等)
- 部署配置(`deploy/` 目录下的定制修改)
**PR 流程**
1.`develop` 创建功能分支,只包含要提交给上游的改动
2. 推送分支后,**等待 4 个 CI job 全部通过**
3. 确认通过后再创建 PR
4. 使用 `gh run list --repo touwaeriol/sub2api --branch <branch>` 检查状态
### 自有分支推送develop / main
推送到我们自己的 `develop``main` 分支时,包含所有改动(定制化 + 通用功能)。
**推送流程**
1. 本地运行 `cd backend && make test-unit` 确保单元测试通过
2. 本地运行 `cd backend && gofmt -l ./...` 确保格式正确
3. 推送后确认 CI 和 Security Scan 两个 workflow 的 4 个 job 全部绿色 ✅
4. 任何 job 失败必须立即修复,**禁止在 CI 未通过的状态下继续后续操作**
### 发布版本
1. 确保 `main` 分支最新提交的 4 个 CI job 全部通过
2. 递增 `backend/cmd/server/VERSION`,提交并推送
3. 打 tag 推送后,确认 tag 触发的 3 个 workflowCI、Security Scan、Release全部通过
4. **Release workflow 失败时禁止部署** — 必须先修复问题,删除旧 tag重新打 tag
5. 使用 `gh run list --repo touwaeriol/sub2api --limit 10` 确认状态
### 常见 CI 失败原因及修复
- **gofmt**struct 字段对齐不一致 → 运行 `gofmt -w <file>` 修复
- **golangci-lint**:未使用的变量/导入 → 删除或使用 `_` 忽略
- **test 失败**mock 函数签名不一致 → 同步更新 mock
- **gosec**:安全漏洞 → 根据提示修复或添加例外

723
CLAUDE.md Normal file
View File

@@ -0,0 +1,723 @@
# Sub2API 开发说明
## 版本管理策略
### 版本号规则
我们在官方版本号后面添加自己的小版本号:
- 官方版本:`v0.1.68`
- 我们的版本:`v0.1.68.1``v0.1.68.2`(递增)
### 分支策略
| 分支 | 说明 |
|------|------|
| `main` | 我们的主分支,包含所有定制功能 |
| `release/custom-X.Y.Z` | 基于官方 `vX.Y.Z` 的发布分支 |
| `upstream/main` | 上游官方仓库 |
---
## 发布流程(基于新官方版本)
当官方发布新版本(如 `v0.1.69`)时:
### 1. 同步上游并创建发布分支
```bash
# 获取上游最新代码
git fetch upstream --tags
# 基于官方标签创建新的发布分支
git checkout v0.1.69 -b release/custom-0.1.69
# 合并我们的 main 分支(包含所有定制功能)
git merge main --no-edit
# 解决可能的冲突后继续
```
### 2. 更新版本号并打标签
```bash
# 更新版本号文件
echo "0.1.69.1" > backend/cmd/server/VERSION
git add backend/cmd/server/VERSION
git commit -m "chore: bump version to 0.1.69.1"
# 打上我们自己的标签
git tag v0.1.69.1
# 推送分支和标签
git push origin release/custom-0.1.69
git push origin v0.1.69.1
```
### 3. 更新 main 分支
```bash
# 将发布分支合并回 main保持 main 包含最新定制功能
git checkout main
git merge release/custom-0.1.69
git push origin main
```
---
## 热修复发布(在现有版本上修复)
当需要在当前版本上发布修复时:
```bash
# 在当前发布分支上修复
git checkout release/custom-0.1.68
# ... 进行修复 ...
git commit -m "fix: 修复描述"
# 递增小版本号
echo "0.1.68.2" > backend/cmd/server/VERSION
git add backend/cmd/server/VERSION
git commit -m "chore: bump version to 0.1.68.2"
# 打标签并推送
git tag v0.1.68.2
git push origin release/custom-0.1.68
git push origin v0.1.68.2
# 同步修复到 main
git checkout main
git cherry-pick <fix-commit-hash>
git push origin main
```
---
## 服务器部署流程
### 前置条件
- 本地已配置 SSH 别名 `clicodeplus` 连接到服务器
- 服务器部署目录:`/root/sub2api`(正式)、`/root/sub2api-beta`(测试)
- 服务器使用 Docker Compose 部署
### 部署环境说明
| 环境 | 目录 | 端口 | 数据库 | 容器名 |
|------|------|------|--------|--------|
| 正式 | `/root/sub2api` | 8080 | `sub2api` | `sub2api` |
| Beta | `/root/sub2api-beta` | 8084 | `beta` | `sub2api-beta` |
### 外部数据库
正式和 Beta 环境**共用外部 PostgreSQL 数据库**(非容器内数据库),配置在 `.env` 文件中:
- `DATABASE_HOST`:外部数据库地址
- `DATABASE_SSLMODE`SSL 模式(通常为 `require`
- `POSTGRES_USER` / `POSTGRES_DB`:用户名和数据库名
#### 数据库操作命令
通过 SSH 在服务器上执行数据库操作:
```bash
# 正式环境 - 查询迁移记录
ssh clicodeplus "source /root/sub2api/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c 'SELECT * FROM schema_migrations ORDER BY applied_at DESC LIMIT 5;'"
# Beta 环境 - 查询迁移记录
ssh clicodeplus "source /root/sub2api-beta/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c 'SELECT * FROM schema_migrations ORDER BY applied_at DESC LIMIT 5;'"
# Beta 环境 - 清除指定迁移记录(重新执行迁移)
ssh clicodeplus "source /root/sub2api-beta/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c \"DELETE FROM schema_migrations WHERE filename LIKE '%049%';\""
# Beta 环境 - 更新账号数据
ssh clicodeplus "source /root/sub2api-beta/deploy/.env && PGPASSWORD=\"\$POSTGRES_PASSWORD\" psql -h \$DATABASE_HOST -U \$POSTGRES_USER -d \$POSTGRES_DB -c \"UPDATE accounts SET credentials = credentials - 'model_mapping' WHERE platform = 'antigravity';\""
```
> **注意**:使用 `source .env` 加载环境变量,避免在命令行中暴露密码。
### 部署步骤
**重要:每次部署都必须递增版本号!**
#### 0. 递增版本号(本地操作)
每次部署前,先在本地递增小版本号:
```bash
# 查看当前版本号
cat backend/cmd/server/VERSION
# 假设当前是 0.1.69.1
# 递增版本号
echo "0.1.69.2" > backend/cmd/server/VERSION
git add backend/cmd/server/VERSION
git commit -m "chore: bump version to 0.1.69.2"
git push origin release/custom-0.1.69
```
#### 1. 服务器拉取代码
```bash
ssh clicodeplus "cd /root/sub2api && git fetch fork && git checkout -B release/custom-0.1.69 fork/release/custom-0.1.69"
```
#### 2. 服务器构建镜像
```bash
ssh clicodeplus "cd /root/sub2api && docker build --no-cache -t sub2api:latest -f Dockerfile ."
```
#### 3. 更新镜像标签并重启服务
```bash
ssh clicodeplus "docker tag sub2api:latest weishaw/sub2api:latest"
ssh clicodeplus "cd /root/sub2api/deploy && docker compose up -d --force-recreate sub2api"
```
#### 4. 验证部署
```bash
# 查看启动日志
ssh clicodeplus "docker logs sub2api --tail 20"
# 确认版本号(必须与步骤 0 中设置的版本号一致)
ssh clicodeplus "cat /root/sub2api/backend/cmd/server/VERSION"
# 检查容器状态
ssh clicodeplus "docker ps | grep sub2api"
```
---
## Beta 并行部署(不影响现网)
目标:在同一台服务器上并行启动一个 beta 实例(例如端口 `8084`**严禁改动/重启**现网实例(默认目录 `/root/sub2api`)。
### 设计原则
- **新目录**beta 使用独立目录,例如 `/root/sub2api-beta`
- **敏感信息只放 `.env`**beta 的数据库密码、JWT_SECRET 等只写入 `/root/sub2api-beta/deploy/.env`,不要提交到 git。
- **独立 Compose Project**:通过 `docker compose -p sub2api-beta ...` 启动,确保 network/volume 隔离。
- **独立端口**:通过 `.env``SERVER_PORT` 映射宿主机端口(例如 `8084:8080`)。
### 前置检查
```bash
# 1) 确保 8084 未被占用
ssh clicodeplus "ss -ltnp | grep :8084 || echo '8084 is free'"
# 2) 确认现网容器还在(只读检查)
ssh clicodeplus "docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' | sed -n '1,200p'"
```
### 首次部署步骤
```bash
# 0) 进入服务器
ssh clicodeplus
# 1) 克隆代码到新目录(示例使用你的 fork
cd /root
git clone https://github.com/touwaeriol/sub2api.git sub2api-beta
cd /root/sub2api-beta
git checkout release/custom-0.1.71
# 2) 准备 beta 的 .env敏感信息只写这里
cd /root/sub2api-beta/deploy
# 推荐:从现网 .env 复制,保证除 DB 名/用户/端口外完全一致
cp -f /root/sub2api/deploy/.env ./.env
# 仅修改以下三项(其他保持不变)
perl -pi -e 's/^SERVER_PORT=.*/SERVER_PORT=8084/' ./.env
perl -pi -e 's/^POSTGRES_USER=.*/POSTGRES_USER=beta/' ./.env
perl -pi -e 's/^POSTGRES_DB=.*/POSTGRES_DB=beta/' ./.env
# 3) 写 compose override避免与现网容器名冲突镜像使用本地构建的 sub2api:beta
cat > docker-compose.override.yml <<'YAML'
services:
sub2api:
image: sub2api:beta
container_name: sub2api-beta
redis:
container_name: sub2api-beta-redis
YAML
# 4) 构建 beta 镜像(基于当前代码)
cd /root/sub2api-beta
docker build -t sub2api:beta -f Dockerfile .
# 5) 启动 beta独立 project确保不影响现网
cd /root/sub2api-beta/deploy
docker compose -p sub2api-beta --env-file .env -f docker-compose.yml -f docker-compose.override.yml up -d
# 6) 验证 beta
curl -fsS http://127.0.0.1:8084/health
docker logs sub2api-beta --tail 50
```
### 数据库配置约定beta
- 数据库地址/SSL/密码:与现网一致(从现网 `.env` 复制即可)。
- 仅修改:
- `POSTGRES_USER=beta`
- `POSTGRES_DB=beta`
注意:需要数据库侧已存在 `beta` 用户与 `beta` 数据库,并授予权限;否则容器会启动失败并不断重启。
### 更新 beta拉代码 + 仅重建 beta 容器)
```bash
ssh clicodeplus "set -e; cd /root/sub2api-beta && git fetch --all --tags && git checkout -f release/custom-0.1.71 && git reset --hard origin/release/custom-0.1.71"
ssh clicodeplus "cd /root/sub2api-beta && docker build -t sub2api:beta -f Dockerfile ."
ssh clicodeplus "cd /root/sub2api-beta/deploy && docker compose -p sub2api-beta --env-file .env -f docker-compose.yml -f docker-compose.override.yml up -d --no-deps --force-recreate sub2api"
ssh clicodeplus "curl -fsS http://127.0.0.1:8084/health"
```
### 停止/回滚 beta只影响 beta
```bash
ssh clicodeplus "cd /root/sub2api-beta/deploy && docker compose -p sub2api-beta -f docker-compose.yml -f docker-compose.override.yml down"
```
---
## 服务器首次部署
### 1. 克隆代码并配置远程仓库
```bash
ssh clicodeplus
cd /root
git clone https://github.com/Wei-Shaw/sub2api.git
cd sub2api
# 添加 fork 仓库
git remote add fork https://github.com/touwaeriol/sub2api.git
```
### 2. 切换到定制分支并配置环境
```bash
git fetch fork
git checkout -B release/custom-0.1.69 fork/release/custom-0.1.69
cd deploy
cp .env.example .env
vim .env # 配置 DATABASE_URL, REDIS_URL, JWT_SECRET 等
```
### 3. 构建并启动
```bash
cd /root/sub2api
docker build -t sub2api:latest -f Dockerfile .
docker tag sub2api:latest weishaw/sub2api:latest
cd deploy && docker compose up -d
```
### 6. 启动服务
```bash
# 进入 deploy 目录
cd deploy
# 启动所有服务PostgreSQL、Redis、sub2api
docker compose up -d
# 查看服务状态
docker compose ps
```
### 7. 验证部署
```bash
# 查看应用日志
docker logs sub2api --tail 50
# 检查健康状态
curl http://localhost:8080/health
# 确认版本号
cat /root/sub2api/backend/cmd/server/VERSION
```
### 8. 常用运维命令
```bash
# 查看实时日志
docker logs -f sub2api
# 重启服务
docker compose restart sub2api
# 停止所有服务
docker compose down
# 停止并删除数据卷(慎用!会删除数据库数据)
docker compose down -v
# 查看资源使用情况
docker stats sub2api
```
---
## 定制功能说明
当前定制分支包含以下功能(相对于官方版本):
### UI/UX 定制
| 功能 | 说明 |
|------|------|
| 首页优化 | 面向用户的价值主张设计 |
| 移除 GitHub 链接 | 用户菜单中不显示 GitHub 导航 |
| 微信客服按钮 | 首页悬浮微信客服入口 |
| 限流时间精确显示 | 账号限流时间显示精确到秒 |
### Antigravity 平台增强
| 功能 | 说明 |
|------|------|
| Scope 级别限流 | 按配额域claude/gemini_text/gemini_image独立限流避免整个账号被锁定 |
| 模型级别限流 | 按具体模型(如 claude-opus-4-5独立限流更精细的限流控制 |
| 限流预检查 | 调度时预检查账号/模型限流状态,避免选中已限流账号 |
| 秒级冷却时间 | 支持 429 响应的秒级精确冷却时间 |
| 身份注入优化 | 模型身份信息注入 + 静默边界防止身份泄露 |
| thoughtSignature 修复 | Gemini 3 函数调用 400 错误修复 |
| max_tokens 自动修正 | 自动修正 max_tokens <= budget_tokens 导致的 400 错误 |
### 调度算法优化
| 功能 | 说明 |
|------|------|
| 分层过滤选择 | 调度算法从全排序改为分层过滤,提升性能 |
| LRU 随机选择 | 相同 LRU 时间时随机选择,避免账号集中 |
| 限流等待阈值配置化 | 可配置的限流等待阈值 |
### 运维增强
| 功能 | 说明 |
|------|------|
| Scope 限流统计 | 运维界面展示 Antigravity 账号 scope 级别限流统计 |
| 账号限流状态显示 | 账号列表显示 scope 和模型级别限流状态 |
| 清除限流按钮增强 | 有 scope/模型限流时也显示清除限流按钮 |
### 其他修复
| 功能 | 说明 |
|------|------|
| .gitattributes | 确保迁移文件使用 LF 换行符(解决 Windows 下 SQL 摘要不一致) |
| 部署配置优化 | DATABASE_HOST 和 DATABASE_SSLMODE 可通过 .env 配置 |
---
## 注意事项
1. **前端必须打包进镜像**:使用 `docker build` 在服务器上构建Dockerfile 会自动编译前端并 embed 到后端二进制中
2. **镜像标签**docker-compose.yml 使用 `weishaw/sub2api:latest`,本地构建后需要 `docker tag` 覆盖
3. **Windows 换行符问题**:已通过 `.gitattributes` 解决,确保 `*.sql` 文件始终使用 LF
4. **版本号管理**:每次发布必须更新 `backend/cmd/server/VERSION` 并打标签
5. **合并冲突**:合并上游新版本时,重点关注以下文件可能的冲突:
- `backend/internal/service/antigravity_gateway_service.go`
- `backend/internal/service/gateway_service.go`
- `backend/internal/pkg/antigravity/request_transformer.go`
---
## Go 代码规范
### 1. 函数设计
#### 单一职责原则
- **函数行数**:单个函数常规不应超过 **30 行**,超过时应拆分为子函数。若某段逻辑确实不可拆分(如复杂的状态机、协议解析等),可以例外,但需添加注释说明原因
- **嵌套层级**:避免超过 3 层嵌套,使用 early return 减少嵌套
```go
// ❌ 不推荐:深层嵌套
func process(data []Item) {
for _, item := range data {
if item.Valid {
if item.Type == "A" {
if item.Status == "active" {
// 业务逻辑...
}
}
}
}
}
// ✅ 推荐early return
func process(data []Item) {
for _, item := range data {
if !item.Valid {
continue
}
if item.Type != "A" {
continue
}
if item.Status != "active" {
continue
}
// 业务逻辑...
}
}
```
#### 复杂逻辑提取
将复杂的条件判断或处理逻辑提取为独立函数:
```go
// ❌ 不推荐:内联复杂逻辑
if resp.StatusCode == 429 || resp.StatusCode == 503 {
// 80+ 行处理逻辑...
}
// ✅ 推荐:提取为独立函数
result := handleRateLimitResponse(resp, params)
switch result.action {
case actionRetry:
continue
case actionBreak:
return result.resp, nil
}
```
### 2. 重复代码消除
#### 配置获取模式
将重复的配置获取逻辑提取为方法:
```go
// ❌ 不推荐:重复代码
logBody := s.settingService != nil && s.settingService.cfg != nil && s.settingService.cfg.Gateway.LogUpstreamErrorBody
maxBytes := 2048
if s.settingService != nil && s.settingService.cfg != nil && s.settingService.cfg.Gateway.LogUpstreamErrorBodyMaxBytes > 0 {
maxBytes = s.settingService.cfg.Gateway.LogUpstreamErrorBodyMaxBytes
}
// ✅ 推荐:提取为方法
func (s *Service) getLogConfig() (logBody bool, maxBytes int) {
maxBytes = 2048
if s.settingService == nil || s.settingService.cfg == nil {
return false, maxBytes
}
cfg := s.settingService.cfg.Gateway
if cfg.LogUpstreamErrorBodyMaxBytes > 0 {
maxBytes = cfg.LogUpstreamErrorBodyMaxBytes
}
return cfg.LogUpstreamErrorBody, maxBytes
}
```
### 3. 常量管理
#### 避免魔法数字
所有硬编码的数值都应定义为常量:
```go
// ❌ 不推荐
if retryDelay >= 10*time.Second {
resetAt := time.Now().Add(30 * time.Second)
}
// ✅ 推荐
const (
rateLimitThreshold = 10 * time.Second
defaultRateLimitDuration = 30 * time.Second
)
if retryDelay >= rateLimitThreshold {
resetAt := time.Now().Add(defaultRateLimitDuration)
}
```
#### 注释引用常量名
在注释中引用常量名而非硬编码值:
```go
// ❌ 不推荐
// < 10s: 等待后重试
// ✅ 推荐
// < rateLimitThreshold: 等待后重试
```
### 4. 错误处理
#### 使用结构化日志
优先使用 `slog` 进行结构化日志记录:
```go
// ❌ 不推荐
log.Printf("%s status=%d model_rate_limit_failed model=%s error=%v", prefix, statusCode, modelName, err)
// ✅ 推荐
slog.Error("failed to set model rate limit",
"prefix", prefix,
"status_code", statusCode,
"model", modelName,
"error", err,
)
```
### 5. 测试规范
#### Mock 函数签名同步
修改函数签名时,必须同步更新所有测试中的 mock 函数:
```go
// 如果修改了 handleError 签名
handleError func(..., groupID int64, sessionHash string) *Result
// 必须同步更新测试中的 mock
handleError: func(..., groupID int64, sessionHash string) *Result {
return nil
},
```
#### 测试构建标签
统一使用测试构建标签:
```go
//go:build unit
package service
```
### 6. 时间格式解析
#### 使用标准库
优先使用 `time.ParseDuration`,支持所有 Go duration 格式:
```go
// ❌ 不推荐:手动限制格式
if !strings.HasSuffix(delay, "s") || strings.Contains(delay, "m") {
continue
}
// ✅ 推荐:使用标准库
dur, err := time.ParseDuration(delay) // 支持 "0.5s", "4m50s", "1h30m" 等
```
### 7. 接口设计
#### 接口隔离原则
定义最小化接口,只包含必需的方法:
```go
// ❌ 不推荐:使用过于宽泛的接口
type AccountRepository interface {
// 20+ 个方法...
}
// ✅ 推荐:定义最小化接口
type ModelRateLimiter interface {
SetModelRateLimit(ctx context.Context, id int64, modelKey string, resetAt time.Time) error
}
```
### 8. 并发安全
#### 共享数据保护
访问可能被并发修改的数据时,确保线程安全:
```go
// 如果 Account.Extra 可能被并发修改
// 需要使用互斥锁或原子操作保护读取
func (a *Account) GetRateLimitRemainingTime(model string) time.Duration {
a.mu.RLock()
defer a.mu.RUnlock()
// 读取 Extra 字段...
}
```
### 9. 命名规范
#### 一致的命名风格
- 常量使用 camelCase`rateLimitThreshold`
- 类型使用 PascalCase`AntigravityQuotaScope`
- 同一概念使用统一命名:`Threshold``Limit`,不要混用
```go
// ❌ 不推荐:命名不一致
antigravitySmartRetryMinWait // 使用 Min
antigravityRateLimitThreshold // 使用 Threshold
// ✅ 推荐:统一风格
antigravityMinRetryWait
antigravityRateLimitThreshold
```
### 10. 代码审查清单
在提交代码前,检查以下项目:
- [ ] 函数是否超过 30 行?(不可拆分的逻辑除外,需注释说明)
- [ ] 嵌套是否超过 3 层?
- [ ] 是否有重复代码可以提取?
- [ ] 是否使用了魔法数字?
- [ ] Mock 函数签名是否与实际函数一致?
- [ ] 测试是否覆盖了新增逻辑?
- [ ] 日志是否包含足够的上下文信息?
- [ ] 是否考虑了并发安全?
---
## CI 检查与发布门禁
### GitHub Actions 检查项
本项目有 4 个 CI 任务,**任何代码推送或发布前都必须全部通过**
| Workflow | Job | 说明 | 本地验证命令 |
|----------|-----|------|-------------|
| CI | `test` | 单元测试 + 集成测试 | `cd backend && make test-unit && make test-integration` |
| CI | `golangci-lint` | Go 代码静态检查golangci-lint v2.7 | `cd backend && golangci-lint run --timeout=5m` |
| Security Scan | `backend-security` | govulncheck + gosec 安全扫描 | `cd backend && govulncheck ./... && gosec -severity high -confidence high ./...` |
| Security Scan | `frontend-security` | pnpm audit 前端依赖安全检查 | `cd frontend && pnpm audit --prod --audit-level=high` |
### 向上游提交 PR
PR 目标是上游官方仓库,**只包含通用功能改动**bug fix、新功能、性能优化等
**以下文件禁止出现在 PR 中**(属于我们 fork 的定制化内容):
- `CLAUDE.md``AGENTS.md` — 我们的开发文档
- `backend/cmd/server/VERSION` — 我们的版本号文件
- UI 定制改动GitHub 链接移除、微信客服按钮、首页定制等)
- 部署配置(`deploy/` 目录下的定制修改)
**PR 流程**
1.`develop` 创建功能分支,只包含要提交给上游的改动
2. 推送分支后,**等待 4 个 CI job 全部通过**
3. 确认通过后再创建 PR
4. 使用 `gh run list --repo touwaeriol/sub2api --branch <branch>` 检查状态
### 自有分支推送develop / main
推送到我们自己的 `develop``main` 分支时,包含所有改动(定制化 + 通用功能)。
**推送流程**
1. 本地运行 `cd backend && make test-unit` 确保单元测试通过
2. 本地运行 `cd backend && gofmt -l ./...` 确保格式正确
3. 推送后确认 CI 和 Security Scan 两个 workflow 的 4 个 job 全部绿色 ✅
4. 任何 job 失败必须立即修复,**禁止在 CI 未通过的状态下继续后续操作**
### 发布版本
1. 确保 `main` 分支最新提交的 4 个 CI job 全部通过
2. 递增 `backend/cmd/server/VERSION`,提交并推送
3. 打 tag 推送后,确认 tag 触发的 3 个 workflowCI、Security Scan、Release全部通过
4. **Release workflow 失败时禁止部署** — 必须先修复问题,删除旧 tag重新打 tag
5. 使用 `gh run list --repo touwaeriol/sub2api --limit 10` 确认状态
### 常见 CI 失败原因及修复
- **gofmt**struct 字段对齐不一致 → 运行 `gofmt -w <file>` 修复
- **golangci-lint**:未使用的变量/导入 → 删除或使用 `_` 忽略
- **test 失败**mock 函数签名不一致 → 同步更新 mock
- **gosec**:安全漏洞 → 根据提示修复或添加例外

View File

@@ -47,13 +47,15 @@ services:
# =======================================================================
# Database Configuration (PostgreSQL)
# Default: uses local postgres container
# External DB: set DATABASE_HOST and DATABASE_SSLMODE in .env
# =======================================================================
- DATABASE_HOST=postgres
- DATABASE_PORT=5432
- DATABASE_HOST=${DATABASE_HOST:-postgres}
- DATABASE_PORT=${DATABASE_PORT:-5432}
- DATABASE_USER=${POSTGRES_USER:-sub2api}
- DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- DATABASE_DBNAME=${POSTGRES_DB:-sub2api}
- DATABASE_SSLMODE=disable
- DATABASE_SSLMODE=${DATABASE_SSLMODE:-disable}
# =======================================================================
# Redis Configuration
@@ -128,8 +130,6 @@ services:
# Examples: http://host:port, socks5://host:port
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
@@ -141,35 +141,6 @@ services:
retries: 3
start_period: 30s
# ===========================================================================
# PostgreSQL Database
# ===========================================================================
postgres:
image: postgres:18-alpine
container_name: sub2api-postgres
restart: unless-stopped
ulimits:
nofile:
soft: 100000
hard: 100000
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
- TZ=${TZ:-Asia/Shanghai}
networks:
- sub2api-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# 注意:不暴露端口到宿主机,应用通过内部网络连接
# 如需调试可临时添加ports: ["127.0.0.1:5433:5432"]
# ===========================================================================
# Redis Cache
# ===========================================================================
@@ -209,8 +180,6 @@ services:
volumes:
sub2api_data:
driver: local
postgres_data:
driver: local
redis_data:
driver: local

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -0,0 +1,104 @@
<template>
<!-- 悬浮按钮 - 使用主题色 -->
<button
@click="showModal = true"
class="fixed bottom-6 right-6 z-50 flex items-center gap-2 rounded-full bg-gradient-to-r from-primary-500 to-primary-600 px-4 py-3 text-white shadow-lg shadow-primary-500/25 transition-all hover:from-primary-600 hover:to-primary-700 hover:shadow-xl hover:shadow-primary-500/30"
>
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.328.328 0 00.186-.059l2.114-1.225a.87.87 0 01.415-.106.807.807 0 01.213.026 10.07 10.07 0 002.696.37c.262 0 .52-.011.776-.028a5.91 5.91 0 01-.193-1.479c0-3.644 3.374-6.6 7.536-6.6.262 0 .52.011.776.028-.628-3.513-4.27-6.472-8.885-6.472zM5.785 5.97a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm5.813 0a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm5.192 2.642c-3.703 0-6.71 2.567-6.71 5.73 0 3.163 3.007 5.73 6.71 5.73a7.9 7.9 0 002.126-.288.644.644 0 01.17-.022.69.69 0 01.329.085l1.672.97a.262.262 0 00.147.046c.128 0 .23-.104.23-.233a.403.403 0 00-.038-.168l-.309-1.17a.468.468 0 01.168-.527c1.449-1.065 2.374-2.643 2.374-4.423 0-3.163-3.007-5.73-6.71-5.73h-.159zm-2.434 3.34a.88.88 0 110 1.76.88.88 0 010-1.76zm4.868 0a.88.88 0 110 1.76.88.88 0 010-1.76z"/>
</svg>
<span class="text-sm font-medium">客服</span>
</button>
<!-- 弹窗 -->
<Teleport to="body">
<Transition name="fade">
<div
v-if="showModal"
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"
@click.self="showModal = false"
>
<Transition name="scale">
<div
v-if="showModal"
class="relative w-full max-w-sm rounded-2xl bg-white p-6 shadow-2xl dark:bg-dark-700"
>
<!-- 关闭按钮 -->
<button
@click="showModal = false"
class="absolute right-4 top-4 text-gray-400 transition-colors hover:text-gray-600 dark:text-dark-400 dark:hover:text-dark-200"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- 标题 -->
<div class="mb-4 flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-primary-500 to-primary-600">
<svg class="h-6 w-6 text-white" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.328.328 0 00.186-.059l2.114-1.225a.87.87 0 01.415-.106.807.807 0 01.213.026 10.07 10.07 0 002.696.37c.262 0 .52-.011.776-.028a5.91 5.91 0 01-.193-1.479c0-3.644 3.374-6.6 7.536-6.6.262 0 .52.011.776.028-.628-3.513-4.27-6.472-8.885-6.472zM5.785 5.97a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm5.813 0a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zm5.192 2.642c-3.703 0-6.71 2.567-6.71 5.73 0 3.163 3.007 5.73 6.71 5.73a7.9 7.9 0 002.126-.288.644.644 0 01.17-.022.69.69 0 01.329.085l1.672.97a.262.262 0 00.147.046c.128 0 .23-.104.23-.233a.403.403 0 00-.038-.168l-.309-1.17a.468.468 0 01.168-.527c1.449-1.065 2.374-2.643 2.374-4.423 0-3.163-3.007-5.73-6.71-5.73h-.159zm-2.434 3.34a.88.88 0 110 1.76.88.88 0 010-1.76zm4.868 0a.88.88 0 110 1.76.88.88 0 010-1.76z"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">联系客服</h3>
<p class="text-sm text-gray-500 dark:text-dark-400">扫码添加好友</p>
</div>
</div>
<!-- 二维码卡片 -->
<div class="mb-4 overflow-hidden rounded-xl border border-primary-100 bg-gradient-to-br from-primary-50 to-white p-3 dark:border-primary-800/30 dark:from-primary-900/10 dark:to-dark-800">
<img
src="/wechat-qr.jpg"
alt="微信二维码"
class="w-full rounded-lg"
/>
</div>
<!-- 提示文字 -->
<div class="text-center">
<p class="mb-2 text-sm font-medium text-primary-600 dark:text-primary-400">
微信扫码添加客服
</p>
<p class="flex items-center justify-center gap-1 text-xs text-gray-500 dark:text-dark-400">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
工作时间周一至周五 9:00-18:00
</p>
</div>
</div>
</Transition>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.scale-enter-active,
.scale-leave-active {
transition: all 0.2s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.95);
}
</style>

View File

@@ -121,23 +121,6 @@
<Icon name="key" size="sm" />
{{ t('nav.apiKeys') }}
</router-link>
<a
href="https://github.com/Wei-Shaw/sub2api"
target="_blank"
rel="noopener noreferrer"
@click="closeDropdown"
class="dropdown-item"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"
/>
</svg>
{{ t('nav.github') }}
</a>
</div>
<!-- Contact Support (only show if configured) -->

View File

@@ -122,8 +122,11 @@
>
{{ siteName }}
</h1>
<p class="mb-8 text-lg text-gray-600 dark:text-dark-300 md:text-xl">
{{ siteSubtitle }}
<p class="mb-3 text-xl font-semibold text-primary-600 dark:text-primary-400 md:text-2xl">
{{ t('home.heroSubtitle') }}
</p>
<p class="mb-8 text-base text-gray-600 dark:text-dark-300 md:text-lg">
{{ t('home.heroDescription') }}
</p>
<!-- CTA Button -->
@@ -177,7 +180,7 @@
</div>
<!-- Feature Tags - Centered -->
<div class="mb-12 flex flex-wrap items-center justify-center gap-4 md:gap-6">
<div class="mb-16 flex flex-wrap items-center justify-center gap-4 md:gap-6">
<div
class="inline-flex items-center gap-2.5 rounded-full border border-gray-200/50 bg-white/80 px-5 py-2.5 shadow-sm backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/80"
>
@@ -204,6 +207,63 @@
</div>
</div>
<!-- Pain Points Section -->
<div class="mb-16">
<h2 class="mb-8 text-center text-2xl font-bold text-gray-900 dark:text-white md:text-3xl">
{{ t('home.painPoints.title') }}
</h2>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- Pain Point 1: Expensive -->
<div class="rounded-xl border border-red-200/50 bg-red-50/50 p-5 dark:border-red-900/30 dark:bg-red-950/20">
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100 dark:bg-red-900/30">
<svg class="h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.expensive.title') }}</h3>
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.expensive.desc') }}</p>
</div>
<!-- Pain Point 2: Complex -->
<div class="rounded-xl border border-orange-200/50 bg-orange-50/50 p-5 dark:border-orange-900/30 dark:bg-orange-950/20">
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100 dark:bg-orange-900/30">
<svg class="h-5 w-5 text-orange-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.complex.title') }}</h3>
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.complex.desc') }}</p>
</div>
<!-- Pain Point 3: Unstable -->
<div class="rounded-xl border border-yellow-200/50 bg-yellow-50/50 p-5 dark:border-yellow-900/30 dark:bg-yellow-950/20">
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100 dark:bg-yellow-900/30">
<svg class="h-5 w-5 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.unstable.title') }}</h3>
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.unstable.desc') }}</p>
</div>
<!-- Pain Point 4: No Control -->
<div class="rounded-xl border border-gray-200/50 bg-gray-50/50 p-5 dark:border-dark-700/50 dark:bg-dark-800/50">
<div class="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-dark-700">
<svg class="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
</div>
<h3 class="mb-1.5 font-semibold text-gray-900 dark:text-white">{{ t('home.painPoints.items.noControl.title') }}</h3>
<p class="text-sm text-gray-600 dark:text-dark-400">{{ t('home.painPoints.items.noControl.desc') }}</p>
</div>
</div>
</div>
<!-- Solutions Section Title -->
<div class="mb-8 text-center">
<h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white md:text-3xl">
{{ t('home.solutions.title') }}
</h2>
<p class="text-gray-600 dark:text-dark-400">{{ t('home.solutions.subtitle') }}</p>
</div>
<!-- Features Grid -->
<div class="mb-12 grid gap-6 md:grid-cols-3">
<!-- Feature 1: Unified Gateway -->
@@ -369,6 +429,77 @@
>
</div>
</div>
<!-- Comparison Table -->
<div class="mb-16">
<h2 class="mb-8 text-center text-2xl font-bold text-gray-900 dark:text-white md:text-3xl">
{{ t('home.comparison.title') }}
</h2>
<div class="overflow-x-auto">
<table class="w-full rounded-xl border border-gray-200/50 bg-white/60 backdrop-blur-sm dark:border-dark-700/50 dark:bg-dark-800/60">
<thead>
<tr class="border-b border-gray-200/50 dark:border-dark-700/50">
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 dark:text-white">{{ t('home.comparison.headers.feature') }}</th>
<th class="px-6 py-4 text-center text-sm font-semibold text-gray-500 dark:text-dark-400">{{ t('home.comparison.headers.official') }}</th>
<th class="px-6 py-4 text-center text-sm font-semibold text-primary-600 dark:text-primary-400">{{ t('home.comparison.headers.us') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200/50 dark:divide-dark-700/50">
<tr>
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.pricing.feature') }}</td>
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.pricing.official') }}</td>
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.pricing.us') }}</td>
</tr>
<tr>
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.models.feature') }}</td>
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.models.official') }}</td>
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.models.us') }}</td>
</tr>
<tr>
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.management.feature') }}</td>
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.management.official') }}</td>
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.management.us') }}</td>
</tr>
<tr>
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.stability.feature') }}</td>
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.stability.official') }}</td>
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.stability.us') }}</td>
</tr>
<tr>
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ t('home.comparison.items.control.feature') }}</td>
<td class="px-6 py-4 text-center text-sm text-gray-500 dark:text-dark-400">{{ t('home.comparison.items.control.official') }}</td>
<td class="px-6 py-4 text-center text-sm font-medium text-primary-600 dark:text-primary-400">{{ t('home.comparison.items.control.us') }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- CTA Section -->
<div class="mb-8 rounded-2xl bg-gradient-to-r from-primary-500 to-primary-600 p-8 text-center shadow-xl shadow-primary-500/20 md:p-12">
<h2 class="mb-3 text-2xl font-bold text-white md:text-3xl">
{{ t('home.cta.title') }}
</h2>
<p class="mb-6 text-primary-100">
{{ t('home.cta.description') }}
</p>
<router-link
v-if="!isAuthenticated"
to="/register"
class="inline-flex items-center gap-2 rounded-full bg-white px-8 py-3 font-semibold text-primary-600 shadow-lg transition-all hover:bg-gray-50 hover:shadow-xl"
>
{{ t('home.cta.button') }}
<Icon name="arrowRight" size="md" :stroke-width="2" />
</router-link>
<router-link
v-else
:to="dashboardPath"
class="inline-flex items-center gap-2 rounded-full bg-white px-8 py-3 font-semibold text-primary-600 shadow-lg transition-all hover:bg-gray-50 hover:shadow-xl"
>
{{ t('home.goToDashboard') }}
<Icon name="arrowRight" size="md" :stroke-width="2" />
</router-link>
</div>
</div>
</main>
@@ -380,27 +511,20 @@
<p class="text-sm text-gray-500 dark:text-dark-400">
&copy; {{ currentYear }} {{ siteName }}. {{ t('home.footer.allRightsReserved') }}
</p>
<div class="flex items-center gap-4">
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-white"
>
{{ t('home.docs') }}
</a>
<a
:href="githubUrl"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-white"
>
GitHub
</a>
</div>
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-white"
>
{{ t('home.docs') }}
</a>
</div>
</footer>
<!-- 微信客服悬浮按钮 -->
<WechatServiceButton />
</div>
</template>
@@ -410,6 +534,7 @@ import { useI18n } from 'vue-i18n'
import { useAuthStore, useAppStore } from '@/stores'
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
import Icon from '@/components/icons/Icon.vue'
import WechatServiceButton from '@/components/common/WechatServiceButton.vue'
const { t } = useI18n()
@@ -419,7 +544,6 @@ const appStore = useAppStore()
// Site settings - directly from appStore (already initialized from injected config)
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'Sub2API')
const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '')
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'AI API Gateway Platform')
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
@@ -432,9 +556,6 @@ const isHomeContentUrl = computed(() => {
// Theme
const isDark = ref(document.documentElement.classList.contains('dark'))
// GitHub URL
const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
// Auth state
const isAuthenticated = computed(() => authStore.isAuthenticated)
const isAdmin = computed(() => authStore.isAdmin)

View File

@@ -0,0 +1,127 @@
#!/bin/bash
# Gemini 粘性会话压力测试脚本
# 测试目标:验证不同会话分配不同账号,同一会话保持同一账号
BASE_URL="http://host.clicodeplus.com:8080"
API_KEY="sk-32ad0a3197e528c840ea84f0dc6b2056dd3fead03526b5c605a60709bd408f7e"
MODEL="gemini-2.5-flash"
# 创建临时目录存放结果
RESULT_DIR="/tmp/gemini_stress_test_$(date +%s)"
mkdir -p "$RESULT_DIR"
echo "=========================================="
echo "Gemini 粘性会话压力测试"
echo "结果目录: $RESULT_DIR"
echo "=========================================="
# 函数:发送请求并记录
send_request() {
local session_id=$1
local round=$2
local system_prompt=$3
local contents=$4
local output_file="$RESULT_DIR/session_${session_id}_round_${round}.json"
local request_body=$(cat <<EOF
{
"systemInstruction": {
"parts": [{"text": "$system_prompt"}]
},
"contents": $contents
}
EOF
)
curl -s -X POST "${BASE_URL}/v1beta/models/${MODEL}:generateContent" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: ${API_KEY}" \
-d "$request_body" > "$output_file" 2>&1
echo "[Session $session_id Round $round] 完成"
}
# 会话1数学计算器累加序列
run_session_1() {
local sys_prompt="你是一个数学计算器,只返回计算结果数字,不要任何解释"
# Round 1: 1+1=?
send_request 1 1 "$sys_prompt" '[{"role":"user","parts":[{"text":"1+1=?"}]}]'
# Round 2: 继续 2+2=?(累加历史)
send_request 1 2 "$sys_prompt" '[{"role":"user","parts":[{"text":"1+1=?"}]},{"role":"model","parts":[{"text":"2"}]},{"role":"user","parts":[{"text":"2+2=?"}]}]'
# Round 3: 继续 3+3=?
send_request 1 3 "$sys_prompt" '[{"role":"user","parts":[{"text":"1+1=?"}]},{"role":"model","parts":[{"text":"2"}]},{"role":"user","parts":[{"text":"2+2=?"}]},{"role":"model","parts":[{"text":"4"}]},{"role":"user","parts":[{"text":"3+3=?"}]}]'
# Round 4: 批量计算 10+10, 20+20, 30+30
send_request 1 4 "$sys_prompt" '[{"role":"user","parts":[{"text":"1+1=?"}]},{"role":"model","parts":[{"text":"2"}]},{"role":"user","parts":[{"text":"2+2=?"}]},{"role":"model","parts":[{"text":"4"}]},{"role":"user","parts":[{"text":"3+3=?"}]},{"role":"model","parts":[{"text":"6"}]},{"role":"user","parts":[{"text":"计算: 10+10=? 20+20=? 30+30=?"}]}]'
}
# 会话2英文翻译器不同系统提示词 = 不同会话)
run_session_2() {
local sys_prompt="你是一个英文翻译器,将中文翻译成英文,只返回翻译结果"
send_request 2 1 "$sys_prompt" '[{"role":"user","parts":[{"text":"你好"}]}]'
send_request 2 2 "$sys_prompt" '[{"role":"user","parts":[{"text":"你好"}]},{"role":"model","parts":[{"text":"Hello"}]},{"role":"user","parts":[{"text":"世界"}]}]'
send_request 2 3 "$sys_prompt" '[{"role":"user","parts":[{"text":"你好"}]},{"role":"model","parts":[{"text":"Hello"}]},{"role":"user","parts":[{"text":"世界"}]},{"role":"model","parts":[{"text":"World"}]},{"role":"user","parts":[{"text":"早上好"}]}]'
}
# 会话3日文翻译器
run_session_3() {
local sys_prompt="你是一个日文翻译器,将中文翻译成日文,只返回翻译结果"
send_request 3 1 "$sys_prompt" '[{"role":"user","parts":[{"text":"你好"}]}]'
send_request 3 2 "$sys_prompt" '[{"role":"user","parts":[{"text":"你好"}]},{"role":"model","parts":[{"text":"こんにちは"}]},{"role":"user","parts":[{"text":"谢谢"}]}]'
send_request 3 3 "$sys_prompt" '[{"role":"user","parts":[{"text":"你好"}]},{"role":"model","parts":[{"text":"こんにちは"}]},{"role":"user","parts":[{"text":"谢谢"}]},{"role":"model","parts":[{"text":"ありがとう"}]},{"role":"user","parts":[{"text":"再见"}]}]'
}
# 会话4乘法计算器另一个数学会话但系统提示词不同
run_session_4() {
local sys_prompt="你是一个乘法专用计算器,只计算乘法,返回数字结果"
send_request 4 1 "$sys_prompt" '[{"role":"user","parts":[{"text":"2*3=?"}]}]'
send_request 4 2 "$sys_prompt" '[{"role":"user","parts":[{"text":"2*3=?"}]},{"role":"model","parts":[{"text":"6"}]},{"role":"user","parts":[{"text":"4*5=?"}]}]'
send_request 4 3 "$sys_prompt" '[{"role":"user","parts":[{"text":"2*3=?"}]},{"role":"model","parts":[{"text":"6"}]},{"role":"user","parts":[{"text":"4*5=?"}]},{"role":"model","parts":[{"text":"20"}]},{"role":"user","parts":[{"text":"计算: 10*10=? 20*20=?"}]}]'
}
# 会话5诗人完全不同的角色
run_session_5() {
local sys_prompt="你是一位诗人,用简短的诗句回应每个话题,每次只写一句诗"
send_request 5 1 "$sys_prompt" '[{"role":"user","parts":[{"text":"春天"}]}]'
send_request 5 2 "$sys_prompt" '[{"role":"user","parts":[{"text":"春天"}]},{"role":"model","parts":[{"text":"春风拂面花满枝"}]},{"role":"user","parts":[{"text":"夏天"}]}]'
send_request 5 3 "$sys_prompt" '[{"role":"user","parts":[{"text":"春天"}]},{"role":"model","parts":[{"text":"春风拂面花满枝"}]},{"role":"user","parts":[{"text":"夏天"}]},{"role":"model","parts":[{"text":"蝉鸣蛙声伴荷香"}]},{"role":"user","parts":[{"text":"秋天"}]}]'
}
echo ""
echo "开始并发测试 5 个独立会话..."
echo ""
# 并发运行所有会话
run_session_1 &
run_session_2 &
run_session_3 &
run_session_4 &
run_session_5 &
# 等待所有后台任务完成
wait
echo ""
echo "=========================================="
echo "所有请求完成,结果保存在: $RESULT_DIR"
echo "=========================================="
# 显示结果摘要
echo ""
echo "响应摘要:"
for f in "$RESULT_DIR"/*.json; do
filename=$(basename "$f")
response=$(cat "$f" | head -c 200)
echo "[$filename]: ${response}..."
done
echo ""
echo "请检查服务器日志确认账号分配情况"