-
Notifications
You must be signed in to change notification settings - Fork 11.1k
feat(web): Introduce Token Sleep Mode and UI enhancements #2813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8e4f098
6b2af66
7dad444
fa1561d
cc4c5d8
d9ca915
989f504
a112ba1
14afb66
bc5ce1d
c470a36
5cdd17d
c5502d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,14 @@ | ||
| FROM oven/bun:latest AS builder | ||
| FROM golang:latest | ||
|
|
||
| WORKDIR /build | ||
| COPY web/package.json . | ||
| COPY web/bun.lock . | ||
| RUN bun install | ||
| COPY ./web . | ||
| COPY ./VERSION . | ||
| RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build | ||
| WORKDIR /app | ||
|
|
||
| FROM golang:alpine AS builder2 | ||
| ENV GO111MODULE=on CGO_ENABLED=0 | ||
|
|
||
| ARG TARGETOS | ||
| ARG TARGETARCH | ||
| ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} | ||
| ENV GOEXPERIMENT=greenteagc | ||
|
|
||
| WORKDIR /build | ||
|
|
||
| ADD go.mod go.sum ./ | ||
| RUN go mod download | ||
| ENV GOOS=windows | ||
| ENV GOARCH=amd64 | ||
| ENV CGO_ENABLED=0 | ||
|
|
||
| # 复制源码(包含我们伪造的 web/dist) | ||
| COPY . . | ||
| COPY --from=builder /build/dist ./web/dist | ||
| RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api | ||
|
|
||
| FROM debian:bookworm-slim | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && update-ca-certificates | ||
|
|
||
| COPY --from=builder2 /build/new-api / | ||
| EXPOSE 3000 | ||
| WORKDIR /data | ||
| ENTRYPOINT ["/new-api"] | ||
| # 下载依赖并编译 | ||
| RUN go mod download | ||
| RUN go build -ldflags "-s -w" -o new-api-galaxy.exe | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,6 +191,10 @@ func AddToken(c *gin.Context) { | |
| AllowIps: token.AllowIps, | ||
| Group: token.Group, | ||
| CrossGroupRetry: token.CrossGroupRetry, | ||
| Duration: token.Duration, | ||
| } | ||
| if token.Duration > 0 { | ||
| cleanToken.ExpiredTime = -1 | ||
| } | ||
|
Comment on lines
+194
to
198
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate 🔧 Suggested guard if len(token.Name) > 50 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "令牌名称过长",
})
return
}
+if token.Duration < 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "Duration 不能为负数",
+ })
+ return
+}🤖 Prompt for AI Agents |
||
| err = cleanToken.Insert() | ||
| if err != nil { | ||
|
|
@@ -286,6 +290,7 @@ func UpdateToken(c *gin.Context) { | |
| cleanToken.AllowIps = token.AllowIps | ||
| cleanToken.Group = token.Group | ||
| cleanToken.CrossGroupRetry = token.CrossGroupRetry | ||
| cleanToken.Duration = token.Duration | ||
| } | ||
| err = cleanToken.Update() | ||
| if err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| # New API 部署指南(避坑记录) | ||
|
|
||
| > 记录日期:2026-01-30 | ||
| > 部署环境:Windows 11 本地开发 → Debian 12 服务器 | ||
|
|
||
| ## 部署方式 | ||
|
|
||
| 本项目采用**本地编译 + 上传可执行文件**的方式部署,而非 Docker 镜像。 | ||
|
|
||
| ### 为什么选择这种方式? | ||
|
|
||
| | 方式 | 优点 | 缺点 | | ||
| |------|------|------| | ||
| | Docker 官方镜像 | 一键部署,简单 | 无法使用自定义修改的代码 | | ||
| | **本地编译上传** | 可部署自定义版本 | 需要手动处理依赖 | | ||
|
|
||
| ## 遇到的问题及解决方案 | ||
|
|
||
| ### 1. SSH 密钥加载失败 | ||
|
|
||
| **错误信息:** | ||
| ``` | ||
| Load key "xxx/id_ed25519": error in libcrypto | ||
| ``` | ||
|
Comment on lines
+22
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add language identifiers to fenced code blocks. Several code blocks are missing language specifiers, which triggers MD040 warnings and affects syntax highlighting. 📝 Suggested fixes for code block languages **错误信息:**
-```
+```text
Load key "xxx/id_ed25519": error in libcryptoAlso applies to: 41-43, 65-67, 93-95, 213-218 🧰 Tools🪛 markdownlint-cli2 (0.20.0)[warning] 22-22: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
|
|
||
| **原因:** Windows 系统的 SSH 密钥文件包含 CRLF 行尾符(`\r\n`),Linux 只认 LF(`\n`)。 | ||
|
|
||
| **解决方案:** | ||
| ```bash | ||
| # 去除 Windows 行尾符 | ||
| cat /path/to/id_ed25519 | tr -d '\r' > /tmp/fixed_key | ||
| chmod 600 /tmp/fixed_key | ||
| ssh -i /tmp/fixed_key user@server | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### 2. 编译成了 Windows 可执行文件 | ||
|
|
||
| **错误信息:** | ||
| ``` | ||
| /www1/new-api/new-api: cannot execute binary file: Exec format error | ||
| ``` | ||
|
|
||
| **原因:** 在 Windows 上直接运行 `go build`,默认编译成 Windows PE 格式。 | ||
|
|
||
| **解决方案:** 必须设置交叉编译环境变量: | ||
| ```bash | ||
| # 正确的 Linux 编译命令 | ||
| GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o new-api-linux . | ||
| ``` | ||
|
|
||
| **验证方法:** | ||
| ```bash | ||
| file new-api-linux | ||
| # 应显示:ELF 64-bit LSB executable, x86-64 | ||
| # 而不是:PE32+ executable (console) x86-64, for MS Windows | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### 3. 前端组件缺少导入导致黑屏 | ||
|
|
||
| **错误信息:** | ||
| ``` | ||
| ReferenceError: Space is not defined | ||
| ``` | ||
|
|
||
| **原因:** 某些组件使用了 `<Space>` 但忘记从 `@douyinfe/semi-ui` 导入。 | ||
|
|
||
| **受影响文件:** | ||
| - `web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx` | ||
| - `web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx` | ||
|
|
||
| **解决方案:** 在导入语句中添加 `Space`: | ||
| ```jsx | ||
| import { | ||
| Button, | ||
| // ... 其他组件 | ||
| Space, // 添加这行 | ||
| } from '@douyinfe/semi-ui'; | ||
| ``` | ||
|
|
||
| **预防措施:** | ||
| - 使用 ESLint 的 `no-undef` 规则 | ||
| - 提交前运行 `bun run build` 检查是否有错误 | ||
|
|
||
| --- | ||
|
|
||
| ### 4. 上传文件时服务正在运行 | ||
|
|
||
| **错误信息:** | ||
| ``` | ||
| scp: dest open "/www1/new-api/new-api": Failure | ||
| ``` | ||
|
|
||
| **原因:** Linux 下正在运行的可执行文件无法被覆盖。 | ||
|
|
||
| **解决方案:** 先停止服务再上传: | ||
| ```bash | ||
| ssh user@server "systemctl stop new-api" | ||
| scp new-api-linux user@server:/www1/new-api/new-api | ||
| ssh user@server "chmod +x /www1/new-api/new-api && systemctl start new-api" | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 完整部署流程 | ||
|
|
||
| ### 前置条件 | ||
|
|
||
| - 本地安装 Go 1.20+ | ||
| - 本地安装 Bun(用于前端构建) | ||
| - 服务器安装 Docker(用于 PostgreSQL 和 Redis) | ||
|
|
||
| ### 步骤 | ||
|
|
||
| ```bash | ||
| # 1. 构建前端 | ||
| cd web | ||
| bun install | ||
| bun run build | ||
|
|
||
| # 2. 编译后端(Linux 版本) | ||
| cd .. | ||
| GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o new-api-linux . | ||
|
|
||
| # 3. 上传到服务器 | ||
| ssh user@server "systemctl stop new-api 2>/dev/null; mkdir -p /www1/new-api/logs" | ||
| scp new-api-linux user@server:/www1/new-api/new-api | ||
| ssh user@server "chmod +x /www1/new-api/new-api" | ||
|
|
||
| # 4. 首次部署:启动数据库 | ||
| ssh user@server << 'EOF' | ||
| docker run -d \ | ||
| --name new-api-postgres \ | ||
| --restart always \ | ||
| -e POSTGRES_USER=newapi \ | ||
| -e POSTGRES_PASSWORD=YOUR_SECURE_PASSWORD \ | ||
| -e POSTGRES_DB=newapi \ | ||
| -p 127.0.0.1:5433:5432 \ | ||
| -v /www1/new-api/pg_data:/var/lib/postgresql/data \ | ||
| postgres:15 | ||
|
|
||
| docker run -d \ | ||
| --name new-api-redis \ | ||
| --restart always \ | ||
| -p 127.0.0.1:6380:6379 \ | ||
| redis:latest | ||
| EOF | ||
|
|
||
| # 5. 创建启动脚本 | ||
| ssh user@server << 'EOF' | ||
| cat > /www1/new-api/start.sh << 'SCRIPT' | ||
| #!/bin/bash | ||
| cd /www1/new-api | ||
| export SQL_DSN="postgresql://newapi:YOUR_SECURE_PASSWORD@127.0.0.1:5433/newapi" | ||
| export REDIS_CONN_STRING="redis://127.0.0.1:6380" | ||
| export TZ=Asia/Shanghai | ||
| exec ./new-api --port 9527 --log-dir /www1/new-api/logs | ||
| SCRIPT | ||
| chmod +x /www1/new-api/start.sh | ||
| EOF | ||
|
|
||
| # 6. 创建 systemd 服务 | ||
| ssh user@server << 'EOF' | ||
| cat > /etc/systemd/system/new-api.service << 'SERVICE' | ||
| [Unit] | ||
| Description=New API Service | ||
| After=network.target docker.service | ||
| Requires=docker.service | ||
|
|
||
| [Service] | ||
| Type=simple | ||
| User=root | ||
| WorkingDirectory=/www1/new-api | ||
| ExecStart=/www1/new-api/start.sh | ||
| Restart=always | ||
| RestartSec=5 | ||
|
|
||
| [Install] | ||
| WantedBy=multi-user.target | ||
| SERVICE | ||
| systemctl daemon-reload | ||
| systemctl enable new-api | ||
| systemctl start new-api | ||
| EOF | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 常用运维命令 | ||
|
|
||
| ```bash | ||
| # 查看服务状态 | ||
| systemctl status new-api | ||
|
|
||
| # 查看日志 | ||
| tail -f /www1/new-api/logs/*.log | ||
| journalctl -u new-api -f | ||
|
|
||
| # 重启服务 | ||
| systemctl restart new-api | ||
|
|
||
| # 数据库备份 | ||
| docker exec new-api-postgres pg_dump -U newapi newapi > backup_$(date +%Y%m%d).sql | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 目录结构 | ||
|
|
||
| ``` | ||
| /www1/new-api/ | ||
| ├── new-api # 可执行文件 | ||
| ├── start.sh # 启动脚本 | ||
| ├── logs/ # 日志目录 | ||
| └── pg_data/ # PostgreSQL 数据目录 | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,8 @@ type Token struct { | |
| UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota | ||
| Group string `json:"group" gorm:"default:''"` | ||
| CrossGroupRetry bool `json:"cross_group_retry"` // 跨分组重试,仅auto分组有效 | ||
| ActivatedTime int64 `json:"activated_time" gorm:"bigint;default:0"` | ||
| Duration int64 `json:"duration" gorm:"bigint;default:0"` | ||
| DeletedAt gorm.DeletedAt `gorm:"index"` | ||
| } | ||
|
|
||
|
|
@@ -87,6 +89,20 @@ func ValidateUserToken(key string) (token *Token, err error) { | |
| if token.Status != common.TokenStatusEnabled { | ||
| return token, errors.New("该令牌状态不可用") | ||
| } | ||
| if token.ExpiredTime == -1 && token.Duration > 0 && token.ActivatedTime == 0 { | ||
| token.ActivatedTime = common.GetTimestamp() | ||
| token.ExpiredTime = token.ActivatedTime + token.Duration | ||
| // Update DB | ||
| DB.Model(token).Select("activated_time", "expired_time").Updates(token) | ||
| // Update Redis if enabled | ||
| if common.RedisEnabled { | ||
| gopool.Go(func() { | ||
| if err := cacheSetToken(*token); err != nil { | ||
| common.SysLog("failed to update token cache: " + err.Error()) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
Comment on lines
+92
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling for DB update during token activation. The database update on line 96 does not check the returned error. If the update fails, the token's in-memory state will be inconsistent with the database, and the function will continue as if activation succeeded. Additionally, there's a potential race condition: if two concurrent requests validate the same sleeping token simultaneously, both may attempt activation before either persists. Consider using a transaction with row-level locking or an atomic conditional update (e.g., Proposed fix for error handling and atomicity if token.ExpiredTime == -1 && token.Duration > 0 && token.ActivatedTime == 0 {
- token.ActivatedTime = common.GetTimestamp()
- token.ExpiredTime = token.ActivatedTime + token.Duration
- // Update DB
- DB.Model(token).Select("activated_time", "expired_time").Updates(token)
+ now := common.GetTimestamp()
+ // Atomic conditional update to prevent race conditions
+ result := DB.Model(token).
+ Where("activated_time = ?", 0).
+ Select("activated_time", "expired_time").
+ Updates(map[string]interface{}{
+ "activated_time": now,
+ "expired_time": now + token.Duration,
+ })
+ if result.Error != nil {
+ common.SysLog("failed to activate token: " + result.Error.Error())
+ return token, errors.New("令牌激活失败")
+ }
+ if result.RowsAffected > 0 {
+ token.ActivatedTime = now
+ token.ExpiredTime = now + token.Duration
+ } else {
+ // Another request already activated; reload token
+ token, err = GetTokenByKey(key, true)
+ if err != nil {
+ return nil, err
+ }
+ }
// Update Redis if enabled🤖 Prompt for AI Agents |
||
| if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() { | ||
| if !common.RedisEnabled { | ||
| token.Status = common.TokenStatusExpired | ||
|
|
@@ -190,7 +206,7 @@ func (token *Token) Update() (err error) { | |
| } | ||
| }() | ||
| err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", | ||
| "model_limits_enabled", "model_limits", "allow_ips", "group", "cross_group_retry").Updates(token).Error | ||
| "model_limits_enabled", "model_limits", "allow_ips", "group", "cross_group_retry", "duration", "activated_time").Updates(token).Error | ||
| return err | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| "@visactor/react-vchart": "~1.8.8", | ||
| "@visactor/vchart": "~1.8.8", | ||
| "@visactor/vchart-semi-theme": "~1.8.8", | ||
| "antd": "^6.2.2", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat web/package.jsonRepository: QuantumNous/new-api Length of output: 2745 🌐 Web query:
💡 Result:
Sources: [1] [2] [3] 🏁 Script executed: # Search for existing style/CSS imports in the codebase
rg -i "antd|styles|css|import.*from.*antd" web/src --type js --type jsx --type ts --type tsx -A 2 -B 2 | head -50Repository: QuantumNous/new-api Length of output: 90 🏁 Script executed: # Check if there's a main entry point or setup file
fd -e main.js -e main.jsx -e main.ts -e main.tsx -e app.js -e app.jsx -e app.ts -e app.tsx web/srcRepository: QuantumNous/new-api Length of output: 396 🏁 Script executed: # Find main entry point in web/src
fd . web/src --max-depth 1 -e js -e jsx -e ts -e tsxRepository: QuantumNous/new-api Length of output: 96 🏁 Script executed: # Search for any existing antd or CSS imports
rg "import.*antd|import.*\.css|from.*antd" web/src --max-count 20Repository: QuantumNous/new-api Length of output: 437 🏁 Script executed: # Check vite.config file
fd vite.config webRepository: QuantumNous/new-api Length of output: 81 🏁 Script executed: cat -n web/src/index.jsx | head -30Repository: QuantumNous/new-api Length of output: 1475 🏁 Script executed: cat -n web/src/App.jsx | head -50Repository: QuantumNous/new-api Length of output: 2610 🏁 Script executed: cat -n web/vite.config.jsRepository: QuantumNous/new-api Length of output: 3587 🏁 Script executed: # Check if any antd components are actually imported/used
rg "from 'antd'|from \"antd\"|Button|Modal|Form|Input|Select" web/src --max-count 15Repository: QuantumNous/new-api Length of output: 50376 🏁 Script executed: rg "from\s+['\"]antd['\"]" web/srcRepository: QuantumNous/new-api Length of output: 45 🏁 Script executed: # Check if antd is imported/used anywhere at all
rg "import.*from.*['\"]antd['\"]" web/srcRepository: QuantumNous/new-api Length of output: 45 🏁 Script executed: # Verify the full index.jsx to see all imports
wc -l web/src/index.jsxRepository: QuantumNous/new-api Length of output: 83 🏁 Script executed: cat -n web/src/index.jsx | tail -20Repository: QuantumNous/new-api Length of output: 664 Add required Ant Design CSS import and clarify intent to use it. Ant Design v6.2.2 is compatible with React 18.2.0 and Vite 5.2.0, but the integration is incomplete: no CSS is imported in the entry point, so any antd components will render unstyled. Additionally, antd is not imported anywhere in the codebase, yet the project already uses Either add 🤖 Prompt for AI Agents |
||
| "axios": "1.12.0", | ||
| "clsx": "^2.1.1", | ||
| "country-flag-icons": "^1.5.19", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: QuantumNous/new-api
Length of output: 3423
🏁 Script executed:
Repository: QuantumNous/new-api
Length of output: 10206
🏁 Script executed:
Repository: QuantumNous/new-api
Length of output: 1514
🏁 Script executed:
Repository: QuantumNous/new-api
Length of output: 4514
Critical: Dockerfile produces Windows binary, breaking Docker deployments and CI/CD.
This Dockerfile compiles a Windows executable (
new-api-galaxy.exe) but the CI/CD workflows (docker-image-alpha.yml, docker-image-arm64.yml) are configured to build for Linux platforms (linux/amd64, linux/arm64) and push to Docker Hub. The docker-compose.yml depends on the Linux imagecalciumion/new-api:latest. This is a fundamental incompatibility that will break:Issues identified:
.exebinary incompatible with linux/amd64 and linux/arm64 targetsThe application is an HTTP server (Gin framework, port 3000 per main.go), not a Windows desktop app. Restore the Linux-compatible Dockerfile or clarify if this is a separate Windows build artifact (not for Docker deployment).
🤖 Prompt for AI Agents