Spaces:
Paused
Paused
github-actions[bot]
commited on
Commit
·
3392510
1
Parent(s):
8210f8c
Update from GitHub Actions
Browse files- .env.example +33 -0
- .gitattributes +2 -0
- Dockerfile +45 -0
- api/auth.go +43 -0
- api/handler.go +1358 -0
- api/login.go +134 -0
- api/token_handler.go +869 -0
- api/token_reset.go +96 -0
- config/config.go +75 -0
- config/redis.go +107 -0
- docker-compose.yml +38 -0
- go.mod +46 -0
- go.sum +120 -0
- main.go +288 -0
- middleware/concurrency.go +97 -0
- middleware/cors.go +15 -0
- pkg/logger/logger.go +61 -0
- static/augment.svg +10 -0
- templates/admin.html +1529 -0
- templates/login.html +275 -0
.env.example
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Redis 连接配置
|
| 2 |
+
# 格式: redis://default:password@host:port
|
| 3 |
+
# 必填项
|
| 4 |
+
REDIS_CONN_STRING=redis://default:your_secure_password@redis:6379
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# 面板访问密码
|
| 8 |
+
# 为了安全,此配置必填!
|
| 9 |
+
ACCESS_PWD=your_secure_access_password
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# API 认证令牌
|
| 13 |
+
# 如果设置,则所有 API 请求需要在 Authorization 头中提供此令牌
|
| 14 |
+
# 格式: 随机数字+字母组合即可
|
| 15 |
+
# 可选项,如果不设置则不启用认证
|
| 16 |
+
AUTH_TOKEN=your_secure_auth_token
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# API 请求前缀
|
| 20 |
+
# 可自定义,默认为空
|
| 21 |
+
ROUTE_PREFIX=your_api_prefix
|
| 22 |
+
|
| 23 |
+
# 调试模式
|
| 24 |
+
# 设置为 true 时启用更详细的日志输出
|
| 25 |
+
# 可选项,默认为 false
|
| 26 |
+
DEBUG=false
|
| 27 |
+
|
| 28 |
+
# 编码模式配置
|
| 29 |
+
# 仅用于开发和调试
|
| 30 |
+
# 可选项,默认为 false
|
| 31 |
+
CODING_MODE=false
|
| 32 |
+
CODING_TOKEN=
|
| 33 |
+
TENANT_URL=
|
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 使用官方 Golang 镜像作为构建环境
|
| 2 |
+
FROM golang:1.23-alpine AS builder
|
| 3 |
+
|
| 4 |
+
# 设置工作目录
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 复制 go.mod 和 go.sum 文件
|
| 8 |
+
COPY go.mod go.sum ./
|
| 9 |
+
|
| 10 |
+
# 下载依赖
|
| 11 |
+
RUN go mod download
|
| 12 |
+
|
| 13 |
+
# 复制源代码
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# 构建应用
|
| 17 |
+
RUN CGO_ENABLED=0 GOOS=linux go build -o augment2api
|
| 18 |
+
|
| 19 |
+
# 使用轻量级的 alpine 镜像
|
| 20 |
+
FROM alpine:latest
|
| 21 |
+
|
| 22 |
+
# 安装 ca-certificates 以支持 HTTPS
|
| 23 |
+
RUN apk --no-cache add ca-certificates tzdata
|
| 24 |
+
|
| 25 |
+
# 创建非 root 用户
|
| 26 |
+
RUN adduser -D -g '' appuser
|
| 27 |
+
|
| 28 |
+
# 从构建阶段复制二进制文件
|
| 29 |
+
COPY --from=builder /app/augment2api /app/augment2api
|
| 30 |
+
|
| 31 |
+
# 复制静态文件和模板
|
| 32 |
+
COPY --from=builder /app/templates /app/templates
|
| 33 |
+
|
| 34 |
+
# 设置工作目录
|
| 35 |
+
WORKDIR /app
|
| 36 |
+
|
| 37 |
+
# 使用非 root 用户运行
|
| 38 |
+
USER appuser
|
| 39 |
+
|
| 40 |
+
# 暴露端口
|
| 41 |
+
EXPOSE 7860
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# 运行应用
|
| 45 |
+
CMD ["/app/augment2api"]
|
api/auth.go
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/config"
|
| 5 |
+
"augment2api/pkg/logger"
|
| 6 |
+
"fmt"
|
| 7 |
+
"net/http"
|
| 8 |
+
"strings"
|
| 9 |
+
|
| 10 |
+
"github.com/gin-gonic/gin"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// AuthMiddleware 验证请求的Authorization header
|
| 14 |
+
func AuthMiddleware() gin.HandlerFunc {
|
| 15 |
+
return func(c *gin.Context) {
|
| 16 |
+
// 如果未设置 AuthToken,则不启用鉴权
|
| 17 |
+
if config.AppConfig.AuthToken == "" {
|
| 18 |
+
c.Next()
|
| 19 |
+
return
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
authHeader := c.GetHeader("Authorization")
|
| 23 |
+
if authHeader == "" {
|
| 24 |
+
logger.Log.Error("Authorization is empty")
|
| 25 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
| 26 |
+
c.Abort()
|
| 27 |
+
return
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// 支持 "Bearer <token>" 格式
|
| 31 |
+
token := strings.TrimPrefix(authHeader, "Bearer ")
|
| 32 |
+
token = strings.TrimSpace(token)
|
| 33 |
+
|
| 34 |
+
if token != config.AppConfig.AuthToken {
|
| 35 |
+
logger.Log.Error(fmt.Sprintf("Invalid authorization token:%s", token))
|
| 36 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization token"})
|
| 37 |
+
c.Abort()
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
c.Next()
|
| 42 |
+
}
|
| 43 |
+
}
|
api/handler.go
ADDED
|
@@ -0,0 +1,1358 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/config"
|
| 5 |
+
"augment2api/pkg/logger"
|
| 6 |
+
"bufio"
|
| 7 |
+
"crypto/sha256"
|
| 8 |
+
"encoding/json"
|
| 9 |
+
"fmt"
|
| 10 |
+
"io"
|
| 11 |
+
"log"
|
| 12 |
+
"math/rand"
|
| 13 |
+
"net/http"
|
| 14 |
+
"net/url"
|
| 15 |
+
"strings"
|
| 16 |
+
"sync"
|
| 17 |
+
"time"
|
| 18 |
+
|
| 19 |
+
"github.com/gin-gonic/gin"
|
| 20 |
+
"github.com/google/uuid"
|
| 21 |
+
"github.com/sirupsen/logrus"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
// OpenAIRequest OpenAI兼容的请求结构
|
| 25 |
+
type OpenAIRequest struct {
|
| 26 |
+
Model string `json:"model,omitempty"`
|
| 27 |
+
Messages []ChatMessage `json:"messages,omitempty"`
|
| 28 |
+
Stream bool `json:"stream,omitempty"`
|
| 29 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 30 |
+
MaxTokens int `json:"max_tokens,omitempty"`
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// OpenAIResponse OpenAI兼容的响应结构
|
| 34 |
+
type OpenAIResponse struct {
|
| 35 |
+
ID string `json:"id"`
|
| 36 |
+
Object string `json:"object"`
|
| 37 |
+
Created int64 `json:"created"`
|
| 38 |
+
Model string `json:"model"`
|
| 39 |
+
Choices []Choice `json:"choices"`
|
| 40 |
+
Usage Usage `json:"usage"`
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// OpenAIStreamResponse OpenAI兼容的流式响应结构
|
| 44 |
+
type OpenAIStreamResponse struct {
|
| 45 |
+
ID string `json:"id"`
|
| 46 |
+
Object string `json:"object"`
|
| 47 |
+
Created int64 `json:"created"`
|
| 48 |
+
Model string `json:"model"`
|
| 49 |
+
Choices []StreamChoice `json:"choices"`
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
type StreamChoice struct {
|
| 53 |
+
Index int `json:"index"`
|
| 54 |
+
Delta ChatMessage `json:"delta"`
|
| 55 |
+
FinishReason *string `json:"finish_reason"`
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
type Choice struct {
|
| 59 |
+
Index int `json:"index"`
|
| 60 |
+
Message ChatMessage `json:"message"`
|
| 61 |
+
FinishReason *string `json:"finish_reason"`
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
type ChatMessage struct {
|
| 65 |
+
Role string `json:"role"`
|
| 66 |
+
Content interface{} `json:"content"`
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// GetContent 添加一个辅助方法来获取消息内容
|
| 70 |
+
func (m ChatMessage) GetContent() string {
|
| 71 |
+
switch v := m.Content.(type) {
|
| 72 |
+
case string:
|
| 73 |
+
return v
|
| 74 |
+
case []interface{}:
|
| 75 |
+
var result string
|
| 76 |
+
for _, item := range v {
|
| 77 |
+
if contentMap, ok := item.(map[string]interface{}); ok {
|
| 78 |
+
if text, exists := contentMap["text"]; exists {
|
| 79 |
+
if textStr, ok := text.(string); ok {
|
| 80 |
+
result += textStr
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
return result
|
| 86 |
+
default:
|
| 87 |
+
return ""
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
type Usage struct {
|
| 92 |
+
PromptTokens int `json:"prompt_tokens"`
|
| 93 |
+
CompletionTokens int `json:"completion_tokens"`
|
| 94 |
+
TotalTokens int `json:"total_tokens"`
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// ToolDefinition 工具定义结构
|
| 98 |
+
type ToolDefinition struct {
|
| 99 |
+
Name string `json:"name"`
|
| 100 |
+
Description string `json:"description"`
|
| 101 |
+
InputSchemaJSON string `json:"input_schema_json"`
|
| 102 |
+
ToolSafety int `json:"tool_safety"`
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Node 节点结构
|
| 106 |
+
type Node struct {
|
| 107 |
+
ID int `json:"id"`
|
| 108 |
+
Type int `json:"type"`
|
| 109 |
+
Content string `json:"content"`
|
| 110 |
+
ToolUse ToolUse `json:"tool_use"`
|
| 111 |
+
AgentMemory AgentMemory `json:"agent_memory"`
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
type ToolUse struct {
|
| 115 |
+
ToolUseID string `json:"tool_use_id"`
|
| 116 |
+
ToolName string `json:"tool_name"`
|
| 117 |
+
InputJSON string `json:"input_json"`
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
type AgentMemory struct {
|
| 121 |
+
Content string `json:"content"`
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// AugmentRequest Augment API请求结构
|
| 125 |
+
type AugmentRequest struct {
|
| 126 |
+
ChatHistory []AugmentChatHistory `json:"chat_history"`
|
| 127 |
+
Message string `json:"message"`
|
| 128 |
+
AgentMemories string `json:"agent_memories"`
|
| 129 |
+
Mode string `json:"mode"`
|
| 130 |
+
Prefix string `json:"prefix"`
|
| 131 |
+
Suffix string `json:"suffix"`
|
| 132 |
+
Lang string `json:"lang"`
|
| 133 |
+
Path string `json:"path"`
|
| 134 |
+
UserGuideLines string `json:"user_guidelines"`
|
| 135 |
+
Blobs struct {
|
| 136 |
+
CheckpointID string `json:"checkpoint_id"`
|
| 137 |
+
AddedBlobs []interface{} `json:"added_blobs"`
|
| 138 |
+
DeletedBlobs []interface{} `json:"deleted_blobs"`
|
| 139 |
+
} `json:"blobs"`
|
| 140 |
+
UserGuidedBlobs []interface{} `json:"user_guided_blobs"`
|
| 141 |
+
ExternalSourceIds []interface{} `json:"external_source_ids"`
|
| 142 |
+
FeatureDetectionFlags struct {
|
| 143 |
+
SupportRawOutput bool `json:"support_raw_output"`
|
| 144 |
+
} `json:"feature_detection_flags"`
|
| 145 |
+
ToolDefinitions []ToolDefinition `json:"tool_definitions"`
|
| 146 |
+
Nodes []Node `json:"nodes"`
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
type AugmentChatHistory struct {
|
| 150 |
+
ResponseText string `json:"response_text"`
|
| 151 |
+
RequestMessage string `json:"request_message"`
|
| 152 |
+
RequestID string `json:"request_id"`
|
| 153 |
+
RequestNodes []Node `json:"request_nodes"`
|
| 154 |
+
ResponseNodes []Node `json:"response_nodes"`
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// AugmentResponse Augment API响应结构
|
| 158 |
+
type AugmentResponse struct {
|
| 159 |
+
Text string `json:"text"`
|
| 160 |
+
Done bool `json:"done"`
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// CodeResponse 用于解析从授权服务返回的代码
|
| 164 |
+
type CodeResponse struct {
|
| 165 |
+
Code string `json:"code"`
|
| 166 |
+
State string `json:"state"`
|
| 167 |
+
TenantURL string `json:"tenant_url"`
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// ModelObject OpenAI模型对象结构
|
| 171 |
+
type ModelObject struct {
|
| 172 |
+
ID string `json:"id"`
|
| 173 |
+
Object string `json:"object"`
|
| 174 |
+
Created int `json:"created"`
|
| 175 |
+
OwnedBy string `json:"owned_by"`
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// ModelsResponse OpenAI模型列表响应结构
|
| 179 |
+
type ModelsResponse struct {
|
| 180 |
+
Object string `json:"object"`
|
| 181 |
+
Data []ModelObject `json:"data"`
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// 全局变量
|
| 185 |
+
var (
|
| 186 |
+
accessToken string
|
| 187 |
+
tenantURL string
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
const (
|
| 191 |
+
// 错误信息
|
| 192 |
+
errBlocked = "Request blocked. Please reach out to support@augmentcode.com if you think this was a mistake."
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
// SetAuthInfo 设置认证信息
|
| 196 |
+
func SetAuthInfo(token, tenant string) {
|
| 197 |
+
accessToken = token
|
| 198 |
+
tenantURL = tenant
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// GetAuthInfo 获取认证信息
|
| 202 |
+
func GetAuthInfo() (string, string) {
|
| 203 |
+
if config.AppConfig.CodingMode == "true" {
|
| 204 |
+
// 调试模式
|
| 205 |
+
return config.AppConfig.CodingToken, config.AppConfig.TenantURL
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// 直接返回内存中的token和tenantURL
|
| 209 |
+
return accessToken, tenantURL
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
const (
|
| 213 |
+
// 默认提示,不加这个会导致Agent触发文件创建,回复截断
|
| 214 |
+
defaultPrompt = "Your are claude3.7, All replies cannot create, modify, or delete files, and must provide content directly!"
|
| 215 |
+
// 默认上下文,影响模型回复风格
|
| 216 |
+
defaultPrefix = "You are AI assistant,help me to solve problems!"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
// generateCheckpointID 生成一个基于时间戳的SHA-256哈希值作为CheckpointID
|
| 220 |
+
func generateCheckpointID() string {
|
| 221 |
+
// 使用当前时间戳作为输入
|
| 222 |
+
timestamp := fmt.Sprintf("%d", time.Now().UnixNano())
|
| 223 |
+
hash := sha256.New()
|
| 224 |
+
hash.Write([]byte(timestamp))
|
| 225 |
+
// 将哈希值转换为十六进制字符串
|
| 226 |
+
return fmt.Sprintf("%x", hash.Sum(nil))
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// generatePath 生成一个随机文件路径(暂时无用)
|
| 230 |
+
func generatePath() string {
|
| 231 |
+
extensions := []string{".txt", ".md", ".go", ".py", ".js", ".html", ".css"}
|
| 232 |
+
dirs := []string{"src", "docs", "test", "lib", "utils"}
|
| 233 |
+
dir := dirs[rand.Intn(len(dirs))]
|
| 234 |
+
ext := extensions[rand.Intn(len(extensions))]
|
| 235 |
+
filename := fmt.Sprintf("%x", rand.Int31())
|
| 236 |
+
return fmt.Sprintf("%s/%s%s", dir, filename, ext)
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// convertToAugmentRequest 将OpenAI请求转换为Augment请求
|
| 240 |
+
func convertToAugmentRequest(req OpenAIRequest) AugmentRequest {
|
| 241 |
+
// 确定模式和其他参数基于模型名称
|
| 242 |
+
mode := "CHAT" // 默认使用CHAT模式
|
| 243 |
+
userGuideLines := "must answer in Chinese."
|
| 244 |
+
includeToolDefinitions := false
|
| 245 |
+
includeDefaultPrompt := false
|
| 246 |
+
|
| 247 |
+
// 将模型名称转换为小写,然后检查后缀
|
| 248 |
+
modelLower := strings.ToLower(req.Model)
|
| 249 |
+
|
| 250 |
+
// 检查模型名称后缀 (不区分大小写)
|
| 251 |
+
if strings.HasSuffix(modelLower, "-chat") {
|
| 252 |
+
// 保持使用CHAT模式的默认设置
|
| 253 |
+
mode = "CHAT"
|
| 254 |
+
} else if strings.HasSuffix(modelLower, "-agent") {
|
| 255 |
+
// 使用AGENT模式
|
| 256 |
+
mode = "AGENT"
|
| 257 |
+
userGuideLines = "Answer in Chinese, do not use any tools, and for questions involving internet searches, please answer based on your existing knowledge."
|
| 258 |
+
includeToolDefinitions = true
|
| 259 |
+
includeDefaultPrompt = true
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
augmentReq := AugmentRequest{
|
| 263 |
+
Path: "", // 这个是关联的项目文件路径,暂时传空,不影响对话
|
| 264 |
+
Mode: mode, // 根据模型名称决定模式
|
| 265 |
+
Prefix: defaultPrefix, // 固定前缀,影响模型回复风格
|
| 266 |
+
Suffix: " ", // 固定后缀,暂时传空,不影响对话
|
| 267 |
+
Lang: detectLanguage(req), // 简单检测当前对话语言类型,不传好像回答有问题
|
| 268 |
+
Message: "", // 当前对话消息
|
| 269 |
+
UserGuideLines: userGuideLines, // 根据模型类型设置指南
|
| 270 |
+
// 初始化为空列表
|
| 271 |
+
ChatHistory: make([]AugmentChatHistory, 0),
|
| 272 |
+
Blobs: struct {
|
| 273 |
+
CheckpointID string `json:"checkpoint_id"`
|
| 274 |
+
AddedBlobs []interface{} `json:"added_blobs"`
|
| 275 |
+
DeletedBlobs []interface{} `json:"deleted_blobs"`
|
| 276 |
+
}{
|
| 277 |
+
CheckpointID: generateCheckpointID(),
|
| 278 |
+
AddedBlobs: make([]interface{}, 0),
|
| 279 |
+
DeletedBlobs: make([]interface{}, 0),
|
| 280 |
+
},
|
| 281 |
+
UserGuidedBlobs: make([]interface{}, 0),
|
| 282 |
+
ExternalSourceIds: make([]interface{}, 0),
|
| 283 |
+
FeatureDetectionFlags: struct {
|
| 284 |
+
SupportRawOutput bool `json:"support_raw_output"`
|
| 285 |
+
}{
|
| 286 |
+
SupportRawOutput: true,
|
| 287 |
+
},
|
| 288 |
+
ToolDefinitions: []ToolDefinition{}, // 初始化为空
|
| 289 |
+
Nodes: make([]Node, 0),
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// 根据模型类型决定是否包含工具定义
|
| 293 |
+
if includeToolDefinitions {
|
| 294 |
+
augmentReq.ToolDefinitions = getFullToolDefinitions()
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// 处理消息历史
|
| 298 |
+
if len(req.Messages) > 1 { // 有历史消息
|
| 299 |
+
// 每次处理一对消息(用户问题和助手回答)
|
| 300 |
+
for i := 0; i < len(req.Messages)-1; i += 2 {
|
| 301 |
+
if i+1 < len(req.Messages) {
|
| 302 |
+
userMsg := req.Messages[i]
|
| 303 |
+
assistantMsg := req.Messages[i+1]
|
| 304 |
+
|
| 305 |
+
chatHistory := AugmentChatHistory{
|
| 306 |
+
RequestMessage: userMsg.GetContent(),
|
| 307 |
+
ResponseText: assistantMsg.GetContent(),
|
| 308 |
+
RequestID: generateRequestID(), // 生成唯一的请求ID
|
| 309 |
+
RequestNodes: make([]Node, 0),
|
| 310 |
+
ResponseNodes: []Node{
|
| 311 |
+
{
|
| 312 |
+
ID: 0,
|
| 313 |
+
Type: 0,
|
| 314 |
+
Content: assistantMsg.GetContent(),
|
| 315 |
+
ToolUse: ToolUse{
|
| 316 |
+
ToolUseID: "",
|
| 317 |
+
ToolName: "",
|
| 318 |
+
InputJSON: "",
|
| 319 |
+
},
|
| 320 |
+
AgentMemory: AgentMemory{
|
| 321 |
+
Content: "",
|
| 322 |
+
},
|
| 323 |
+
},
|
| 324 |
+
},
|
| 325 |
+
}
|
| 326 |
+
augmentReq.ChatHistory = append(augmentReq.ChatHistory, chatHistory)
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
// 设置当前消息
|
| 332 |
+
if len(req.Messages) > 0 {
|
| 333 |
+
lastMsg := req.Messages[len(req.Messages)-1]
|
| 334 |
+
if includeDefaultPrompt {
|
| 335 |
+
augmentReq.Message = defaultPrompt + "\n" + lastMsg.GetContent()
|
| 336 |
+
} else {
|
| 337 |
+
augmentReq.Message = lastMsg.GetContent()
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
return augmentReq
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// generateRequestID 生成唯一的请求ID
|
| 345 |
+
func generateRequestID() string {
|
| 346 |
+
// 使用UUID v4生成唯一ID
|
| 347 |
+
return uuid.New().String()
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// detectLanguage 检测编程语言
|
| 351 |
+
func detectLanguage(req OpenAIRequest) string {
|
| 352 |
+
if len(req.Messages) == 0 {
|
| 353 |
+
return ""
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
content := req.Messages[len(req.Messages)-1].GetContent()
|
| 357 |
+
// 简单判断一下当前对话语言类型
|
| 358 |
+
if strings.Contains(strings.ToLower(content), "html") {
|
| 359 |
+
return "HTML"
|
| 360 |
+
} else if strings.Contains(strings.ToLower(content), "python") {
|
| 361 |
+
return "Python"
|
| 362 |
+
} else if strings.Contains(strings.ToLower(content), "javascript") {
|
| 363 |
+
return "JavaScript"
|
| 364 |
+
} else if strings.Contains(strings.ToLower(content), "go") {
|
| 365 |
+
return "Go"
|
| 366 |
+
} else if strings.Contains(strings.ToLower(content), "rust") {
|
| 367 |
+
return "Rust"
|
| 368 |
+
} else if strings.Contains(strings.ToLower(content), "java") {
|
| 369 |
+
return "Java"
|
| 370 |
+
} else if strings.Contains(strings.ToLower(content), "c++") {
|
| 371 |
+
return "C++"
|
| 372 |
+
} else if strings.Contains(strings.ToLower(content), "c#") {
|
| 373 |
+
return "C#"
|
| 374 |
+
} else if strings.Contains(strings.ToLower(content), "php") {
|
| 375 |
+
return "PHP"
|
| 376 |
+
} else if strings.Contains(strings.ToLower(content), "ruby") {
|
| 377 |
+
return "Ruby"
|
| 378 |
+
} else if strings.Contains(strings.ToLower(content), "swift") {
|
| 379 |
+
return "Swift"
|
| 380 |
+
} else if strings.Contains(strings.ToLower(content), "kotlin") {
|
| 381 |
+
return "Kotlin"
|
| 382 |
+
} else if strings.Contains(strings.ToLower(content), "typescript") {
|
| 383 |
+
return "TypeScript"
|
| 384 |
+
} else if strings.Contains(strings.ToLower(content), "c") {
|
| 385 |
+
return "C"
|
| 386 |
+
}
|
| 387 |
+
return "HTML"
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// getFullToolDefinitions 返回官方定义的完整工具定义列表
|
| 391 |
+
// TODO 验证实际作用
|
| 392 |
+
func getFullToolDefinitions() []ToolDefinition {
|
| 393 |
+
return []ToolDefinition{
|
| 394 |
+
{
|
| 395 |
+
Name: "web-search",
|
| 396 |
+
Description: "Search the web for information. Returns results in markdown format.\nEach result includes the URL, title, and a snippet from the page if available.\n\nThis tool uses Google's Custom Search API to find relevant web pages.",
|
| 397 |
+
InputSchemaJSON: `{
|
| 398 |
+
"description": "Input schema for the web search tool.",
|
| 399 |
+
"properties": {
|
| 400 |
+
"query": {
|
| 401 |
+
"description": "The search query to send.",
|
| 402 |
+
"title": "Query",
|
| 403 |
+
"type": "string"
|
| 404 |
+
},
|
| 405 |
+
"num_results": {
|
| 406 |
+
"default": 5,
|
| 407 |
+
"description": "Number of results to return",
|
| 408 |
+
"maximum": 10,
|
| 409 |
+
"minimum": 1,
|
| 410 |
+
"title": "Num Results",
|
| 411 |
+
"type": "integer"
|
| 412 |
+
}
|
| 413 |
+
},
|
| 414 |
+
"required": ["query"],
|
| 415 |
+
"title": "WebSearchInput",
|
| 416 |
+
"type": "object"
|
| 417 |
+
}`,
|
| 418 |
+
ToolSafety: 0,
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
Name: "web-fetch",
|
| 422 |
+
Description: "Fetches data from a webpage and converts it into Markdown.\n\n1. The tool takes in a URL and returns the content of the page in Markdown format;\n2. If the return is not valid Markdown, it means the tool cannot successfully parse this page.",
|
| 423 |
+
InputSchemaJSON: `{
|
| 424 |
+
"type": "object",
|
| 425 |
+
"properties": {
|
| 426 |
+
"url": {
|
| 427 |
+
"type": "string",
|
| 428 |
+
"description": "The URL to fetch."
|
| 429 |
+
}
|
| 430 |
+
},
|
| 431 |
+
"required": ["url"]
|
| 432 |
+
}`,
|
| 433 |
+
ToolSafety: 0,
|
| 434 |
+
},
|
| 435 |
+
{
|
| 436 |
+
Name: "codebase-retrieval",
|
| 437 |
+
Description: "This tool is Augment's context engine, the world's best codebase context engine. It:\n1. Takes in a natural language description of the code you are looking for;\n2. Uses a proprietary retrieval/embedding model suite that produces the highest-quality recall of relevant code snippets from across the codebase;\n3. Maintains a real-time index of the codebase, so the results are always up-to-date and reflects the current state of the codebase;\n4. Can retrieve across different programming languages;\n5. Only reflects the current state of the codebase on the disk, and has no information on version control or code history.",
|
| 438 |
+
InputSchemaJSON: `{
|
| 439 |
+
"type": "object",
|
| 440 |
+
"properties": {
|
| 441 |
+
"information_request": {
|
| 442 |
+
"type": "string",
|
| 443 |
+
"description": "A description of the information you need."
|
| 444 |
+
}
|
| 445 |
+
},
|
| 446 |
+
"required": ["information_request"]
|
| 447 |
+
}`,
|
| 448 |
+
ToolSafety: 1,
|
| 449 |
+
},
|
| 450 |
+
{
|
| 451 |
+
Name: "shell",
|
| 452 |
+
Description: "Execute a shell command.\n\n- You can use this tool to interact with the user's local version control system. Do not use the\nretrieval tool for that purpose.\n- If there is a more specific tool available that can perform the function, use that tool instead of\nthis one.\n\nThe OS is darwin. The shell is 'bash'.",
|
| 453 |
+
InputSchemaJSON: `{
|
| 454 |
+
"type": "object",
|
| 455 |
+
"properties": {
|
| 456 |
+
"command": {
|
| 457 |
+
"type": "string",
|
| 458 |
+
"description": "The shell command to execute."
|
| 459 |
+
}
|
| 460 |
+
},
|
| 461 |
+
"required": ["command"]
|
| 462 |
+
}`,
|
| 463 |
+
ToolSafety: 2,
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
Name: "str-replace-editor",
|
| 467 |
+
Description: "Custom editing tool for viewing, creating and editing files\n* `path` is a file path relative to the workspace root\n* command `view` displays the result of applying `cat -n`.\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* `insert` and `str_replace` commands output a snippet of the edited section for each entry. This snippet reflects the final state of the file after all edits and IDE auto-formatting have been applied.\n\n\nNotes for using the `str_replace` command:\n* Use the `str_replace_entries` parameter with an array of objects\n* Each object should have `old_str`, `new_str`, `old_str_start_line_number` and `old_str_end_line_number` properties\n* The `old_str_start_line_number` and `old_str_end_line_number` parameters are 1-based line numbers\n* Both `old_str_start_line_number` and `old_str_end_line_number` are INCLUSIVE\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespace!\n* Empty `old_str` is allowed only when the file is empty or contains only whitespaces\n* It is important to specify `old_str_start_line_number` and `old_str_end_line_number` to disambiguate between multiple occurrences of `old_str` in the file\n* Make sure that `old_str_start_line_number` and `old_str_end_line_number` do not overlap with other entries in `str_replace_entries`\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`. Can be an empty string to delete content\n\nNotes for using the `insert` command:\n* Use the `insert_line_entries` parameter with an array of objects\n* Each object should have `insert_line` and `new_str` properties\n* The `insert_line` parameter specifies the line number after which to insert the new string\n* The `insert_line` parameter is 1-based line number\n* To insert at the very beginning of the file, use `insert_line: 0`\n\nNotes for using the `view` command:\n* Strongly prefer to use larger ranges of at least 1000 lines when scanning through files. One call with large range is much more efficient than many calls with small ranges\n* Prefer to use grep instead of view when looking for a specific symbol in the file\n\nIMPORTANT:\n* This is the only tool you should use for editing files.\n* If it fails try your best to fix inputs and retry.\n* DO NOT fall back to removing the whole file and recreating it from scratch.\n* DO NOT use sed or any other command line tools for editing files.\n* Try to fit as many edits in one tool call as possible\n* Use view command to read the file before editing it.\n",
|
| 468 |
+
InputSchemaJSON: `{
|
| 469 |
+
"type": "object",
|
| 470 |
+
"properties": {
|
| 471 |
+
"command": {
|
| 472 |
+
"type": "string",
|
| 473 |
+
"enum": ["view", "str_replace", "insert"],
|
| 474 |
+
"description": "The commands to run. Allowed options are: 'view', 'str_replace', 'insert'."
|
| 475 |
+
},
|
| 476 |
+
"path": {
|
| 477 |
+
"description": "Full path to file relative to the workspace root, e.g. 'services/api_proxy/file.py' or 'services/api_proxy'.",
|
| 478 |
+
"type": "string"
|
| 479 |
+
},
|
| 480 |
+
"view_range": {
|
| 481 |
+
"description": "Optional parameter of 'view' command when 'path' points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting '[start_line, -1]' shows all lines from 'start_line' to the end of the file.",
|
| 482 |
+
"type": "array",
|
| 483 |
+
"items": {
|
| 484 |
+
"type": "integer"
|
| 485 |
+
}
|
| 486 |
+
},
|
| 487 |
+
"insert_line_entries": {
|
| 488 |
+
"description": "Required parameter of 'insert' command. A list of entries to insert. Each entry is a dictionary with keys 'insert_line' and 'new_str'.",
|
| 489 |
+
"type": "array",
|
| 490 |
+
"items": {
|
| 491 |
+
"type": "object",
|
| 492 |
+
"properties": {
|
| 493 |
+
"insert_line": {
|
| 494 |
+
"description": "The line number after which to insert the new string. This line number is relative to the state of the file before any insertions in the current tool call have been applied.",
|
| 495 |
+
"type": "integer"
|
| 496 |
+
},
|
| 497 |
+
"new_str": {
|
| 498 |
+
"description": "The string to insert. Can be an empty string.",
|
| 499 |
+
"type": "string"
|
| 500 |
+
}
|
| 501 |
+
},
|
| 502 |
+
"required": ["insert_line", "new_str"]
|
| 503 |
+
}
|
| 504 |
+
},
|
| 505 |
+
"str_replace_entries": {
|
| 506 |
+
"description": "Required parameter of 'str_replace' command. A list of entries to replace. Each entry is a dictionary with keys 'old_str', 'old_str_start_line_number', 'old_str_end_line_number' and 'new_str'. 'old_str' from different entries should not overlap.",
|
| 507 |
+
"type": "array",
|
| 508 |
+
"items": {
|
| 509 |
+
"type": "object",
|
| 510 |
+
"properties": {
|
| 511 |
+
"old_str": {
|
| 512 |
+
"description": "The string in 'path' to replace.",
|
| 513 |
+
"type": "string"
|
| 514 |
+
},
|
| 515 |
+
"old_str_start_line_number": {
|
| 516 |
+
"description": "The line number of the first line of 'old_str' in the file. This is used to disambiguate between multiple occurrences of 'old_str' in the file.",
|
| 517 |
+
"type": "integer"
|
| 518 |
+
},
|
| 519 |
+
"old_str_end_line_number": {
|
| 520 |
+
"description": "The line number of the last line of 'old_str' in the file. This is used to disambiguate between multiple occurrences of 'old_str' in the file.",
|
| 521 |
+
"type": "integer"
|
| 522 |
+
},
|
| 523 |
+
"new_str": {
|
| 524 |
+
"description": "The string to replace 'old_str' with. Can be an empty string to delete content.",
|
| 525 |
+
"type": "string"
|
| 526 |
+
}
|
| 527 |
+
},
|
| 528 |
+
"required": ["old_str", "new_str", "old_str_start_line_number", "old_str_end_line_number"]
|
| 529 |
+
}
|
| 530 |
+
}
|
| 531 |
+
},
|
| 532 |
+
"required": ["command", "path"]
|
| 533 |
+
}`,
|
| 534 |
+
ToolSafety: 1,
|
| 535 |
+
},
|
| 536 |
+
{
|
| 537 |
+
Name: "save-file",
|
| 538 |
+
Description: "Save a file.",
|
| 539 |
+
InputSchemaJSON: `{
|
| 540 |
+
"type": "object",
|
| 541 |
+
"properties": {
|
| 542 |
+
"file_path": {
|
| 543 |
+
"type": "string",
|
| 544 |
+
"description": "The path of the file to save."
|
| 545 |
+
},
|
| 546 |
+
"file_content": {
|
| 547 |
+
"type": "string",
|
| 548 |
+
"description": "The content of the file to save."
|
| 549 |
+
},
|
| 550 |
+
"add_last_line_newline": {
|
| 551 |
+
"type": "boolean",
|
| 552 |
+
"description": "Whether to add a newline at the end of the file (default: true)."
|
| 553 |
+
}
|
| 554 |
+
},
|
| 555 |
+
"required": ["file_path", "file_content"]
|
| 556 |
+
}`,
|
| 557 |
+
ToolSafety: 1,
|
| 558 |
+
},
|
| 559 |
+
{
|
| 560 |
+
Name: "launch-process",
|
| 561 |
+
Description: "Launch a new process.\nIf wait is specified, waits up to that many seconds for the process to complete.\nIf the process completes within wait seconds, returns its output.\nIf it doesn't complete within wait seconds, returns partial output and process ID.\nIf wait is not specified, returns immediately with just the process ID.\nThe process's stdin is always enbled, so you can use write_process to send input if needed.",
|
| 562 |
+
InputSchemaJSON: `{
|
| 563 |
+
"type": "object",
|
| 564 |
+
"properties": {
|
| 565 |
+
"command": {
|
| 566 |
+
"type": "string",
|
| 567 |
+
"description": "The shell command to execute"
|
| 568 |
+
},
|
| 569 |
+
"wait": {
|
| 570 |
+
"type": "number",
|
| 571 |
+
"description": "Optional: number of seconds to wait for the command to complete."
|
| 572 |
+
},
|
| 573 |
+
"cwd": {
|
| 574 |
+
"type": "string",
|
| 575 |
+
"description": "Working directory for the command. If not supplied, uses the current working directory."
|
| 576 |
+
}
|
| 577 |
+
},
|
| 578 |
+
"required": ["command"]
|
| 579 |
+
}`,
|
| 580 |
+
ToolSafety: 2,
|
| 581 |
+
},
|
| 582 |
+
{
|
| 583 |
+
Name: "read-process",
|
| 584 |
+
Description: "Read output from a terminal.",
|
| 585 |
+
InputSchemaJSON: `{
|
| 586 |
+
"type": "object",
|
| 587 |
+
"properties": {
|
| 588 |
+
"terminal_id": {
|
| 589 |
+
"type": "number",
|
| 590 |
+
"description": "Terminal ID to read from."
|
| 591 |
+
}
|
| 592 |
+
},
|
| 593 |
+
"required": ["terminal_id"]
|
| 594 |
+
}`,
|
| 595 |
+
ToolSafety: 1,
|
| 596 |
+
},
|
| 597 |
+
{
|
| 598 |
+
Name: "kill-process",
|
| 599 |
+
Description: "Kill a process by its terminal ID.",
|
| 600 |
+
InputSchemaJSON: `{
|
| 601 |
+
"type": "object",
|
| 602 |
+
"properties": {
|
| 603 |
+
"terminal_id": {
|
| 604 |
+
"type": "number",
|
| 605 |
+
"description": "Terminal ID to kill."
|
| 606 |
+
}
|
| 607 |
+
},
|
| 608 |
+
"required": ["terminal_id"]
|
| 609 |
+
}`,
|
| 610 |
+
ToolSafety: 1,
|
| 611 |
+
},
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// AuthHandler 处理授权请求
|
| 616 |
+
func AuthHandler(c *gin.Context, authorizeURL string) {
|
| 617 |
+
c.JSON(http.StatusOK, gin.H{
|
| 618 |
+
"authorize_url": authorizeURL,
|
| 619 |
+
})
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
// CallbackHandler 处理回调请求
|
| 623 |
+
func CallbackHandler(c *gin.Context, getAccessTokenFunc func(string, string, string) (string, error)) {
|
| 624 |
+
// 1. 解析请求数据
|
| 625 |
+
var codeResp CodeResponse
|
| 626 |
+
if err := c.ShouldBindJSON(&codeResp); err != nil {
|
| 627 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
|
| 628 |
+
return
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
// 2. 使用授权码获取访问令牌
|
| 632 |
+
token, err := getAccessTokenFunc(codeResp.TenantURL, "", codeResp.Code)
|
| 633 |
+
if err != nil {
|
| 634 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 635 |
+
return
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// 3. 保存令牌和租户URL
|
| 639 |
+
SetAuthInfo(token, codeResp.TenantURL)
|
| 640 |
+
|
| 641 |
+
// 4. 保存到Redis
|
| 642 |
+
if err := SaveTokenToRedis(token, codeResp.TenantURL); err != nil {
|
| 643 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token到Redis失败: " + err.Error()})
|
| 644 |
+
return
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
// 5. 返回成功响应
|
| 648 |
+
c.JSON(http.StatusOK, gin.H{
|
| 649 |
+
"status": "success",
|
| 650 |
+
"token": token,
|
| 651 |
+
})
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
// ModelsHandler 处理模型请求
|
| 655 |
+
func ModelsHandler(c *gin.Context) {
|
| 656 |
+
// 这里直接返回写死的模型
|
| 657 |
+
response := ModelsResponse{
|
| 658 |
+
Object: "list",
|
| 659 |
+
Data: []ModelObject{
|
| 660 |
+
{
|
| 661 |
+
ID: "claude-3.7-agent",
|
| 662 |
+
Object: "model",
|
| 663 |
+
Created: 1708387200,
|
| 664 |
+
OwnedBy: "anthropic",
|
| 665 |
+
},
|
| 666 |
+
{
|
| 667 |
+
ID: "augment-chat",
|
| 668 |
+
Object: "model",
|
| 669 |
+
Created: 1708387200,
|
| 670 |
+
OwnedBy: "augment",
|
| 671 |
+
},
|
| 672 |
+
},
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
c.JSON(http.StatusOK, response)
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
// ChatCompletionsHandler 处理OpenAI兼容的聊天完成请求
|
| 679 |
+
func ChatCompletionsHandler(c *gin.Context) {
|
| 680 |
+
// 获取请求数据
|
| 681 |
+
var req OpenAIRequest
|
| 682 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 683 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
|
| 684 |
+
// 确保在错误情况下也清理请求状态
|
| 685 |
+
cleanupRequestStatus(c)
|
| 686 |
+
return
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
// 转换为Augment请求格式
|
| 690 |
+
augmentReq := convertToAugmentRequest(req)
|
| 691 |
+
|
| 692 |
+
// 处理流式请求
|
| 693 |
+
if req.Stream {
|
| 694 |
+
handleStreamRequest(c, augmentReq, req.Model)
|
| 695 |
+
return
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
// 处理非流式请���
|
| 699 |
+
handleNonStreamRequest(c, augmentReq, req.Model)
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
// 处理流式请求
|
| 703 |
+
func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
|
| 704 |
+
defer cleanupRequestStatus(c)
|
| 705 |
+
|
| 706 |
+
c.Header("Content-Type", "text/event-stream")
|
| 707 |
+
c.Header("Cache-Control", "no-cache")
|
| 708 |
+
c.Header("Connection", "keep-alive")
|
| 709 |
+
|
| 710 |
+
// 从上下文中获取token和tenant_url
|
| 711 |
+
tokenInterface, exists := c.Get("token")
|
| 712 |
+
tenantURLInterface, exists2 := c.Get("tenant_url")
|
| 713 |
+
|
| 714 |
+
var token, tenant string
|
| 715 |
+
|
| 716 |
+
if exists && exists2 {
|
| 717 |
+
token, _ = tokenInterface.(string)
|
| 718 |
+
tenant, _ = tenantURLInterface.(string)
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
// 如果上下文中没有,则使用GetAuthInfo获取
|
| 722 |
+
if token == "" || tenant == "" {
|
| 723 |
+
token, tenant = GetAuthInfo()
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
if token == "" || tenant == "" {
|
| 727 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "无可用Token,请先在管理页面获取"})
|
| 728 |
+
return
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
// 增加token使用计数
|
| 732 |
+
incrementTokenUsage(token, model)
|
| 733 |
+
|
| 734 |
+
// 准备请求数据
|
| 735 |
+
jsonData, err := json.Marshal(augmentReq)
|
| 736 |
+
if err != nil {
|
| 737 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
|
| 738 |
+
return
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
// 创建请求
|
| 742 |
+
req, err := http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
|
| 743 |
+
if err != nil {
|
| 744 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
|
| 745 |
+
return
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
req.Header.Set("Content-Type", "application/json")
|
| 749 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 750 |
+
req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
|
| 751 |
+
req.Header.Set("x-api-version", "2")
|
| 752 |
+
req.Header.Set("x-request-id", uuid.New().String())
|
| 753 |
+
req.Header.Set("x-request-session-id", uuid.New().String())
|
| 754 |
+
|
| 755 |
+
// 使用createHTTPClient创建客户端
|
| 756 |
+
client := createHTTPClient()
|
| 757 |
+
|
| 758 |
+
// 设置刷新器以确保数据立即发送
|
| 759 |
+
flusher, ok := c.Writer.(http.Flusher)
|
| 760 |
+
if !ok {
|
| 761 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "流式传输不支持"})
|
| 762 |
+
return
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
// 第一次尝试使用原始模式请求
|
| 766 |
+
resp, err := client.Do(req)
|
| 767 |
+
if err != nil {
|
| 768 |
+
logger.Log.WithFields(logrus.Fields{
|
| 769 |
+
"error": err.Error(),
|
| 770 |
+
"mode": augmentReq.Mode,
|
| 771 |
+
}).Error("请求失败")
|
| 772 |
+
|
| 773 |
+
// 切换到CHAT模式
|
| 774 |
+
augmentReq.Mode = "CHAT"
|
| 775 |
+
augmentReq.UserGuideLines = "使用中文回答"
|
| 776 |
+
augmentReq.ToolDefinitions = []ToolDefinition{}
|
| 777 |
+
|
| 778 |
+
// 重新准备请求数据
|
| 779 |
+
jsonData, err = json.Marshal(augmentReq)
|
| 780 |
+
if err != nil {
|
| 781 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
|
| 782 |
+
return
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
// 创建新的请求
|
| 786 |
+
req, err = http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
|
| 787 |
+
if err != nil {
|
| 788 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
|
| 789 |
+
return
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
// 设置相同的请求头
|
| 793 |
+
req.Header.Set("Content-Type", "application/json")
|
| 794 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 795 |
+
req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
|
| 796 |
+
req.Header.Set("x-api-version", "2")
|
| 797 |
+
req.Header.Set("x-request-id", uuid.New().String())
|
| 798 |
+
req.Header.Set("x-request-session-id", uuid.New().String())
|
| 799 |
+
|
| 800 |
+
// 重新发送请求
|
| 801 |
+
resp, err = client.Do(req)
|
| 802 |
+
if err != nil {
|
| 803 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
|
| 804 |
+
return
|
| 805 |
+
}
|
| 806 |
+
}
|
| 807 |
+
defer resp.Body.Close()
|
| 808 |
+
|
| 809 |
+
// 检查响应状态码
|
| 810 |
+
if resp.StatusCode != http.StatusOK {
|
| 811 |
+
body, err := io.ReadAll(resp.Body)
|
| 812 |
+
errMsg := "Augment response error"
|
| 813 |
+
if err == nil {
|
| 814 |
+
errMsg = errMsg + ": " + string(body)
|
| 815 |
+
}
|
| 816 |
+
c.JSON(resp.StatusCode, gin.H{"error": errMsg})
|
| 817 |
+
return
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
// 读取并转发响应
|
| 821 |
+
reader := bufio.NewReader(resp.Body)
|
| 822 |
+
responseID := fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
|
| 823 |
+
|
| 824 |
+
var fullText string
|
| 825 |
+
var hasError bool
|
| 826 |
+
|
| 827 |
+
for {
|
| 828 |
+
line, err := reader.ReadString('\n')
|
| 829 |
+
if err != nil {
|
| 830 |
+
if err == io.EOF {
|
| 831 |
+
break
|
| 832 |
+
}
|
| 833 |
+
logger.Log.WithFields(logrus.Fields{
|
| 834 |
+
"error": err.Error(),
|
| 835 |
+
"mode": augmentReq.Mode,
|
| 836 |
+
}).Error("读取响应失败")
|
| 837 |
+
|
| 838 |
+
// 切换到CHAT模式
|
| 839 |
+
if augmentReq.Mode != "CHAT" {
|
| 840 |
+
logger.Log.WithFields(logrus.Fields{
|
| 841 |
+
"error": err.Error(),
|
| 842 |
+
"mode": augmentReq.Mode,
|
| 843 |
+
}).Info("切换到CHAT模式")
|
| 844 |
+
|
| 845 |
+
augmentReq.Mode = "CHAT"
|
| 846 |
+
augmentReq.UserGuideLines = "使用中文回答"
|
| 847 |
+
augmentReq.ToolDefinitions = []ToolDefinition{}
|
| 848 |
+
|
| 849 |
+
// 重新准备请求数据
|
| 850 |
+
jsonData, err = json.Marshal(augmentReq)
|
| 851 |
+
if err != nil {
|
| 852 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
|
| 853 |
+
return
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
// 创建新的请求
|
| 857 |
+
req, err = http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
|
| 858 |
+
if err != nil {
|
| 859 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
|
| 860 |
+
return
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
// 设置相同的请求头
|
| 864 |
+
req.Header.Set("Content-Type", "application/json")
|
| 865 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 866 |
+
req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
|
| 867 |
+
req.Header.Set("x-api-version", "2")
|
| 868 |
+
req.Header.Set("x-request-id", uuid.New().String())
|
| 869 |
+
req.Header.Set("x-request-session-id", uuid.New().String())
|
| 870 |
+
|
| 871 |
+
// 重新发送请求
|
| 872 |
+
resp, err = client.Do(req)
|
| 873 |
+
if err != nil {
|
| 874 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
|
| 875 |
+
return
|
| 876 |
+
}
|
| 877 |
+
defer resp.Body.Close()
|
| 878 |
+
|
| 879 |
+
// 检查响应状态码
|
| 880 |
+
if resp.StatusCode != http.StatusOK {
|
| 881 |
+
body, err := io.ReadAll(resp.Body)
|
| 882 |
+
errMsg := "Augment response error"
|
| 883 |
+
if err == nil {
|
| 884 |
+
errMsg = errMsg + ": " + string(body)
|
| 885 |
+
}
|
| 886 |
+
c.JSON(resp.StatusCode, gin.H{"error": errMsg})
|
| 887 |
+
return
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
// 重新设置reader
|
| 891 |
+
reader = bufio.NewReader(resp.Body)
|
| 892 |
+
continue
|
| 893 |
+
}
|
| 894 |
+
break
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
line = strings.TrimSpace(line)
|
| 898 |
+
if line == "" {
|
| 899 |
+
continue
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
var augmentResp AugmentResponse
|
| 903 |
+
if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
|
| 904 |
+
log.Printf("解析响应失败: %v", err)
|
| 905 |
+
continue
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
// 检查响应内容是否包含错误信息
|
| 909 |
+
if strings.Contains(augmentResp.Text, errBlocked) {
|
| 910 |
+
hasError = true
|
| 911 |
+
|
| 912 |
+
// 将当前token加入冷却队列,冷却时间10分钟
|
| 913 |
+
logger.Log.WithFields(logrus.Fields{
|
| 914 |
+
"token": token,
|
| 915 |
+
"mode": augmentReq.Mode,
|
| 916 |
+
}).Info("检测到block信息,将token加入冷却队列10分钟")
|
| 917 |
+
|
| 918 |
+
err := SetTokenCoolStatus(token, 10*time.Minute)
|
| 919 |
+
if err != nil {
|
| 920 |
+
logger.Log.WithFields(logrus.Fields{
|
| 921 |
+
"token": token,
|
| 922 |
+
"error": err.Error(),
|
| 923 |
+
}).Error("将token加入冷却队列失败")
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
break
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
fullText += augmentResp.Text
|
| 930 |
+
|
| 931 |
+
// 创建OpenAI兼容的流式响应
|
| 932 |
+
streamResp := OpenAIStreamResponse{
|
| 933 |
+
ID: responseID,
|
| 934 |
+
Object: "chat.completion.chunk",
|
| 935 |
+
Created: time.Now().Unix(),
|
| 936 |
+
Model: model,
|
| 937 |
+
Choices: []StreamChoice{
|
| 938 |
+
{
|
| 939 |
+
Index: 0,
|
| 940 |
+
Delta: ChatMessage{
|
| 941 |
+
Role: "assistant",
|
| 942 |
+
Content: augmentResp.Text,
|
| 943 |
+
},
|
| 944 |
+
FinishReason: nil,
|
| 945 |
+
},
|
| 946 |
+
},
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
// 如果是最后一条消息,设置完成原因
|
| 950 |
+
if augmentResp.Done {
|
| 951 |
+
finishReason := "stop"
|
| 952 |
+
streamResp.Choices[0].FinishReason = &finishReason
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
// 序列化并发送响应
|
| 956 |
+
jsonResp, err := json.Marshal(streamResp)
|
| 957 |
+
if err != nil {
|
| 958 |
+
log.Printf("序列化响应失败: %v", err)
|
| 959 |
+
continue
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonResp)
|
| 963 |
+
flusher.Flush()
|
| 964 |
+
|
| 965 |
+
// 如果完成,发送最后的[DONE]标记
|
| 966 |
+
if augmentResp.Done {
|
| 967 |
+
fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
|
| 968 |
+
flusher.Flush()
|
| 969 |
+
break
|
| 970 |
+
}
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
// 如果检测到错误信息,尝试切换到CHAT模式重新请求
|
| 974 |
+
if hasError && augmentReq.Mode != "CHAT" {
|
| 975 |
+
logger.Log.WithFields(logrus.Fields{
|
| 976 |
+
"mode": augmentReq.Mode,
|
| 977 |
+
}).Info("检测到block信息,尝试切换到 CHAT 模式回复!")
|
| 978 |
+
|
| 979 |
+
// 切换到CHAT模式
|
| 980 |
+
augmentReq.Mode = "CHAT"
|
| 981 |
+
augmentReq.UserGuideLines = "使用中文回答"
|
| 982 |
+
augmentReq.ToolDefinitions = []ToolDefinition{}
|
| 983 |
+
|
| 984 |
+
// 重新准备请求数据
|
| 985 |
+
jsonData, err = json.Marshal(augmentReq)
|
| 986 |
+
if err != nil {
|
| 987 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
|
| 988 |
+
return
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
// 创建新的请求
|
| 992 |
+
req, err = http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
|
| 993 |
+
if err != nil {
|
| 994 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
|
| 995 |
+
return
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
// 设置相同的请求头
|
| 999 |
+
req.Header.Set("Content-Type", "application/json")
|
| 1000 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 1001 |
+
req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
|
| 1002 |
+
req.Header.Set("x-api-version", "2")
|
| 1003 |
+
req.Header.Set("x-request-id", uuid.New().String())
|
| 1004 |
+
req.Header.Set("x-request-session-id", uuid.New().String())
|
| 1005 |
+
|
| 1006 |
+
// 重新发送请求
|
| 1007 |
+
resp, err = client.Do(req)
|
| 1008 |
+
if err != nil {
|
| 1009 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
|
| 1010 |
+
return
|
| 1011 |
+
}
|
| 1012 |
+
defer resp.Body.Close()
|
| 1013 |
+
|
| 1014 |
+
// 检查响应状态码
|
| 1015 |
+
if resp.StatusCode != http.StatusOK {
|
| 1016 |
+
body, err := io.ReadAll(resp.Body)
|
| 1017 |
+
errMsg := "Augment response error"
|
| 1018 |
+
if err == nil {
|
| 1019 |
+
errMsg = errMsg + ": " + string(body)
|
| 1020 |
+
}
|
| 1021 |
+
c.JSON(resp.StatusCode, gin.H{"error": errMsg})
|
| 1022 |
+
return
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
// 读取并转发响应
|
| 1026 |
+
reader = bufio.NewReader(resp.Body)
|
| 1027 |
+
responseID = fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
|
| 1028 |
+
|
| 1029 |
+
fullText = ""
|
| 1030 |
+
for {
|
| 1031 |
+
line, err := reader.ReadString('\n')
|
| 1032 |
+
if err != nil {
|
| 1033 |
+
if err == io.EOF {
|
| 1034 |
+
break
|
| 1035 |
+
}
|
| 1036 |
+
log.Printf("读取响应失败: %v", err)
|
| 1037 |
+
break
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
line = strings.TrimSpace(line)
|
| 1041 |
+
if line == "" {
|
| 1042 |
+
continue
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
var augmentResp AugmentResponse
|
| 1046 |
+
if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
|
| 1047 |
+
log.Printf("解析响应失败: %v", err)
|
| 1048 |
+
continue
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
fullText += augmentResp.Text
|
| 1052 |
+
|
| 1053 |
+
// 创建OpenAI兼容的流式响应
|
| 1054 |
+
streamResp := OpenAIStreamResponse{
|
| 1055 |
+
ID: responseID,
|
| 1056 |
+
Object: "chat.completion.chunk",
|
| 1057 |
+
Created: time.Now().Unix(),
|
| 1058 |
+
Model: model,
|
| 1059 |
+
Choices: []StreamChoice{
|
| 1060 |
+
{
|
| 1061 |
+
Index: 0,
|
| 1062 |
+
Delta: ChatMessage{
|
| 1063 |
+
Role: "assistant",
|
| 1064 |
+
Content: augmentResp.Text,
|
| 1065 |
+
},
|
| 1066 |
+
FinishReason: nil,
|
| 1067 |
+
},
|
| 1068 |
+
},
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
// 如果是最后一条消息,设置完成原因
|
| 1072 |
+
if augmentResp.Done {
|
| 1073 |
+
finishReason := "stop"
|
| 1074 |
+
streamResp.Choices[0].FinishReason = &finishReason
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
// 序列化并发送响应
|
| 1078 |
+
jsonResp, err := json.Marshal(streamResp)
|
| 1079 |
+
if err != nil {
|
| 1080 |
+
log.Printf("序列化响应失败: %v", err)
|
| 1081 |
+
continue
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonResp)
|
| 1085 |
+
flusher.Flush()
|
| 1086 |
+
|
| 1087 |
+
// 如果完成,发送最后的[DONE]标记
|
| 1088 |
+
if augmentResp.Done {
|
| 1089 |
+
fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
|
| 1090 |
+
flusher.Flush()
|
| 1091 |
+
break
|
| 1092 |
+
}
|
| 1093 |
+
}
|
| 1094 |
+
}
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
// estimateTokenCount 粗略估计文本中的token数量
|
| 1098 |
+
// 这是一个简单的估算方法,实际token数量取决于具体的分词算法
|
| 1099 |
+
func estimateTokenCount(text string) int {
|
| 1100 |
+
// 英文单词和标点符号大约是1个token
|
| 1101 |
+
// 中文字符大约是1.5个token(每个字符约为0.75个token)
|
| 1102 |
+
// 按空格分割英文单词
|
| 1103 |
+
words := strings.Fields(text)
|
| 1104 |
+
wordCount := len(words)
|
| 1105 |
+
|
| 1106 |
+
// 计算中文字符数量
|
| 1107 |
+
chineseCount := 0
|
| 1108 |
+
for _, r := range text {
|
| 1109 |
+
if r >= 0x4E00 && r <= 0x9FFF {
|
| 1110 |
+
chineseCount++
|
| 1111 |
+
}
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
// 粗略估计:英文单词按1个token计算,中文字符按0.75个token计算
|
| 1115 |
+
return wordCount + int(float64(chineseCount)*0.75)
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
// 处理非流式请求
|
| 1119 |
+
func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
|
| 1120 |
+
defer cleanupRequestStatus(c)
|
| 1121 |
+
|
| 1122 |
+
// 从上下文中获取token和tenant_url
|
| 1123 |
+
tokenInterface, exists := c.Get("token")
|
| 1124 |
+
tenantURLInterface, exists2 := c.Get("tenant_url")
|
| 1125 |
+
|
| 1126 |
+
var token, tenant string
|
| 1127 |
+
|
| 1128 |
+
if exists && exists2 {
|
| 1129 |
+
token, _ = tokenInterface.(string)
|
| 1130 |
+
tenant, _ = tenantURLInterface.(string)
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
// 如果上下文中没有,则使用GetAuthInfo获取
|
| 1134 |
+
if token == "" || tenant == "" {
|
| 1135 |
+
token, tenant = GetAuthInfo()
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
if token == "" || tenant == "" {
|
| 1139 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "无可用Token,请先在管理页面获取"})
|
| 1140 |
+
return
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
// 增加token使用计数
|
| 1144 |
+
incrementTokenUsage(token, model)
|
| 1145 |
+
|
| 1146 |
+
// 准备请求数据
|
| 1147 |
+
jsonData, err := json.Marshal(augmentReq)
|
| 1148 |
+
if err != nil {
|
| 1149 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
|
| 1150 |
+
return
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
// 打印请求参数
|
| 1154 |
+
//log.Printf("发送到远程接口的请求参数: %s", string(jsonData))
|
| 1155 |
+
|
| 1156 |
+
// 创建请求 - 使用获取到的tenant_url
|
| 1157 |
+
req, err := http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
|
| 1158 |
+
if err != nil {
|
| 1159 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
|
| 1160 |
+
return
|
| 1161 |
+
}
|
| 1162 |
+
|
| 1163 |
+
req.Header.Set("Content-Type", "application/json")
|
| 1164 |
+
// 使用获取到的token
|
| 1165 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 1166 |
+
|
| 1167 |
+
client := createHTTPClient()
|
| 1168 |
+
resp, err := client.Do(req)
|
| 1169 |
+
if err != nil {
|
| 1170 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
|
| 1171 |
+
return
|
| 1172 |
+
}
|
| 1173 |
+
defer resp.Body.Close()
|
| 1174 |
+
|
| 1175 |
+
// 检查响应状态码
|
| 1176 |
+
if resp.StatusCode != http.StatusOK {
|
| 1177 |
+
body, err := io.ReadAll(resp.Body)
|
| 1178 |
+
errMsg := "Augment response error"
|
| 1179 |
+
if err == nil {
|
| 1180 |
+
errMsg = errMsg + ": " + string(body)
|
| 1181 |
+
}
|
| 1182 |
+
c.JSON(resp.StatusCode, gin.H{"error": errMsg})
|
| 1183 |
+
return
|
| 1184 |
+
}
|
| 1185 |
+
|
| 1186 |
+
// 读取完整响应
|
| 1187 |
+
reader := bufio.NewReader(resp.Body)
|
| 1188 |
+
var fullText string
|
| 1189 |
+
|
| 1190 |
+
for {
|
| 1191 |
+
line, err := reader.ReadString('\n')
|
| 1192 |
+
if err != nil {
|
| 1193 |
+
if err == io.EOF {
|
| 1194 |
+
break
|
| 1195 |
+
}
|
| 1196 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取响应失败: " + err.Error()})
|
| 1197 |
+
return
|
| 1198 |
+
}
|
| 1199 |
+
|
| 1200 |
+
line = strings.TrimSpace(line)
|
| 1201 |
+
if line == "" {
|
| 1202 |
+
continue
|
| 1203 |
+
}
|
| 1204 |
+
|
| 1205 |
+
var augmentResp AugmentResponse
|
| 1206 |
+
if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
|
| 1207 |
+
continue
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
fullText += augmentResp.Text
|
| 1211 |
+
|
| 1212 |
+
// 检查响应内容是否包含错误信息
|
| 1213 |
+
if strings.Contains(augmentResp.Text, errBlocked) {
|
| 1214 |
+
// 将当前token加入冷却队列,冷却时间10分钟
|
| 1215 |
+
logger.Log.WithFields(logrus.Fields{
|
| 1216 |
+
"token": token,
|
| 1217 |
+
"mode": augmentReq.Mode,
|
| 1218 |
+
}).Info("检测到block信息,将token加入冷却队列10分钟")
|
| 1219 |
+
|
| 1220 |
+
err := SetTokenCoolStatus(token, 10*time.Minute)
|
| 1221 |
+
if err != nil {
|
| 1222 |
+
logger.Log.WithFields(logrus.Fields{
|
| 1223 |
+
"token": token,
|
| 1224 |
+
"error": err.Error(),
|
| 1225 |
+
}).Error("将token加入冷却队列失败")
|
| 1226 |
+
}
|
| 1227 |
+
}
|
| 1228 |
+
|
| 1229 |
+
if augmentResp.Done {
|
| 1230 |
+
break
|
| 1231 |
+
}
|
| 1232 |
+
}
|
| 1233 |
+
|
| 1234 |
+
// 创建OpenAI兼容的响应
|
| 1235 |
+
finishReason := "stop"
|
| 1236 |
+
|
| 1237 |
+
// 估算token数量
|
| 1238 |
+
promptTokens := estimateTokenCount(augmentReq.Message)
|
| 1239 |
+
for _, history := range augmentReq.ChatHistory {
|
| 1240 |
+
promptTokens += estimateTokenCount(history.RequestMessage)
|
| 1241 |
+
promptTokens += estimateTokenCount(history.ResponseText)
|
| 1242 |
+
}
|
| 1243 |
+
completionTokens := estimateTokenCount(fullText)
|
| 1244 |
+
|
| 1245 |
+
openAIResp := OpenAIResponse{
|
| 1246 |
+
ID: fmt.Sprintf("chatcmpl-%d", time.Now().Unix()),
|
| 1247 |
+
Object: "chat.completion",
|
| 1248 |
+
Created: time.Now().Unix(),
|
| 1249 |
+
Model: model,
|
| 1250 |
+
Choices: []Choice{
|
| 1251 |
+
{
|
| 1252 |
+
Index: 0,
|
| 1253 |
+
Message: ChatMessage{
|
| 1254 |
+
Role: "assistant",
|
| 1255 |
+
Content: fullText,
|
| 1256 |
+
},
|
| 1257 |
+
FinishReason: &finishReason,
|
| 1258 |
+
},
|
| 1259 |
+
},
|
| 1260 |
+
Usage: Usage{
|
| 1261 |
+
PromptTokens: promptTokens,
|
| 1262 |
+
CompletionTokens: completionTokens,
|
| 1263 |
+
TotalTokens: promptTokens + completionTokens,
|
| 1264 |
+
},
|
| 1265 |
+
}
|
| 1266 |
+
|
| 1267 |
+
c.JSON(http.StatusOK, openAIResp)
|
| 1268 |
+
}
|
| 1269 |
+
|
| 1270 |
+
// 清理请求状态
|
| 1271 |
+
func cleanupRequestStatus(c *gin.Context) {
|
| 1272 |
+
// 获取锁和 token
|
| 1273 |
+
lockInterface, exists := c.Get("token_lock")
|
| 1274 |
+
if !exists {
|
| 1275 |
+
return
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
tokenInterface, exists := c.Get("token")
|
| 1279 |
+
if !exists {
|
| 1280 |
+
return
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
lock, ok := lockInterface.(*sync.Mutex)
|
| 1284 |
+
if !ok {
|
| 1285 |
+
return
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
token, ok := tokenInterface.(string)
|
| 1289 |
+
if !ok {
|
| 1290 |
+
return
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
// 更新请求状态为已完成
|
| 1294 |
+
err := SetTokenRequestStatus(token, TokenRequestStatus{
|
| 1295 |
+
InProgress: false,
|
| 1296 |
+
LastRequestAt: time.Now(),
|
| 1297 |
+
})
|
| 1298 |
+
|
| 1299 |
+
// 无论更新状态是否成功,都要释放锁
|
| 1300 |
+
defer lock.Unlock()
|
| 1301 |
+
|
| 1302 |
+
if err != nil {
|
| 1303 |
+
log.Printf("清理请求状态失败: %v", err)
|
| 1304 |
+
return
|
| 1305 |
+
}
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
// 创建 HTTP 客户端,如果配置了代理则使用
|
| 1309 |
+
func createHTTPClient() *http.Client {
|
| 1310 |
+
client := &http.Client{}
|
| 1311 |
+
|
| 1312 |
+
// 检查是否配置了代理
|
| 1313 |
+
if config.AppConfig.ProxyURL != "" {
|
| 1314 |
+
proxyURL, err := url.Parse(config.AppConfig.ProxyURL)
|
| 1315 |
+
if err == nil {
|
| 1316 |
+
transport := &http.Transport{
|
| 1317 |
+
Proxy: http.ProxyURL(proxyURL),
|
| 1318 |
+
}
|
| 1319 |
+
client.Transport = transport
|
| 1320 |
+
log.Printf("使用代理: %s", config.AppConfig.ProxyURL)
|
| 1321 |
+
} else {
|
| 1322 |
+
log.Printf("代理URL格式错误: %v", err)
|
| 1323 |
+
}
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
return client
|
| 1327 |
+
}
|
| 1328 |
+
|
| 1329 |
+
// 在处理聊天请求时增加token使用计数
|
| 1330 |
+
func incrementTokenUsage(token string, model string) {
|
| 1331 |
+
// 先将模型名称转换为小写
|
| 1332 |
+
modelLower := strings.ToLower(model)
|
| 1333 |
+
|
| 1334 |
+
// 根据模型类型确定计数键 (不区分大小写)
|
| 1335 |
+
var countKey string
|
| 1336 |
+
if strings.HasSuffix(modelLower, "-chat") {
|
| 1337 |
+
countKey = "token_usage_chat:" + token
|
| 1338 |
+
} else if strings.HasSuffix(modelLower, "-agent") {
|
| 1339 |
+
countKey = "token_usage_agent:" + token
|
| 1340 |
+
} else {
|
| 1341 |
+
countKey = "token_usage:" + token // 默认键
|
| 1342 |
+
}
|
| 1343 |
+
|
| 1344 |
+
// 使用Redis的INCR命令增加计数
|
| 1345 |
+
err := config.RedisIncr(countKey)
|
| 1346 |
+
if err != nil {
|
| 1347 |
+
logger.Log.Error("增加token使用计数失败: %v", err)
|
| 1348 |
+
}
|
| 1349 |
+
|
| 1350 |
+
// 同时增加总使用计数
|
| 1351 |
+
totalCountKey := "token_usage:" + token
|
| 1352 |
+
if countKey != totalCountKey { // 避免重复计数
|
| 1353 |
+
err = config.RedisIncr(totalCountKey)
|
| 1354 |
+
if err != nil {
|
| 1355 |
+
logger.Log.Error("增加token总使用计数失败: %v", err)
|
| 1356 |
+
}
|
| 1357 |
+
}
|
| 1358 |
+
}
|
api/login.go
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/config"
|
| 5 |
+
"augment2api/pkg/logger"
|
| 6 |
+
"net/http"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
const (
|
| 14 |
+
TokenKey = "login:token:"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
// 生成随机会话令牌
|
| 18 |
+
func generateSessionToken() string {
|
| 19 |
+
tokenUUID := uuid.New()
|
| 20 |
+
return tokenUUID.String()
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// LoginRequest 登录请求结构
|
| 24 |
+
type LoginRequest struct {
|
| 25 |
+
Password string `json:"password"`
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// LoginHandler 处理登录请求
|
| 29 |
+
func LoginHandler(c *gin.Context) {
|
| 30 |
+
var req LoginRequest
|
| 31 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 32 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 33 |
+
"status": "error",
|
| 34 |
+
"error": "无效的请求数据",
|
| 35 |
+
})
|
| 36 |
+
return
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// 验证密码
|
| 40 |
+
if req.Password == config.AppConfig.AccessPwd {
|
| 41 |
+
// 生成会话令牌
|
| 42 |
+
token := generateSessionToken()
|
| 43 |
+
|
| 44 |
+
// 将会话令牌保存到Redis,有效期24小时
|
| 45 |
+
sessionKey := TokenKey + token
|
| 46 |
+
err := config.RedisSet(sessionKey, "valid", 24*time.Hour)
|
| 47 |
+
if err != nil {
|
| 48 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 49 |
+
"status": "error",
|
| 50 |
+
"error": "保存会话失败: " + err.Error(),
|
| 51 |
+
})
|
| 52 |
+
return
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
c.JSON(http.StatusOK, gin.H{
|
| 56 |
+
"status": "success",
|
| 57 |
+
"token": token,
|
| 58 |
+
})
|
| 59 |
+
return
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// 密码错误
|
| 63 |
+
c.JSON(http.StatusUnauthorized, gin.H{
|
| 64 |
+
"status": "error",
|
| 65 |
+
"error": "密码错误",
|
| 66 |
+
})
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// ValidateToken 验证Token
|
| 70 |
+
func ValidateToken(token string) bool {
|
| 71 |
+
if token == "" {
|
| 72 |
+
return false
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// 检查Redis中是否存在该token
|
| 76 |
+
tokenKey := TokenKey + token
|
| 77 |
+
exists, err := config.RedisExists(tokenKey)
|
| 78 |
+
if err != nil || !exists {
|
| 79 |
+
return false
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return true
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// AuthTokenMiddleware 会话认证中间件
|
| 86 |
+
func AuthTokenMiddleware() gin.HandlerFunc {
|
| 87 |
+
return func(c *gin.Context) {
|
| 88 |
+
// 如果未设置访问密码,则不需要验证
|
| 89 |
+
if config.AppConfig.AccessPwd == "" {
|
| 90 |
+
c.Next()
|
| 91 |
+
return
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// 从查询参数或Cookie中获取会话令牌
|
| 95 |
+
token := c.GetHeader("X-Auth-Token")
|
| 96 |
+
if token == "" {
|
| 97 |
+
token = c.Query("token")
|
| 98 |
+
}
|
| 99 |
+
if token == "" {
|
| 100 |
+
token, _ = c.Cookie("auth_token")
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// 验证会话令牌
|
| 104 |
+
if !ValidateToken(token) {
|
| 105 |
+
logger.Log.Info("无效的会话令牌:", token)
|
| 106 |
+
c.Redirect(http.StatusFound, "/login?error=token_expired")
|
| 107 |
+
c.Abort()
|
| 108 |
+
return
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
c.Next()
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// LogoutHandler 处理登出请求
|
| 116 |
+
func LogoutHandler(c *gin.Context) {
|
| 117 |
+
token := c.GetHeader("X-Auth-Token")
|
| 118 |
+
if token != "" {
|
| 119 |
+
// 从Redis中删除会话token
|
| 120 |
+
tokenKey := TokenKey + token
|
| 121 |
+
err := config.RedisDel(tokenKey)
|
| 122 |
+
if err != nil {
|
| 123 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 124 |
+
"status": "error",
|
| 125 |
+
"error": "删除会话失败: " + err.Error(),
|
| 126 |
+
})
|
| 127 |
+
return
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
c.JSON(http.StatusOK, gin.H{
|
| 132 |
+
"status": "success",
|
| 133 |
+
})
|
| 134 |
+
}
|
api/token_handler.go
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/config"
|
| 5 |
+
"augment2api/pkg/logger"
|
| 6 |
+
"bytes"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"errors"
|
| 9 |
+
"fmt"
|
| 10 |
+
"math/rand"
|
| 11 |
+
"net/http"
|
| 12 |
+
"strconv"
|
| 13 |
+
"sync"
|
| 14 |
+
"time"
|
| 15 |
+
|
| 16 |
+
"github.com/gin-gonic/gin"
|
| 17 |
+
"github.com/go-redis/redis/v8"
|
| 18 |
+
"github.com/google/uuid"
|
| 19 |
+
"github.com/sirupsen/logrus"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
// TokenInfo 存储token信息
|
| 23 |
+
type TokenInfo struct {
|
| 24 |
+
Token string `json:"token"`
|
| 25 |
+
TenantURL string `json:"tenant_url"`
|
| 26 |
+
UsageCount int `json:"usage_count"` // 总对话次数
|
| 27 |
+
ChatUsageCount int `json:"chat_usage_count"` // CHAT模式对话次数
|
| 28 |
+
AgentUsageCount int `json:"agent_usage_count"` // AGENT模式对话次数
|
| 29 |
+
Remark string `json:"remark"` // 备注字段
|
| 30 |
+
InCool bool `json:"in_cool"` // 是否在冷却中
|
| 31 |
+
CoolEnd time.Time `json:"cool_end,omitempty"` // 冷却结束时间
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// TokenItem token项结构
|
| 35 |
+
type TokenItem struct {
|
| 36 |
+
Token string `json:"token"`
|
| 37 |
+
TenantUrl string `json:"tenantUrl"`
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// TokenRequestStatus 记录 token 请求状态
|
| 41 |
+
type TokenRequestStatus struct {
|
| 42 |
+
InProgress bool `json:"in_progress"`
|
| 43 |
+
LastRequestAt time.Time `json:"last_request_at"`
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// TokenCoolStatus 记录 token 冷却状态
|
| 47 |
+
type TokenCoolStatus struct {
|
| 48 |
+
InCool bool `json:"in_cool"`
|
| 49 |
+
CoolEnd time.Time `json:"cool_end"`
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// GetRedisTokenHandler 从Redis获取token列表,支持分页
|
| 53 |
+
func GetRedisTokenHandler(c *gin.Context) {
|
| 54 |
+
// 获取分页参数(可选)
|
| 55 |
+
page := c.DefaultQuery("page", "1")
|
| 56 |
+
pageSize := c.DefaultQuery("page_size", "0") // 0表示不分页,返回所有
|
| 57 |
+
|
| 58 |
+
pageNum, _ := strconv.Atoi(page)
|
| 59 |
+
pageSizeNum, _ := strconv.Atoi(pageSize)
|
| 60 |
+
|
| 61 |
+
if pageNum < 1 {
|
| 62 |
+
pageNum = 1
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// 获取所有token的key (使用通配符模式)
|
| 66 |
+
keys, err := config.RedisKeys("token:*")
|
| 67 |
+
if err != nil {
|
| 68 |
+
c.JSON(http.StatusOK, gin.H{
|
| 69 |
+
"status": "error",
|
| 70 |
+
"error": "获取token列表失败: " + err.Error(),
|
| 71 |
+
})
|
| 72 |
+
return
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// 如果没有token
|
| 76 |
+
if len(keys) == 0 {
|
| 77 |
+
c.JSON(http.StatusOK, gin.H{
|
| 78 |
+
"status": "success",
|
| 79 |
+
"tokens": []TokenInfo{},
|
| 80 |
+
"total": 0,
|
| 81 |
+
"page": pageNum,
|
| 82 |
+
"page_size": pageSizeNum,
|
| 83 |
+
"total_pages": 0,
|
| 84 |
+
})
|
| 85 |
+
return
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// 构建token列表
|
| 89 |
+
var tokenList []TokenInfo
|
| 90 |
+
for _, key := range keys {
|
| 91 |
+
// 从key中提取token (格式: "token:{token}")
|
| 92 |
+
token := key[6:] // 去掉前缀 "token:"
|
| 93 |
+
|
| 94 |
+
// 获取对应的tenant_url
|
| 95 |
+
tenantURL, err := config.RedisHGet(key, "tenant_url")
|
| 96 |
+
if err != nil {
|
| 97 |
+
continue // 跳过无效的token
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// 获取token状态
|
| 101 |
+
status, err := config.RedisHGet(key, "status")
|
| 102 |
+
if err == nil && status == "disabled" {
|
| 103 |
+
continue // 跳过被标记为不可用的token
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// 获取备注信息
|
| 107 |
+
remark, _ := config.RedisHGet(key, "remark")
|
| 108 |
+
|
| 109 |
+
// 获取token的冷却状态
|
| 110 |
+
coolStatus, _ := GetTokenCoolStatus(token)
|
| 111 |
+
|
| 112 |
+
// 在获取token信息时,同时获取对话次数、备注和冷却状态
|
| 113 |
+
tokenList = append(tokenList, TokenInfo{
|
| 114 |
+
Token: token,
|
| 115 |
+
TenantURL: tenantURL,
|
| 116 |
+
UsageCount: getTokenUsageCount(token),
|
| 117 |
+
ChatUsageCount: getTokenChatUsageCount(token),
|
| 118 |
+
AgentUsageCount: getTokenAgentUsageCount(token),
|
| 119 |
+
Remark: remark,
|
| 120 |
+
InCool: coolStatus.InCool,
|
| 121 |
+
CoolEnd: coolStatus.CoolEnd,
|
| 122 |
+
})
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// 计算总页数和分页数据
|
| 126 |
+
totalItems := len(tokenList)
|
| 127 |
+
totalPages := 1
|
| 128 |
+
|
| 129 |
+
// 如果需要分页
|
| 130 |
+
if pageSizeNum > 0 {
|
| 131 |
+
totalPages = (totalItems + pageSizeNum - 1) / pageSizeNum
|
| 132 |
+
|
| 133 |
+
// 确保页码有效
|
| 134 |
+
if pageNum > totalPages && totalPages > 0 {
|
| 135 |
+
pageNum = totalPages
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// 计算分页的起始和结束索引
|
| 139 |
+
startIndex := (pageNum - 1) * pageSizeNum
|
| 140 |
+
endIndex := startIndex + pageSizeNum
|
| 141 |
+
|
| 142 |
+
if startIndex < totalItems {
|
| 143 |
+
if endIndex > totalItems {
|
| 144 |
+
endIndex = totalItems
|
| 145 |
+
}
|
| 146 |
+
tokenList = tokenList[startIndex:endIndex]
|
| 147 |
+
} else {
|
| 148 |
+
tokenList = []TokenInfo{}
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
c.JSON(http.StatusOK, gin.H{
|
| 153 |
+
"status": "success",
|
| 154 |
+
"tokens": tokenList,
|
| 155 |
+
"total": totalItems,
|
| 156 |
+
"page": pageNum,
|
| 157 |
+
"page_size": pageSizeNum,
|
| 158 |
+
"total_pages": totalPages,
|
| 159 |
+
})
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// SaveTokenToRedis 保存token到Redis
|
| 163 |
+
func SaveTokenToRedis(token, tenantURL string) error {
|
| 164 |
+
// 创建一个唯一的key,包含token和tenant_url
|
| 165 |
+
tokenKey := "token:" + token
|
| 166 |
+
|
| 167 |
+
// token已存在,则跳过
|
| 168 |
+
exists, err := config.RedisExists(tokenKey)
|
| 169 |
+
if err != nil {
|
| 170 |
+
return err
|
| 171 |
+
}
|
| 172 |
+
if exists {
|
| 173 |
+
return nil
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// 将tenant_url存储在token对应的哈希表中
|
| 177 |
+
err = config.RedisHSet(tokenKey, "tenant_url", tenantURL)
|
| 178 |
+
if err != nil {
|
| 179 |
+
return err
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// 默认将新添加的token标记为活跃状态
|
| 183 |
+
err = config.RedisHSet(tokenKey, "status", "active")
|
| 184 |
+
if err != nil {
|
| 185 |
+
return err
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// 初始化备注为空字符串
|
| 189 |
+
return config.RedisHSet(tokenKey, "remark", "")
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// DeleteTokenHandler 删除指定的token
|
| 193 |
+
func DeleteTokenHandler(c *gin.Context) {
|
| 194 |
+
token := c.Param("token")
|
| 195 |
+
if token == "" {
|
| 196 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 197 |
+
"status": "error",
|
| 198 |
+
"error": "未指定token",
|
| 199 |
+
})
|
| 200 |
+
return
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
tokenKey := "token:" + token
|
| 204 |
+
|
| 205 |
+
// 检查token是否存在
|
| 206 |
+
exists, err := config.RedisExists(tokenKey)
|
| 207 |
+
if err != nil {
|
| 208 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 209 |
+
"status": "error",
|
| 210 |
+
"error": "检查token失败: " + err.Error(),
|
| 211 |
+
})
|
| 212 |
+
return
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
if !exists {
|
| 216 |
+
c.JSON(http.StatusNotFound, gin.H{
|
| 217 |
+
"status": "error",
|
| 218 |
+
"error": "token不存在",
|
| 219 |
+
})
|
| 220 |
+
return
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// 删除token
|
| 224 |
+
if err := config.RedisDel(tokenKey); err != nil {
|
| 225 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 226 |
+
"status": "error",
|
| 227 |
+
"error": "删除token失败: " + err.Error(),
|
| 228 |
+
})
|
| 229 |
+
return
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// 删除token关联的使用次数(如果存在)
|
| 233 |
+
// 删除总使用次数
|
| 234 |
+
tokenUsageKey := "token_usage:" + token
|
| 235 |
+
exists, err = config.RedisExists(tokenUsageKey)
|
| 236 |
+
if err != nil {
|
| 237 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 238 |
+
"status": "error",
|
| 239 |
+
"error": "检查token使用次数失败: " + err.Error(),
|
| 240 |
+
})
|
| 241 |
+
return
|
| 242 |
+
}
|
| 243 |
+
if exists {
|
| 244 |
+
if err := config.RedisDel(tokenUsageKey); err != nil {
|
| 245 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 246 |
+
"status": "error",
|
| 247 |
+
"error": "删除token使用次数失败: " + err.Error(),
|
| 248 |
+
})
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// 删除CHAT模式使用次数
|
| 253 |
+
tokenChatUsageKey := "token_usage_chat:" + token
|
| 254 |
+
exists, err = config.RedisExists(tokenChatUsageKey)
|
| 255 |
+
if err == nil && exists {
|
| 256 |
+
config.RedisDel(tokenChatUsageKey)
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// 删除AGENT模式使用次数
|
| 260 |
+
tokenAgentUsageKey := "token_usage_agent:" + token
|
| 261 |
+
exists, err = config.RedisExists(tokenAgentUsageKey)
|
| 262 |
+
if err == nil && exists {
|
| 263 |
+
config.RedisDel(tokenAgentUsageKey)
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
c.JSON(http.StatusOK, gin.H{
|
| 267 |
+
"status": "success",
|
| 268 |
+
})
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
// AddTokenHandler 批量添加token到Redis
|
| 272 |
+
func AddTokenHandler(c *gin.Context) {
|
| 273 |
+
var tokens []TokenItem
|
| 274 |
+
if err := c.ShouldBindJSON(&tokens); err != nil {
|
| 275 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 276 |
+
"status": "error",
|
| 277 |
+
"error": "无效的请求数据",
|
| 278 |
+
})
|
| 279 |
+
return
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
// 检查是否有token数据
|
| 283 |
+
if len(tokens) == 0 {
|
| 284 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 285 |
+
"status": "error",
|
| 286 |
+
"error": "token列表为空",
|
| 287 |
+
})
|
| 288 |
+
return
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
// 批量保存token
|
| 292 |
+
successCount := 0
|
| 293 |
+
failedTokens := make([]string, 0)
|
| 294 |
+
|
| 295 |
+
for _, item := range tokens {
|
| 296 |
+
// 验证token格式
|
| 297 |
+
if item.Token == "" || item.TenantUrl == "" {
|
| 298 |
+
failedTokens = append(failedTokens, item.Token)
|
| 299 |
+
continue
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// 保存到Redis
|
| 303 |
+
err := SaveTokenToRedis(item.Token, item.TenantUrl)
|
| 304 |
+
if err != nil {
|
| 305 |
+
failedTokens = append(failedTokens, item.Token)
|
| 306 |
+
continue
|
| 307 |
+
}
|
| 308 |
+
successCount++
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
// 返回处理结果
|
| 312 |
+
result := gin.H{
|
| 313 |
+
"status": "success",
|
| 314 |
+
"total": len(tokens),
|
| 315 |
+
"success_count": successCount,
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
if len(failedTokens) > 0 {
|
| 319 |
+
result["failed_tokens"] = failedTokens
|
| 320 |
+
result["failed_count"] = len(failedTokens)
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
c.JSON(http.StatusOK, result)
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// CheckTokenTenantURL 检测token的租户地址
|
| 327 |
+
func CheckTokenTenantURL(token string) (string, error) {
|
| 328 |
+
// 构建测试消息
|
| 329 |
+
testMsg := map[string]interface{}{
|
| 330 |
+
"message": "hello,what is your name",
|
| 331 |
+
"mode": "CHAT",
|
| 332 |
+
"prefix": "You are AI assistant,help me to solve problems!",
|
| 333 |
+
"suffix": " ",
|
| 334 |
+
"lang": "HTML",
|
| 335 |
+
"user_guidelines": "You are a helpful assistant, you can help me to solve problems and always answer in Chinese.",
|
| 336 |
+
"workspace_guidelines": "",
|
| 337 |
+
"feature_detection_flags": map[string]interface{}{
|
| 338 |
+
"support_raw_output": true,
|
| 339 |
+
},
|
| 340 |
+
"tool_definitions": []map[string]interface{}{},
|
| 341 |
+
"blobs": map[string]interface{}{
|
| 342 |
+
"checkpoint_id": nil,
|
| 343 |
+
"added_blobs": []string{},
|
| 344 |
+
"deleted_blobs": []string{},
|
| 345 |
+
},
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
jsonData, err := json.Marshal(testMsg)
|
| 349 |
+
if err != nil {
|
| 350 |
+
return "", fmt.Errorf("序列化测试消息失败: %v", err)
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
tokenKey := "token:" + token
|
| 354 |
+
|
| 355 |
+
currentTenantURL, err := config.RedisHGet(tokenKey, "tenant_url")
|
| 356 |
+
|
| 357 |
+
var tenantURLResult string
|
| 358 |
+
var foundValid bool
|
| 359 |
+
var tenantURLsToTest []string
|
| 360 |
+
|
| 361 |
+
// 如果Redis中有有效的租户地址,优先测试该地址
|
| 362 |
+
if err == nil && currentTenantURL != "" {
|
| 363 |
+
tenantURLsToTest = append(tenantURLsToTest, currentTenantURL)
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// 添加其他租户地址
|
| 367 |
+
for i := 20; i >= 1; i-- {
|
| 368 |
+
newTenantURL := fmt.Sprintf("https://d%d.api.augmentcode.com/", i)
|
| 369 |
+
// 避免重复测试已有的租户地址
|
| 370 |
+
if newTenantURL != currentTenantURL {
|
| 371 |
+
tenantURLsToTest = append(tenantURLsToTest, newTenantURL)
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// 测试租户地址
|
| 376 |
+
for _, tenantURL := range tenantURLsToTest {
|
| 377 |
+
// 创建请求
|
| 378 |
+
req, err := http.NewRequest("POST", tenantURL+"chat-stream", bytes.NewReader(jsonData))
|
| 379 |
+
if err != nil {
|
| 380 |
+
continue
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
req.Header.Set("Content-Type", "application/json")
|
| 384 |
+
req.Header.Set("Authorization", "Bearer "+token)
|
| 385 |
+
req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
|
| 386 |
+
req.Header.Set("x-api-version", "2")
|
| 387 |
+
req.Header.Set("x-request-id", uuid.New().String())
|
| 388 |
+
req.Header.Set("x-request-session-id", uuid.New().String())
|
| 389 |
+
|
| 390 |
+
client := createHTTPClient()
|
| 391 |
+
resp, err := client.Do(req)
|
| 392 |
+
if err != nil {
|
| 393 |
+
fmt.Printf("请求失败: %v\n", err)
|
| 394 |
+
continue
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
isInvalid := false
|
| 398 |
+
func() {
|
| 399 |
+
defer resp.Body.Close()
|
| 400 |
+
|
| 401 |
+
// 检查是否返回401状态码(未授权)
|
| 402 |
+
if resp.StatusCode == http.StatusUnauthorized {
|
| 403 |
+
// 读取响应体内容
|
| 404 |
+
buf := make([]byte, 1024)
|
| 405 |
+
n, readErr := resp.Body.Read(buf)
|
| 406 |
+
responseBody := ""
|
| 407 |
+
if readErr == nil && n > 0 {
|
| 408 |
+
responseBody = string(buf[:n])
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
// 只有当响应中包含"Invalid token"时才标记为不可用
|
| 412 |
+
if readErr == nil && n > 0 && bytes.Contains(buf[:n], []byte("Invalid token")) {
|
| 413 |
+
// 将token标记为不可用
|
| 414 |
+
err = config.RedisHSet(tokenKey, "status", "disabled")
|
| 415 |
+
if err != nil {
|
| 416 |
+
fmt.Printf("标记token为不可用失败: %v\n", err)
|
| 417 |
+
}
|
| 418 |
+
logger.Log.WithFields(logrus.Fields{
|
| 419 |
+
"token": token,
|
| 420 |
+
"response_body": responseBody,
|
| 421 |
+
}).Info("token: 已被标记为不可用,返回401未授权")
|
| 422 |
+
isInvalid = true
|
| 423 |
+
}
|
| 424 |
+
return
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
// 检查响应状态
|
| 428 |
+
if resp.StatusCode == http.StatusOK {
|
| 429 |
+
// 尝试读取一小部分响应以确认是否有效
|
| 430 |
+
buf := make([]byte, 1024)
|
| 431 |
+
n, err := resp.Body.Read(buf)
|
| 432 |
+
if err == nil && n > 0 {
|
| 433 |
+
// 更新Redis中的租户地址和状态
|
| 434 |
+
err = config.RedisHSet(tokenKey, "tenant_url", tenantURL)
|
| 435 |
+
if err != nil {
|
| 436 |
+
return
|
| 437 |
+
}
|
| 438 |
+
// 将token标记为可用
|
| 439 |
+
err = config.RedisHSet(tokenKey, "status", "active")
|
| 440 |
+
if err != nil {
|
| 441 |
+
fmt.Printf("标记token为可用失败: %v\n", err)
|
| 442 |
+
}
|
| 443 |
+
logger.Log.WithFields(logrus.Fields{
|
| 444 |
+
"token": token,
|
| 445 |
+
"new_tenant_url": tenantURL,
|
| 446 |
+
}).Info("token: 更新租户地址成功")
|
| 447 |
+
tenantURLResult = tenantURL
|
| 448 |
+
foundValid = true
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
}()
|
| 452 |
+
|
| 453 |
+
// 如果token无效,立即返回错误,不再测试其他地址
|
| 454 |
+
if isInvalid {
|
| 455 |
+
return "", fmt.Errorf("token被标记为不可用")
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
// 如果找到有效的租户地址,跳出循环
|
| 459 |
+
if foundValid {
|
| 460 |
+
return tenantURLResult, nil
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
return "", fmt.Errorf("未找到有效的租户地址")
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
// CheckAllTokensHandler 批量检测所有token的租户地址
|
| 468 |
+
func CheckAllTokensHandler(c *gin.Context) {
|
| 469 |
+
// 获取所有token的key
|
| 470 |
+
keys, err := config.RedisKeys("token:*")
|
| 471 |
+
if err != nil {
|
| 472 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 473 |
+
"status": "error",
|
| 474 |
+
"error": "获取token列表失败: " + err.Error(),
|
| 475 |
+
})
|
| 476 |
+
return
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
if len(keys) == 0 {
|
| 480 |
+
c.JSON(http.StatusOK, gin.H{
|
| 481 |
+
"status": "success",
|
| 482 |
+
"total": 0,
|
| 483 |
+
"updated": 0,
|
| 484 |
+
"disabled": 0,
|
| 485 |
+
})
|
| 486 |
+
return
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
var wg sync.WaitGroup
|
| 490 |
+
// 使用互斥锁保护计数器
|
| 491 |
+
var mu sync.Mutex
|
| 492 |
+
var updatedCount int
|
| 493 |
+
var disabledCount int
|
| 494 |
+
|
| 495 |
+
for _, key := range keys {
|
| 496 |
+
// 获取token状态,跳过已标记为不可用的token
|
| 497 |
+
status, err := config.RedisHGet(key, "status")
|
| 498 |
+
if err == nil && status == "disabled" {
|
| 499 |
+
mu.Lock()
|
| 500 |
+
mu.Unlock()
|
| 501 |
+
continue // 跳过此token
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
wg.Add(1)
|
| 505 |
+
go func(key string) {
|
| 506 |
+
defer wg.Done()
|
| 507 |
+
|
| 508 |
+
// 从key中提取token
|
| 509 |
+
token := key[6:] // 去掉前缀 "token:"
|
| 510 |
+
|
| 511 |
+
// 获取当前的租户地址
|
| 512 |
+
oldTenantURL, _ := config.RedisHGet(key, "tenant_url")
|
| 513 |
+
|
| 514 |
+
// 检测租户地址
|
| 515 |
+
newTenantURL, err := CheckTokenTenantURL(token)
|
| 516 |
+
logger.Log.WithFields(logrus.Fields{
|
| 517 |
+
"token": token,
|
| 518 |
+
"old_tenant_url": oldTenantURL,
|
| 519 |
+
"new_tenant_url": newTenantURL,
|
| 520 |
+
}).Info("检测token租户地址")
|
| 521 |
+
|
| 522 |
+
mu.Lock()
|
| 523 |
+
if err != nil && err.Error() == "token被标记为不可用" {
|
| 524 |
+
disabledCount++
|
| 525 |
+
} else if err == nil && newTenantURL != oldTenantURL {
|
| 526 |
+
updatedCount++
|
| 527 |
+
}
|
| 528 |
+
mu.Unlock()
|
| 529 |
+
}(key)
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
wg.Wait()
|
| 533 |
+
|
| 534 |
+
c.JSON(http.StatusOK, gin.H{
|
| 535 |
+
"status": "success",
|
| 536 |
+
"total": len(keys),
|
| 537 |
+
"updated": updatedCount,
|
| 538 |
+
"disabled": disabledCount,
|
| 539 |
+
})
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
// SetTokenRequestStatus 设置token请求状态
|
| 543 |
+
func SetTokenRequestStatus(token string, status TokenRequestStatus) error {
|
| 544 |
+
// 使用Redis存储token请求状态
|
| 545 |
+
key := "token_status:" + token
|
| 546 |
+
|
| 547 |
+
// 将状态转换为JSON
|
| 548 |
+
statusJSON, err := json.Marshal(status)
|
| 549 |
+
if err != nil {
|
| 550 |
+
return err
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
// 存储到Redis,设置过期时间为1小时
|
| 554 |
+
return config.RedisSet(key, string(statusJSON), time.Hour)
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
// GetTokenRequestStatus 获取token请求状态
|
| 558 |
+
func GetTokenRequestStatus(token string) (TokenRequestStatus, error) {
|
| 559 |
+
key := "token_status:" + token
|
| 560 |
+
|
| 561 |
+
// 从Redis获取状态
|
| 562 |
+
statusJSON, err := config.RedisGet(key)
|
| 563 |
+
if err != nil {
|
| 564 |
+
// 如果key不存在,返回默认状态
|
| 565 |
+
if errors.Is(err, redis.Nil) {
|
| 566 |
+
return TokenRequestStatus{
|
| 567 |
+
InProgress: false,
|
| 568 |
+
LastRequestAt: time.Time{}, // 零值时间
|
| 569 |
+
}, nil
|
| 570 |
+
}
|
| 571 |
+
return TokenRequestStatus{}, err
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
// 解析JSON
|
| 575 |
+
var status TokenRequestStatus
|
| 576 |
+
if err := json.Unmarshal([]byte(statusJSON), &status); err != nil {
|
| 577 |
+
return TokenRequestStatus{}, err
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
return status, nil
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
// SetTokenCoolStatus 将token加入冷却队列
|
| 584 |
+
func SetTokenCoolStatus(token string, duration time.Duration) error {
|
| 585 |
+
// 使用Redis存储token冷却状态
|
| 586 |
+
key := "token_cool_status:" + token
|
| 587 |
+
|
| 588 |
+
coolStatus := TokenCoolStatus{
|
| 589 |
+
InCool: true,
|
| 590 |
+
CoolEnd: time.Now().Add(duration),
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
// 将状态转换为JSON
|
| 594 |
+
coolStatusJSON, err := json.Marshal(coolStatus)
|
| 595 |
+
if err != nil {
|
| 596 |
+
return err
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
// 存储到Redis,设置过期时间与冷却时间相同
|
| 600 |
+
return config.RedisSet(key, string(coolStatusJSON), duration)
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
// GetTokenCoolStatus 获取token冷却状态
|
| 604 |
+
func GetTokenCoolStatus(token string) (TokenCoolStatus, error) {
|
| 605 |
+
key := "token_cool_status:" + token
|
| 606 |
+
|
| 607 |
+
// 从Redis获取状态
|
| 608 |
+
coolStatusJSON, err := config.RedisGet(key)
|
| 609 |
+
if err != nil {
|
| 610 |
+
// 如果key不存在,返回默认状态
|
| 611 |
+
if errors.Is(err, redis.Nil) {
|
| 612 |
+
return TokenCoolStatus{
|
| 613 |
+
InCool: false,
|
| 614 |
+
CoolEnd: time.Time{}, // 零值时间
|
| 615 |
+
}, nil
|
| 616 |
+
}
|
| 617 |
+
return TokenCoolStatus{}, err
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
// 解析JSON
|
| 621 |
+
var coolStatus TokenCoolStatus
|
| 622 |
+
if err := json.Unmarshal([]byte(coolStatusJSON), &coolStatus); err != nil {
|
| 623 |
+
return TokenCoolStatus{}, err
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
// 检查冷却时间是否已过
|
| 627 |
+
if time.Now().After(coolStatus.CoolEnd) {
|
| 628 |
+
coolStatus.InCool = false
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
return coolStatus, nil
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
// GetAvailableToken 获取一个可用的token(未在使用中且冷却时间已过)
|
| 635 |
+
func GetAvailableToken() (string, string) {
|
| 636 |
+
// 获取所有token的key
|
| 637 |
+
keys, err := config.RedisKeys("token:*")
|
| 638 |
+
if err != nil || len(keys) == 0 {
|
| 639 |
+
return "No token", ""
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
// 筛选可用的token
|
| 643 |
+
var availableTokens []string
|
| 644 |
+
var availableTenantURLs []string
|
| 645 |
+
var cooldownTokens []string
|
| 646 |
+
var cooldownTenantURLs []string
|
| 647 |
+
|
| 648 |
+
for _, key := range keys {
|
| 649 |
+
// 获取token状态
|
| 650 |
+
status, err := config.RedisHGet(key, "status")
|
| 651 |
+
if err == nil && status == "disabled" {
|
| 652 |
+
continue // 跳过被标记为不可用的token
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
// 从key中提取token
|
| 656 |
+
token := key[6:] // 去掉前缀 "token:"
|
| 657 |
+
|
| 658 |
+
// 获取token的请求状态
|
| 659 |
+
requestStatus, err := GetTokenRequestStatus(token)
|
| 660 |
+
if err != nil {
|
| 661 |
+
continue
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
// 如果token正在使用中,跳过
|
| 665 |
+
if requestStatus.InProgress {
|
| 666 |
+
continue
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
// 如果距离上次请求不足3秒,跳过
|
| 670 |
+
if time.Since(requestStatus.LastRequestAt) < 3*time.Second {
|
| 671 |
+
continue
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
// 检查CHAT模式和AGENT模式的使用次数限制
|
| 675 |
+
chatUsageCount := getTokenChatUsageCount(token)
|
| 676 |
+
agentUsageCount := getTokenAgentUsageCount(token)
|
| 677 |
+
|
| 678 |
+
// 如果CHAT模式已达到3000次限制,跳过
|
| 679 |
+
if chatUsageCount >= 3000 {
|
| 680 |
+
continue
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// 如果AGENT模式已达到50次限制,跳过
|
| 684 |
+
if agentUsageCount >= 50 {
|
| 685 |
+
continue
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
// 获取对应的tenant_url
|
| 689 |
+
tenantURL, err := config.RedisHGet(key, "tenant_url")
|
| 690 |
+
if err != nil {
|
| 691 |
+
continue
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
// 检查token是否在冷却中
|
| 695 |
+
coolStatus, err := GetTokenCoolStatus(token)
|
| 696 |
+
if err != nil {
|
| 697 |
+
continue
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
// 如果token在冷却中,放入冷却队列
|
| 701 |
+
if coolStatus.InCool {
|
| 702 |
+
cooldownTokens = append(cooldownTokens, token)
|
| 703 |
+
cooldownTenantURLs = append(cooldownTenantURLs, tenantURL)
|
| 704 |
+
} else {
|
| 705 |
+
// 否则放入可用队列
|
| 706 |
+
availableTokens = append(availableTokens, token)
|
| 707 |
+
availableTenantURLs = append(availableTenantURLs, tenantURL)
|
| 708 |
+
}
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
// 优先从可用队列中选择token
|
| 712 |
+
if len(availableTokens) > 0 {
|
| 713 |
+
// 随机选择一个token
|
| 714 |
+
randomIndex := rand.Intn(len(availableTokens))
|
| 715 |
+
return availableTokens[randomIndex], availableTenantURLs[randomIndex]
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
// 如果没有非冷却token可用,则从冷却队列中选择
|
| 719 |
+
if len(cooldownTokens) > 0 {
|
| 720 |
+
// 随机选择一个token
|
| 721 |
+
randomIndex := rand.Intn(len(cooldownTokens))
|
| 722 |
+
return cooldownTokens[randomIndex], cooldownTenantURLs[randomIndex]
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
// 如果没有任何可用的token
|
| 726 |
+
return "No available token", ""
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
// getTokenUsageCount 获取token的使用次数
|
| 730 |
+
func getTokenUsageCount(token string) int {
|
| 731 |
+
// 使用Redis中的计数器获取使用次数
|
| 732 |
+
countKey := "token_usage:" + token
|
| 733 |
+
count, err := config.RedisGet(countKey)
|
| 734 |
+
if err != nil {
|
| 735 |
+
return 0 // 如果出错或不存在,返回0
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
// 将字符串转换为整数
|
| 739 |
+
countInt, err := strconv.Atoi(count)
|
| 740 |
+
if err != nil {
|
| 741 |
+
return 0
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
return countInt
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
// getTokenChatUsageCount 获取token的CHAT模式使用次数
|
| 748 |
+
func getTokenChatUsageCount(token string) int {
|
| 749 |
+
// 使用Redis中的计数器获取使用次数
|
| 750 |
+
countKey := "token_usage_chat:" + token
|
| 751 |
+
count, err := config.RedisGet(countKey)
|
| 752 |
+
if err != nil {
|
| 753 |
+
return 0 // 如果出错或不存在,返回0
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
// 将字符串转换为整数
|
| 757 |
+
countInt, err := strconv.Atoi(count)
|
| 758 |
+
if err != nil {
|
| 759 |
+
return 0
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
return countInt
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
// getTokenAgentUsageCount 获取token的AGENT模式使用次数
|
| 766 |
+
func getTokenAgentUsageCount(token string) int {
|
| 767 |
+
// 使用Redis中的计数器获取使用次数
|
| 768 |
+
countKey := "token_usage_agent:" + token
|
| 769 |
+
count, err := config.RedisGet(countKey)
|
| 770 |
+
if err != nil {
|
| 771 |
+
return 0 // 如果出错或不存在,返回0
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
// 将字符串转换为整数
|
| 775 |
+
countInt, err := strconv.Atoi(count)
|
| 776 |
+
if err != nil {
|
| 777 |
+
return 0
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
return countInt
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
// UpdateTokenRemark 更新token的备注信息
|
| 784 |
+
func UpdateTokenRemark(c *gin.Context) {
|
| 785 |
+
token := c.Param("token")
|
| 786 |
+
if token == "" {
|
| 787 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 788 |
+
"status": "error",
|
| 789 |
+
"error": "未指定token",
|
| 790 |
+
})
|
| 791 |
+
return
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
var req struct {
|
| 795 |
+
Remark string `json:"remark"`
|
| 796 |
+
}
|
| 797 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 798 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 799 |
+
"status": "error",
|
| 800 |
+
"error": "无效的请求数据",
|
| 801 |
+
})
|
| 802 |
+
return
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
tokenKey := "token:" + token
|
| 806 |
+
|
| 807 |
+
// 检查token是否存在
|
| 808 |
+
exists, err := config.RedisExists(tokenKey)
|
| 809 |
+
if err != nil {
|
| 810 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 811 |
+
"status": "error",
|
| 812 |
+
"error": "检查token失败: " + err.Error(),
|
| 813 |
+
})
|
| 814 |
+
return
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
if !exists {
|
| 818 |
+
c.JSON(http.StatusNotFound, gin.H{
|
| 819 |
+
"status": "error",
|
| 820 |
+
"error": "token不存在",
|
| 821 |
+
})
|
| 822 |
+
return
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
// 更新备注
|
| 826 |
+
err = config.RedisHSet(tokenKey, "remark", req.Remark)
|
| 827 |
+
if err != nil {
|
| 828 |
+
c.JSON(http.StatusInternalServerError, gin.H{
|
| 829 |
+
"status": "error",
|
| 830 |
+
"error": "更新备注失败: " + err.Error(),
|
| 831 |
+
})
|
| 832 |
+
return
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
c.JSON(http.StatusOK, gin.H{
|
| 836 |
+
"status": "success",
|
| 837 |
+
})
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
// MigrateTokensRemark 确保所有token都有remark字段
|
| 841 |
+
func MigrateTokensRemark() error {
|
| 842 |
+
// 获取所有token的key
|
| 843 |
+
keys, err := config.RedisKeys("token:*")
|
| 844 |
+
if err != nil {
|
| 845 |
+
return fmt.Errorf("获取token列表失败: %v", err)
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
for _, key := range keys {
|
| 849 |
+
// 检查是否已有remark字段
|
| 850 |
+
exists, err := config.RedisHExists(key, "remark")
|
| 851 |
+
if err != nil {
|
| 852 |
+
logger.Log.Error("check remark field of token %s failed: %v", key, err)
|
| 853 |
+
continue
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
// 如果没有remark字段,添加一个空的remark
|
| 857 |
+
if !exists {
|
| 858 |
+
err = config.RedisHSet(key, "remark", "")
|
| 859 |
+
if err != nil {
|
| 860 |
+
logger.Log.Error("add remark field to token %s failed: %v", key, err)
|
| 861 |
+
continue
|
| 862 |
+
}
|
| 863 |
+
logger.Log.Info("add remark field to token %s success", key)
|
| 864 |
+
}
|
| 865 |
+
}
|
| 866 |
+
logger.Log.Info("migrate remark field to all tokens success!")
|
| 867 |
+
|
| 868 |
+
return nil
|
| 869 |
+
}
|
api/token_reset.go
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package api
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/config"
|
| 5 |
+
"augment2api/pkg/logger"
|
| 6 |
+
|
| 7 |
+
"github.com/robfig/cron/v3"
|
| 8 |
+
"github.com/sirupsen/logrus"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
// ResetTokenUsage 重置所有token的使用次数
|
| 12 |
+
func ResetTokenUsage() error {
|
| 13 |
+
// 获取所有token的key
|
| 14 |
+
keys, err := config.RedisKeys("token:*")
|
| 15 |
+
if err != nil {
|
| 16 |
+
return err
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
for _, key := range keys {
|
| 20 |
+
// 从key中提取token
|
| 21 |
+
token := key[6:] // 去掉前缀 "token:"
|
| 22 |
+
|
| 23 |
+
// 重置总使用次数
|
| 24 |
+
totalUsageKey := "token_usage:" + token
|
| 25 |
+
err = config.RedisSet(totalUsageKey, "0", 0) // 0表示永不过期
|
| 26 |
+
if err != nil {
|
| 27 |
+
logger.Log.WithFields(logrus.Fields{
|
| 28 |
+
"token": token,
|
| 29 |
+
"error": err,
|
| 30 |
+
}).Error("重置Token总使用次数失败")
|
| 31 |
+
continue
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// 重置CHAT模式使用次数
|
| 35 |
+
chatUsageKey := "token_usage_chat:" + token
|
| 36 |
+
err = config.RedisSet(chatUsageKey, "0", 0) // 0表示永不过期
|
| 37 |
+
if err != nil {
|
| 38 |
+
logger.Log.WithFields(logrus.Fields{
|
| 39 |
+
"token": token,
|
| 40 |
+
"error": err,
|
| 41 |
+
}).Error("重置Token CHAT模式使用次数失败")
|
| 42 |
+
continue
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// 重置AGENT模式使用次数
|
| 46 |
+
agentUsageKey := "token_usage_agent:" + token
|
| 47 |
+
err = config.RedisSet(agentUsageKey, "0", 0) // 0表示永不过期
|
| 48 |
+
if err != nil {
|
| 49 |
+
logger.Log.WithFields(logrus.Fields{
|
| 50 |
+
"token": token,
|
| 51 |
+
"error": err,
|
| 52 |
+
}).Error("重置Token AGENT模式使用次数失败")
|
| 53 |
+
continue
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
logger.Log.WithFields(logrus.Fields{
|
| 57 |
+
"token": token,
|
| 58 |
+
}).Info("重置token使用次数成功")
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
return nil
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// StartTokenUsageResetScheduler 启动token使用次数重置调度器
|
| 65 |
+
func StartTokenUsageResetScheduler() {
|
| 66 |
+
// 创建cron调度器
|
| 67 |
+
c := cron.New(cron.WithSeconds()) // 启用秒级精度
|
| 68 |
+
|
| 69 |
+
// 添加定时任务,每月1号零点一分执行
|
| 70 |
+
// 格式:秒 分 时 日 月 周
|
| 71 |
+
_, err := c.AddFunc("0 1 0 1 * *", func() {
|
| 72 |
+
logger.Log.Info("开始执行Token使用次数重置任务")
|
| 73 |
+
err := ResetTokenUsage()
|
| 74 |
+
if err != nil {
|
| 75 |
+
logger.Log.WithFields(logrus.Fields{
|
| 76 |
+
"error": err,
|
| 77 |
+
}).Error("执行Token使用次数重置任务失败")
|
| 78 |
+
} else {
|
| 79 |
+
logger.Log.Info("Token使用次数重置任务执行完成")
|
| 80 |
+
}
|
| 81 |
+
})
|
| 82 |
+
|
| 83 |
+
if err != nil {
|
| 84 |
+
logger.Log.WithFields(logrus.Fields{
|
| 85 |
+
"error": err,
|
| 86 |
+
}).Error("添加Token使用次数重置定时任务失败")
|
| 87 |
+
return
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// 启动cron调度器
|
| 91 |
+
c.Start()
|
| 92 |
+
logger.Log.Info("Token使用次数重置调度器启动成功!")
|
| 93 |
+
|
| 94 |
+
// 保持程序运行
|
| 95 |
+
select {}
|
| 96 |
+
}
|
config/config.go
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package config
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/pkg/logger"
|
| 5 |
+
"os"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
type Config struct {
|
| 9 |
+
RedisConnString string
|
| 10 |
+
AuthToken string
|
| 11 |
+
CodingMode string
|
| 12 |
+
CodingToken string
|
| 13 |
+
TenantURL string
|
| 14 |
+
AccessPwd string
|
| 15 |
+
RoutePrefix string
|
| 16 |
+
ProxyURL string
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const version = "v1.0.2"
|
| 20 |
+
|
| 21 |
+
var AppConfig Config
|
| 22 |
+
|
| 23 |
+
func InitConfig() error {
|
| 24 |
+
// 从环境变量读取配置
|
| 25 |
+
AppConfig = Config{
|
| 26 |
+
// 必填配置
|
| 27 |
+
RedisConnString: getEnv("REDIS_CONN_STRING", ""),
|
| 28 |
+
AccessPwd: getEnv("ACCESS_PWD", ""),
|
| 29 |
+
// 非必填配置
|
| 30 |
+
AuthToken: getEnv("AUTH_TOKEN", ""), // api鉴权token
|
| 31 |
+
RoutePrefix: getEnv("ROUTE_PREFIX", ""), // 自定义openai接口路由前缀
|
| 32 |
+
CodingMode: getEnv("CODING_MODE", "false"),
|
| 33 |
+
CodingToken: getEnv("CODING_TOKEN", ""),
|
| 34 |
+
TenantURL: getEnv("TENANT_URL", ""),
|
| 35 |
+
ProxyURL: getEnv("PROXY_URL", ""), // 代理URL配置
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
if AppConfig.CodingMode == "false" {
|
| 39 |
+
|
| 40 |
+
// redis连接字符串 示例: redis://default:pwd@localhost:6379
|
| 41 |
+
if AppConfig.RedisConnString == "" {
|
| 42 |
+
logger.Log.Fatalln("未配置环境变量 REDIS_CONN_STRING")
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// 为了安全,必须配置访问密码
|
| 48 |
+
if AppConfig.AccessPwd == "" {
|
| 49 |
+
logger.Log.Fatalln("未配置环境变量 ACCESS_PWD")
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// 打印欢迎信息
|
| 53 |
+
logger.Log.Info("Welcome to use Augment2Api! Current Version: " + version)
|
| 54 |
+
|
| 55 |
+
logger.Log.Info("Augment2Api配置加载完成:\n" +
|
| 56 |
+
"----------------------------------------\n" +
|
| 57 |
+
"AuthToken: " + AppConfig.AuthToken + "\n" +
|
| 58 |
+
"AccessPwd: " + AppConfig.AccessPwd + "\n" +
|
| 59 |
+
"RedisConnString: " + AppConfig.RedisConnString + "\n" +
|
| 60 |
+
"RoutePrefix: " + AppConfig.RoutePrefix + "\n" +
|
| 61 |
+
"ProxyURL: " + AppConfig.ProxyURL + "\n" +
|
| 62 |
+
"----------------------------------------")
|
| 63 |
+
|
| 64 |
+
logger.Log.Info("Everything is set up, now start to fully enjoy the charm of AI !")
|
| 65 |
+
|
| 66 |
+
return nil
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
func getEnv(key, defaultValue string) string {
|
| 70 |
+
value := os.Getenv(key)
|
| 71 |
+
if value == "" {
|
| 72 |
+
return defaultValue
|
| 73 |
+
}
|
| 74 |
+
return value
|
| 75 |
+
}
|
config/redis.go
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package config
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/pkg/logger"
|
| 5 |
+
"context"
|
| 6 |
+
"os"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/go-redis/redis/v8"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
var RDB redis.Cmdable
|
| 13 |
+
|
| 14 |
+
// InitRedisClient This function is called after init()
|
| 15 |
+
func InitRedisClient() (err error) {
|
| 16 |
+
|
| 17 |
+
RedisConnString := AppConfig.RedisConnString
|
| 18 |
+
if RedisConnString == "" {
|
| 19 |
+
logger.Log.Debug("REDIS_CONN_STRING not set, Redis is not enabled")
|
| 20 |
+
return nil
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
opt, err := redis.ParseURL(RedisConnString)
|
| 24 |
+
if err != nil {
|
| 25 |
+
logger.Log.Fatalln("failed to parse Redis connection string: " + err.Error())
|
| 26 |
+
}
|
| 27 |
+
RDB = redis.NewClient(opt)
|
| 28 |
+
|
| 29 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 30 |
+
defer cancel()
|
| 31 |
+
|
| 32 |
+
_, err = RDB.Ping(ctx).Result()
|
| 33 |
+
if err != nil {
|
| 34 |
+
logger.Log.Fatalln("Redis ping test failed: " + err.Error())
|
| 35 |
+
}
|
| 36 |
+
return err
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func ParseRedisOption() *redis.Options {
|
| 40 |
+
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
| 41 |
+
if err != nil {
|
| 42 |
+
logger.Log.Fatalln("failed to parse Redis connection string: " + err.Error())
|
| 43 |
+
}
|
| 44 |
+
return opt
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
func RedisSet(key string, value string, expiration time.Duration) error {
|
| 48 |
+
ctx := context.Background()
|
| 49 |
+
return RDB.Set(ctx, key, value, expiration).Err()
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
func RedisGet(key string) (string, error) {
|
| 53 |
+
ctx := context.Background()
|
| 54 |
+
return RDB.Get(ctx, key).Result()
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func RedisDel(key string) error {
|
| 58 |
+
ctx := context.Background()
|
| 59 |
+
return RDB.Del(ctx, key).Err()
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// RedisHSet 设置哈希表字段值
|
| 63 |
+
func RedisHSet(key, field, value string) error {
|
| 64 |
+
ctx := context.Background()
|
| 65 |
+
return RDB.HSet(ctx, key, field, value).Err()
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// RedisHGet 获取哈希表字段值
|
| 69 |
+
func RedisHGet(key, field string) (string, error) {
|
| 70 |
+
ctx := context.Background()
|
| 71 |
+
return RDB.HGet(ctx, key, field).Result()
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// RedisExpire 设置键的过期时间
|
| 75 |
+
func RedisExpire(key string, expiration time.Duration) error {
|
| 76 |
+
ctx := context.Background()
|
| 77 |
+
return RDB.Expire(ctx, key, expiration).Err()
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// RedisKeys 获取匹配指定模式的所有键
|
| 81 |
+
func RedisKeys(pattern string) ([]string, error) {
|
| 82 |
+
ctx := context.Background()
|
| 83 |
+
return RDB.Keys(ctx, pattern).Result()
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// RedisExists 检查键是否存在
|
| 87 |
+
func RedisExists(key string) (bool, error) {
|
| 88 |
+
ctx := context.Background()
|
| 89 |
+
result, err := RDB.Exists(ctx, key).Result()
|
| 90 |
+
if err != nil {
|
| 91 |
+
return false, err
|
| 92 |
+
}
|
| 93 |
+
return result > 0, nil
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// RedisIncr 增加Redis中的计数器
|
| 97 |
+
func RedisIncr(key string) error {
|
| 98 |
+
ctx := context.Background()
|
| 99 |
+
_, err := RDB.Incr(ctx, key).Result()
|
| 100 |
+
return err
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// RedisHExists 检查哈希表字段是否存在
|
| 104 |
+
func RedisHExists(key, field string) (bool, error) {
|
| 105 |
+
ctx := context.Background()
|
| 106 |
+
return RDB.HExists(ctx, key, field).Result()
|
| 107 |
+
}
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
redis:
|
| 5 |
+
image: redis:alpine
|
| 6 |
+
restart: always
|
| 7 |
+
volumes:
|
| 8 |
+
- redis_data:/data
|
| 9 |
+
command: redis-server --requirepass $${REDIS_PASSWORD:-yourpassword}
|
| 10 |
+
healthcheck:
|
| 11 |
+
test: ["CMD", "redis-cli", "-a", "$${REDIS_PASSWORD:-yourpassword}", "ping"]
|
| 12 |
+
interval: 5s
|
| 13 |
+
timeout: 3s
|
| 14 |
+
retries: 5
|
| 15 |
+
environment:
|
| 16 |
+
- TZ=Asia/Shanghai
|
| 17 |
+
- REDIS_PASSWORD=${REDIS_PASSWORD:-yourpassword}
|
| 18 |
+
|
| 19 |
+
augment2api:
|
| 20 |
+
build: .
|
| 21 |
+
restart: always
|
| 22 |
+
ports:
|
| 23 |
+
- "27080:27080"
|
| 24 |
+
environment:
|
| 25 |
+
- REDIS_CONN_STRING=redis://default:${REDIS_PASSWORD:-yourpassword}@redis:6379
|
| 26 |
+
- ACCESS_PWD=${ACCESS_PWD:-your-access-password}
|
| 27 |
+
- AUTH_TOKEN=${AUTH_TOKEN:-your-auth-token}
|
| 28 |
+
- TZ=Asia/Shanghai
|
| 29 |
+
depends_on:
|
| 30 |
+
redis:
|
| 31 |
+
condition: service_healthy
|
| 32 |
+
|
| 33 |
+
volumes:
|
| 34 |
+
redis_data:
|
| 35 |
+
|
| 36 |
+
networks:
|
| 37 |
+
default:
|
| 38 |
+
driver: bridge
|
go.mod
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module augment2api
|
| 2 |
+
|
| 3 |
+
go 1.23.0
|
| 4 |
+
|
| 5 |
+
toolchain go1.23.6
|
| 6 |
+
|
| 7 |
+
require (
|
| 8 |
+
github.com/gin-contrib/cors v1.7.4
|
| 9 |
+
github.com/gin-gonic/gin v1.10.0
|
| 10 |
+
github.com/go-redis/redis/v8 v8.11.5
|
| 11 |
+
github.com/google/uuid v1.6.0
|
| 12 |
+
github.com/robfig/cron/v3 v3.0.1
|
| 13 |
+
github.com/sirupsen/logrus v1.9.3
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
require (
|
| 17 |
+
github.com/bytedance/sonic v1.12.6 // indirect
|
| 18 |
+
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
| 19 |
+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
| 20 |
+
github.com/cloudwego/base64x v0.1.4 // indirect
|
| 21 |
+
github.com/cloudwego/iasm v0.2.0 // indirect
|
| 22 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
| 23 |
+
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
| 24 |
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
| 25 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
| 26 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
| 27 |
+
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
| 28 |
+
github.com/goccy/go-json v0.10.4 // indirect
|
| 29 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 30 |
+
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
| 31 |
+
github.com/kr/text v0.2.0 // indirect
|
| 32 |
+
github.com/leodido/go-urn v1.4.0 // indirect
|
| 33 |
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 34 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 35 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 36 |
+
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
| 37 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 38 |
+
github.com/ugorji/go/codec v1.2.12 // indirect
|
| 39 |
+
golang.org/x/arch v0.12.0 // indirect
|
| 40 |
+
golang.org/x/crypto v0.36.0 // indirect
|
| 41 |
+
golang.org/x/net v0.37.0 // indirect
|
| 42 |
+
golang.org/x/sys v0.31.0 // indirect
|
| 43 |
+
golang.org/x/text v0.23.0 // indirect
|
| 44 |
+
google.golang.org/protobuf v1.36.1 // indirect
|
| 45 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 46 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
| 2 |
+
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
| 3 |
+
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
| 4 |
+
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
| 5 |
+
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
| 6 |
+
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
| 7 |
+
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
| 8 |
+
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
| 9 |
+
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
| 10 |
+
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
| 11 |
+
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
| 12 |
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
| 13 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 14 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 15 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 16 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
| 17 |
+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
| 18 |
+
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
| 19 |
+
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
| 20 |
+
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
| 21 |
+
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
| 22 |
+
github.com/gin-contrib/cors v1.7.4 h1:/fC6/wk7rCRtqKqki8lLr2Xq+hnV49aXDLIuSek9g4k=
|
| 23 |
+
github.com/gin-contrib/cors v1.7.4/go.mod h1:vGc/APSgLMlQfEJV5NAzkrAHb0C8DetL3K6QZuvGii0=
|
| 24 |
+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
| 25 |
+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
| 26 |
+
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
| 27 |
+
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
| 28 |
+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
| 29 |
+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
| 30 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
| 31 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
| 32 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
| 33 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
| 34 |
+
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
| 35 |
+
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
| 36 |
+
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
| 37 |
+
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
| 38 |
+
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
| 39 |
+
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
| 40 |
+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
| 41 |
+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 42 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 43 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 44 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 45 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 46 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 47 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 48 |
+
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
| 49 |
+
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
| 50 |
+
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
| 51 |
+
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
| 52 |
+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
| 53 |
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
| 54 |
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
| 55 |
+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
| 56 |
+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
| 57 |
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 58 |
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 59 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 60 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 61 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 62 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 63 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 64 |
+
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
| 65 |
+
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
| 66 |
+
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
| 67 |
+
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
| 68 |
+
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
| 69 |
+
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
| 70 |
+
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
| 71 |
+
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
| 72 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 73 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 74 |
+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
| 75 |
+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
| 76 |
+
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
| 77 |
+
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
| 78 |
+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
| 79 |
+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
| 80 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 81 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 82 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 83 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 84 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 85 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 86 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 87 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 88 |
+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
| 89 |
+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
| 90 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 91 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 92 |
+
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
| 93 |
+
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
| 94 |
+
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
| 95 |
+
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
| 96 |
+
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
| 97 |
+
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
| 98 |
+
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
| 99 |
+
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
| 100 |
+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 101 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 102 |
+
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
| 103 |
+
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
| 104 |
+
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
| 105 |
+
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
| 106 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
| 107 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 108 |
+
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
| 109 |
+
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
| 110 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 111 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
| 112 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
| 113 |
+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
| 114 |
+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
| 115 |
+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
| 116 |
+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
| 117 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 118 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 119 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 120 |
+
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
main.go
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/api"
|
| 5 |
+
"augment2api/config"
|
| 6 |
+
"augment2api/middleware"
|
| 7 |
+
"augment2api/pkg/logger"
|
| 8 |
+
"crypto/rand"
|
| 9 |
+
"crypto/sha256"
|
| 10 |
+
"encoding/base64"
|
| 11 |
+
"encoding/json"
|
| 12 |
+
"fmt"
|
| 13 |
+
"log"
|
| 14 |
+
"net/http"
|
| 15 |
+
"net/url"
|
| 16 |
+
"strings"
|
| 17 |
+
"time"
|
| 18 |
+
|
| 19 |
+
"github.com/gin-gonic/gin"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
const clientID = "v"
|
| 23 |
+
|
| 24 |
+
// OAuthState 存储OAuth状态信息
|
| 25 |
+
type OAuthState struct {
|
| 26 |
+
CodeVerifier string `json:"code_verifier"`
|
| 27 |
+
CodeChallenge string `json:"code_challenge"`
|
| 28 |
+
State string `json:"state"`
|
| 29 |
+
CreationTime time.Time `json:"creation_time"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// 全局变量存储OAuth状态
|
| 33 |
+
var (
|
| 34 |
+
globalOAuthState OAuthState
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
// base64URLEncode 编码Buffer为base64 URL安全格式
|
| 38 |
+
func base64URLEncode(data []byte) string {
|
| 39 |
+
encoded := base64.StdEncoding.EncodeToString(data)
|
| 40 |
+
encoded = strings.ReplaceAll(encoded, "+", "-")
|
| 41 |
+
encoded = strings.ReplaceAll(encoded, "/", "_")
|
| 42 |
+
encoded = strings.ReplaceAll(encoded, "=", "")
|
| 43 |
+
return encoded
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// sha256Hash 计算SHA256哈希
|
| 47 |
+
func sha256Hash(input []byte) []byte {
|
| 48 |
+
hash := sha256.Sum256(input)
|
| 49 |
+
return hash[:]
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// createOAuthState 创建OAuth状态
|
| 53 |
+
func createOAuthState() OAuthState {
|
| 54 |
+
codeVerifierBytes := make([]byte, 32)
|
| 55 |
+
_, err := rand.Read(codeVerifierBytes)
|
| 56 |
+
if err != nil {
|
| 57 |
+
log.Fatalf("生成随机字节失败: %v", err)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
codeVerifier := base64URLEncode(codeVerifierBytes)
|
| 61 |
+
codeChallenge := base64URLEncode(sha256Hash([]byte(codeVerifier)))
|
| 62 |
+
|
| 63 |
+
stateBytes := make([]byte, 8)
|
| 64 |
+
_, err = rand.Read(stateBytes)
|
| 65 |
+
if err != nil {
|
| 66 |
+
log.Fatalf("生成随机状态失败: %v", err)
|
| 67 |
+
}
|
| 68 |
+
state := base64URLEncode(stateBytes)
|
| 69 |
+
|
| 70 |
+
return OAuthState{
|
| 71 |
+
CodeVerifier: codeVerifier,
|
| 72 |
+
CodeChallenge: codeChallenge,
|
| 73 |
+
State: state,
|
| 74 |
+
CreationTime: time.Now(),
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// generateAuthorizeURL 生成授权URL
|
| 79 |
+
func generateAuthorizeURL(oauthState OAuthState) string {
|
| 80 |
+
params := url.Values{}
|
| 81 |
+
params.Add("response_type", "code")
|
| 82 |
+
params.Add("code_challenge", oauthState.CodeChallenge)
|
| 83 |
+
params.Add("client_id", clientID)
|
| 84 |
+
params.Add("state", oauthState.State)
|
| 85 |
+
params.Add("prompt", "login")
|
| 86 |
+
|
| 87 |
+
authorizeURL := fmt.Sprintf("https://auth.augmentcode.com/authorize?%s", params.Encode())
|
| 88 |
+
return authorizeURL
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// getAccessToken 获取访问令牌
|
| 92 |
+
func getAccessToken(tenantURL, codeVerifier, code string) (string, error) {
|
| 93 |
+
data := map[string]string{
|
| 94 |
+
"grant_type": "authorization_code",
|
| 95 |
+
"client_id": clientID,
|
| 96 |
+
"code_verifier": codeVerifier,
|
| 97 |
+
"redirect_uri": "",
|
| 98 |
+
"code": code,
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
jsonData, err := json.Marshal(data)
|
| 102 |
+
if err != nil {
|
| 103 |
+
return "", fmt.Errorf("序列化数据失败: %v", err)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
resp, err := http.Post(tenantURL+"token", "application/json", strings.NewReader(string(jsonData)))
|
| 107 |
+
if err != nil {
|
| 108 |
+
return "", fmt.Errorf("请求令牌失败: %v", err)
|
| 109 |
+
}
|
| 110 |
+
defer resp.Body.Close()
|
| 111 |
+
|
| 112 |
+
var result map[string]interface{}
|
| 113 |
+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
| 114 |
+
return "", fmt.Errorf("解析响应失败: %v", err)
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
token, ok := result["access_token"].(string)
|
| 118 |
+
if !ok {
|
| 119 |
+
return "", fmt.Errorf("响应中没有访问令牌")
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
return token, nil
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// 初始化路由
|
| 126 |
+
func setupRouter() *gin.Engine {
|
| 127 |
+
r := gin.Default()
|
| 128 |
+
|
| 129 |
+
// 跨域
|
| 130 |
+
r.Use(middleware.CORS())
|
| 131 |
+
|
| 132 |
+
// 初始化OAuth状态
|
| 133 |
+
globalOAuthState = createOAuthState()
|
| 134 |
+
|
| 135 |
+
// 静态文件服务
|
| 136 |
+
r.Static("/static", "./static")
|
| 137 |
+
r.LoadHTMLGlob("templates/*")
|
| 138 |
+
|
| 139 |
+
// 登录页面
|
| 140 |
+
r.GET("/login", func(c *gin.Context) {
|
| 141 |
+
c.HTML(http.StatusOK, "login.html", gin.H{})
|
| 142 |
+
})
|
| 143 |
+
|
| 144 |
+
// 登录
|
| 145 |
+
r.POST("/api/login", api.LoginHandler)
|
| 146 |
+
|
| 147 |
+
// 登出
|
| 148 |
+
r.POST("/api/logout", api.LogoutHandler)
|
| 149 |
+
|
| 150 |
+
// 管理页面 - 需要会话验证
|
| 151 |
+
r.GET("/", func(c *gin.Context) {
|
| 152 |
+
// 如果设置了访问密码,检查是否已登录
|
| 153 |
+
if config.AppConfig.AccessPwd != "" {
|
| 154 |
+
// 从查询参数或Cookie中获取会话令牌
|
| 155 |
+
token := c.Query("token")
|
| 156 |
+
if token == "" {
|
| 157 |
+
// 尝试从Cookie获取
|
| 158 |
+
token, _ = c.Cookie("auth_token")
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// 从请求头获取
|
| 162 |
+
if token == "" {
|
| 163 |
+
token = c.GetHeader("X-Auth-Token")
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// 验证会话令牌
|
| 167 |
+
if !api.ValidateToken(token) {
|
| 168 |
+
c.Redirect(http.StatusFound, "/login")
|
| 169 |
+
return
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
c.HTML(http.StatusOK, "admin.html", gin.H{})
|
| 173 |
+
})
|
| 174 |
+
|
| 175 |
+
// 管理页面 - 需要会话验证
|
| 176 |
+
r.GET("/admin", api.AuthTokenMiddleware(), func(c *gin.Context) {
|
| 177 |
+
c.HTML(http.StatusOK, "admin.html", gin.H{})
|
| 178 |
+
})
|
| 179 |
+
|
| 180 |
+
// 授权端点 - 需要会话验证
|
| 181 |
+
r.GET("/auth", api.AuthTokenMiddleware(), func(c *gin.Context) {
|
| 182 |
+
authorizeURL := generateAuthorizeURL(globalOAuthState)
|
| 183 |
+
api.AuthHandler(c, authorizeURL)
|
| 184 |
+
})
|
| 185 |
+
|
| 186 |
+
// 获取token - 需要会话验证
|
| 187 |
+
r.GET("/api/tokens", api.AuthTokenMiddleware(), api.GetRedisTokenHandler)
|
| 188 |
+
|
| 189 |
+
// 删除token - 需要会话验证
|
| 190 |
+
r.DELETE("/api/token/:token", api.AuthTokenMiddleware(), api.DeleteTokenHandler)
|
| 191 |
+
|
| 192 |
+
// 更新token备注 - 需要会话验证
|
| 193 |
+
r.PUT("/api/token/:token/remark", api.AuthTokenMiddleware(), api.UpdateTokenRemark)
|
| 194 |
+
|
| 195 |
+
// 批量检测token - 需要会话���证
|
| 196 |
+
r.GET("/api/check-tokens", api.AuthTokenMiddleware(), api.CheckAllTokensHandler)
|
| 197 |
+
|
| 198 |
+
// 回调端点,用于处理授权码 - 需要会话验证
|
| 199 |
+
r.POST("/callback", api.AuthTokenMiddleware(), func(c *gin.Context) {
|
| 200 |
+
api.CallbackHandler(c, func(tenantURL, _, code string) (string, error) {
|
| 201 |
+
return getAccessToken(tenantURL, globalOAuthState.CodeVerifier, code)
|
| 202 |
+
})
|
| 203 |
+
})
|
| 204 |
+
|
| 205 |
+
// 鉴权路由组
|
| 206 |
+
authGroup := r.Group(ProcessPath(config.AppConfig.RoutePrefix))
|
| 207 |
+
authGroup.Use(api.AuthMiddleware())
|
| 208 |
+
{
|
| 209 |
+
// OpenAI兼容的聊天端点
|
| 210 |
+
chatGroup := authGroup.Group("/")
|
| 211 |
+
// 并发控制
|
| 212 |
+
chatGroup.Use(middleware.TokenConcurrencyMiddleware())
|
| 213 |
+
{
|
| 214 |
+
chatGroup.POST("/v1/chat/completions", api.ChatCompletionsHandler)
|
| 215 |
+
chatGroup.POST("/v1", api.ChatCompletionsHandler)
|
| 216 |
+
chatGroup.POST("/v1/chat", api.ChatCompletionsHandler)
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
authGroup.GET("/v1/models", api.ModelsHandler)
|
| 220 |
+
authGroup.POST("/api/add/tokens", api.AddTokenHandler)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
return r
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
func ProcessPath(path string) string {
|
| 227 |
+
// 判断字符串是否为空
|
| 228 |
+
if path == "" {
|
| 229 |
+
return ""
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// 判断开头是否为/,不是则添加
|
| 233 |
+
if !strings.HasPrefix(path, "/") {
|
| 234 |
+
path = "/" + path
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// 判断结尾是否为/,是则去掉
|
| 238 |
+
if strings.HasSuffix(path, "/") {
|
| 239 |
+
path = path[:len(path)-1]
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
return path
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
func main() {
|
| 246 |
+
// 设置全局时区为东八区(CST)
|
| 247 |
+
time.Local = time.FixedZone("CST", 8*3600)
|
| 248 |
+
|
| 249 |
+
// 设置 Gin 为发布模式
|
| 250 |
+
gin.SetMode(gin.ReleaseMode)
|
| 251 |
+
|
| 252 |
+
// 初始化日志
|
| 253 |
+
logger.Init()
|
| 254 |
+
|
| 255 |
+
// 初始化配置
|
| 256 |
+
err := config.InitConfig()
|
| 257 |
+
if err != nil {
|
| 258 |
+
logger.Log.Fatalln("failed to initialize config: " + err.Error())
|
| 259 |
+
return
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// 初始化Redis
|
| 263 |
+
err = config.InitRedisClient()
|
| 264 |
+
if err != nil {
|
| 265 |
+
logger.Log.Fatalln("failed to initialize Redis: " + err.Error())
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
// token备注字段迁移
|
| 269 |
+
err = api.MigrateTokensRemark()
|
| 270 |
+
if err != nil {
|
| 271 |
+
logger.Log.Error("Token备注字段迁移失败: %v", err)
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// 启动token使用次数重置调度器
|
| 275 |
+
go api.StartTokenUsageResetScheduler()
|
| 276 |
+
|
| 277 |
+
r := setupRouter()
|
| 278 |
+
|
| 279 |
+
// 启动服务器
|
| 280 |
+
if err := r.Run(":7860"); err != nil {
|
| 281 |
+
logger.Log.Fatalf("启动服务失败: %v", err)
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
logger.Log.WithFields(map[string]interface{}{
|
| 285 |
+
"port": 27080,
|
| 286 |
+
"mode": gin.Mode(),
|
| 287 |
+
}).Info("Augment2API 服务启动成功")
|
| 288 |
+
}
|
middleware/concurrency.go
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"augment2api/api"
|
| 5 |
+
"augment2api/config"
|
| 6 |
+
"augment2api/pkg/logger"
|
| 7 |
+
"net/http"
|
| 8 |
+
"strings"
|
| 9 |
+
"sync"
|
| 10 |
+
"time"
|
| 11 |
+
|
| 12 |
+
"github.com/gin-gonic/gin"
|
| 13 |
+
"github.com/sirupsen/logrus"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// 全局锁映射,用于控制每个 token 的并发请求
|
| 17 |
+
var (
|
| 18 |
+
tokenLocks = make(map[string]*sync.Mutex)
|
| 19 |
+
tokenLocksGuard = sync.Mutex{}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
// getTokenLock 获取指定 token 的锁
|
| 23 |
+
func getTokenLock(token string) *sync.Mutex {
|
| 24 |
+
tokenLocksGuard.Lock()
|
| 25 |
+
defer tokenLocksGuard.Unlock()
|
| 26 |
+
|
| 27 |
+
if lock, exists := tokenLocks[token]; exists {
|
| 28 |
+
return lock
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
lock := &sync.Mutex{}
|
| 32 |
+
tokenLocks[token] = lock
|
| 33 |
+
return lock
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// TokenConcurrencyMiddleware 控制Redis中token的使用频率
|
| 37 |
+
func TokenConcurrencyMiddleware() gin.HandlerFunc {
|
| 38 |
+
return func(c *gin.Context) {
|
| 39 |
+
// 只对聊天完成请求进行并发控制
|
| 40 |
+
if !strings.HasSuffix(c.Request.URL.Path, "/chat/completions") {
|
| 41 |
+
c.Next()
|
| 42 |
+
return
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// 调试模式无需限制
|
| 46 |
+
if config.AppConfig.CodingMode == "true" {
|
| 47 |
+
token := config.AppConfig.CodingToken
|
| 48 |
+
tenantURL := config.AppConfig.TenantURL
|
| 49 |
+
c.Set("token", token)
|
| 50 |
+
c.Set("tenant_url", tenantURL)
|
| 51 |
+
c.Next()
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// 获取一个可用的token
|
| 55 |
+
token, tenantURL := api.GetAvailableToken()
|
| 56 |
+
if token == "No token" {
|
| 57 |
+
c.JSON(http.StatusTooManyRequests, gin.H{"error": "当前无可用token,请在页面添加"})
|
| 58 |
+
c.Abort()
|
| 59 |
+
return
|
| 60 |
+
}
|
| 61 |
+
if token == "No available token" || tenantURL == "" {
|
| 62 |
+
c.JSON(http.StatusTooManyRequests, gin.H{"error": "当前请求过多,请稍后再试"})
|
| 63 |
+
c.Abort()
|
| 64 |
+
return
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// 获取该token的锁
|
| 68 |
+
lock := getTokenLock(token)
|
| 69 |
+
|
| 70 |
+
// 尝试获取锁,会阻塞直到获取到锁
|
| 71 |
+
lock.Lock()
|
| 72 |
+
|
| 73 |
+
// 更新请求状态
|
| 74 |
+
err := api.SetTokenRequestStatus(token, api.TokenRequestStatus{
|
| 75 |
+
InProgress: true,
|
| 76 |
+
LastRequestAt: time.Now(),
|
| 77 |
+
})
|
| 78 |
+
|
| 79 |
+
if err != nil {
|
| 80 |
+
lock.Unlock()
|
| 81 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新token请求状态失败"})
|
| 82 |
+
c.Abort()
|
| 83 |
+
return
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
logger.Log.WithFields(logrus.Fields{
|
| 87 |
+
"token": token,
|
| 88 |
+
}).Info("本次请求使用的token: ")
|
| 89 |
+
|
| 90 |
+
// 在请求完成后释放锁
|
| 91 |
+
c.Set("token_lock", lock)
|
| 92 |
+
c.Set("token", token)
|
| 93 |
+
c.Set("tenant_url", tenantURL)
|
| 94 |
+
|
| 95 |
+
c.Next()
|
| 96 |
+
}
|
| 97 |
+
}
|
middleware/cors.go
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-contrib/cors"
|
| 5 |
+
"github.com/gin-gonic/gin"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
func CORS() gin.HandlerFunc {
|
| 9 |
+
config := cors.DefaultConfig()
|
| 10 |
+
config.AllowAllOrigins = true
|
| 11 |
+
config.AllowCredentials = true
|
| 12 |
+
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
|
| 13 |
+
config.AllowHeaders = []string{"*"}
|
| 14 |
+
return cors.New(config)
|
| 15 |
+
}
|
pkg/logger/logger.go
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package logger
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"github.com/sirupsen/logrus"
|
| 6 |
+
"os"
|
| 7 |
+
"strings"
|
| 8 |
+
"time"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
// CustomFormatter 自定义格式化器
|
| 12 |
+
type CustomFormatter struct {
|
| 13 |
+
TimestampFormat string
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
| 17 |
+
// 将时间调整为东八区
|
| 18 |
+
localTime := entry.Time.In(time.FixedZone("CST", 8*3600))
|
| 19 |
+
// 构建日志消息
|
| 20 |
+
timestamp := localTime.Format(f.TimestampFormat)
|
| 21 |
+
level := strings.ToUpper(entry.Level.String())
|
| 22 |
+
|
| 23 |
+
// 将所有字段合并到一个字符串中,添加适当的分隔
|
| 24 |
+
var fieldsStr string
|
| 25 |
+
if len(entry.Data) > 0 {
|
| 26 |
+
pairs := make([]string, 0, len(entry.Data))
|
| 27 |
+
for k, v := range entry.Data {
|
| 28 |
+
pairs = append(pairs, fmt.Sprintf("%s: %v", k, v))
|
| 29 |
+
}
|
| 30 |
+
fieldsStr = " | " + strings.Join(pairs, " | ")
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// 简化的日志格式,移除文件名和行号
|
| 34 |
+
logMsg := fmt.Sprintf("[%s] %-5s %s%s\n",
|
| 35 |
+
timestamp,
|
| 36 |
+
level,
|
| 37 |
+
entry.Message,
|
| 38 |
+
fieldsStr,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
return []byte(logMsg), nil
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
var Log = logrus.New()
|
| 45 |
+
|
| 46 |
+
func Init() {
|
| 47 |
+
// 使用自定义格式化器
|
| 48 |
+
Log.SetFormatter(&CustomFormatter{
|
| 49 |
+
TimestampFormat: "2006-01-02 15:04:05",
|
| 50 |
+
})
|
| 51 |
+
|
| 52 |
+
// 设置输出到标准输出
|
| 53 |
+
Log.SetOutput(os.Stdout)
|
| 54 |
+
|
| 55 |
+
// 设置日志级别
|
| 56 |
+
if os.Getenv("DEBUG") == "true" {
|
| 57 |
+
Log.SetLevel(logrus.DebugLevel)
|
| 58 |
+
} else {
|
| 59 |
+
Log.SetLevel(logrus.InfoLevel)
|
| 60 |
+
}
|
| 61 |
+
}
|
static/augment.svg
ADDED
|
|
templates/admin.html
ADDED
|
@@ -0,0 +1,1529 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Augment2Api-Panel</title>
|
| 7 |
+
<link rel="icon" href="../static/augment.svg" type="image/svg+xml">
|
| 8 |
+
<link rel="alternate icon" href="../static/augment.svg" type="image/x-icon">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--primary-color: #4a6cf7;
|
| 12 |
+
--primary-hover: #3a5ce4;
|
| 13 |
+
--bg-color: #f5f7fa;
|
| 14 |
+
--card-bg: #ffffff;
|
| 15 |
+
--text-color: #333333;
|
| 16 |
+
--text-secondary: #6c757d;
|
| 17 |
+
--border-color: #e9ecef;
|
| 18 |
+
--header-bg: #ffffff;
|
| 19 |
+
--header-color: #333333;
|
| 20 |
+
--sidebar-bg: #ffffff;
|
| 21 |
+
--sidebar-color: #333333;
|
| 22 |
+
--sidebar-hover: #f0f4ff;
|
| 23 |
+
--sidebar-active: #e6edff;
|
| 24 |
+
--footer-bg: #ffffff;
|
| 25 |
+
--footer-color: #6c757d;
|
| 26 |
+
--success-color: #28a745;
|
| 27 |
+
--error-color: #dc3545;
|
| 28 |
+
--warning-color: #ffc107;
|
| 29 |
+
--radius: 8px;
|
| 30 |
+
--shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
| 31 |
+
--transition: all 0.3s ease;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
html, body {
|
| 35 |
+
height: 100%;
|
| 36 |
+
margin: 0;
|
| 37 |
+
padding: 0;
|
| 38 |
+
overflow: hidden;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
body {
|
| 42 |
+
display: flex;
|
| 43 |
+
flex-direction: column;
|
| 44 |
+
background-color: var(--bg-color);
|
| 45 |
+
color: var(--text-color);
|
| 46 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 47 |
+
line-height: 1.6;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.container {
|
| 51 |
+
display: flex;
|
| 52 |
+
flex-direction: column;
|
| 53 |
+
height: 100vh;
|
| 54 |
+
width: 100%;
|
| 55 |
+
max-width: 100%;
|
| 56 |
+
margin: 0;
|
| 57 |
+
padding: 0;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
header {
|
| 61 |
+
background-color: var(--header-bg);
|
| 62 |
+
color: var(--header-color);
|
| 63 |
+
padding: 8px 20px;
|
| 64 |
+
box-shadow: var(--shadow);
|
| 65 |
+
z-index: 10;
|
| 66 |
+
border-bottom: 1px solid var(--border-color);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.header-content {
|
| 70 |
+
display: flex;
|
| 71 |
+
justify-content: space-between;
|
| 72 |
+
align-items: center;
|
| 73 |
+
width: 100%;
|
| 74 |
+
height: 40px; /* 固定高度 */
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.header-content h1 {
|
| 78 |
+
font-size: 18px;
|
| 79 |
+
margin: 0;
|
| 80 |
+
font-weight: 600;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.logout-btn {
|
| 84 |
+
background-color: var(--primary-color);
|
| 85 |
+
color: white;
|
| 86 |
+
border: none;
|
| 87 |
+
border-radius: var(--radius);
|
| 88 |
+
padding: 6px 12px;
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
font-size: 14px;
|
| 91 |
+
transition: var(--transition);
|
| 92 |
+
display: flex;
|
| 93 |
+
align-items: center;
|
| 94 |
+
gap: 5px;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.logout-btn:hover {
|
| 98 |
+
background-color: var(--primary-hover);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.dashboard {
|
| 102 |
+
display: flex;
|
| 103 |
+
flex: 1;
|
| 104 |
+
height: calc(100vh - 100px);
|
| 105 |
+
overflow: hidden;
|
| 106 |
+
margin: 0;
|
| 107 |
+
padding: 15px;
|
| 108 |
+
gap: 15px;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.sidebar {
|
| 112 |
+
width: 200px;
|
| 113 |
+
height: 100%;
|
| 114 |
+
background-color: var(--sidebar-bg);
|
| 115 |
+
color: var(--sidebar-color);
|
| 116 |
+
transition: width 0.3s ease;
|
| 117 |
+
border-radius: var(--radius);
|
| 118 |
+
box-shadow: var(--shadow);
|
| 119 |
+
overflow: hidden;
|
| 120 |
+
display: flex;
|
| 121 |
+
flex-direction: column;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.sidebar.collapsed {
|
| 125 |
+
width: 80px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.sidebar.collapsed .sidebar-header h3 {
|
| 129 |
+
display: none;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.sidebar-header {
|
| 133 |
+
padding: 15px;
|
| 134 |
+
display: flex;
|
| 135 |
+
justify-content: space-between;
|
| 136 |
+
align-items: center;
|
| 137 |
+
border-bottom: 1px solid var(--border-color);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.sidebar-header h3 {
|
| 141 |
+
margin: 0;
|
| 142 |
+
color: var(--text-color);
|
| 143 |
+
font-size: 16px;
|
| 144 |
+
font-weight: 600;
|
| 145 |
+
white-space: nowrap;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.toggle-btn {
|
| 149 |
+
background: transparent;
|
| 150 |
+
border: none;
|
| 151 |
+
color: var(--text-secondary);
|
| 152 |
+
cursor: pointer;
|
| 153 |
+
font-size: 16px;
|
| 154 |
+
padding: 5px;
|
| 155 |
+
display: flex;
|
| 156 |
+
align-items: center;
|
| 157 |
+
justify-content: center;
|
| 158 |
+
transition: transform 0.3s, background-color 0.2s;
|
| 159 |
+
border-radius: 4px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.toggle-btn:hover {
|
| 163 |
+
background-color: #f1f1f1;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.sidebar.collapsed .toggle-btn {
|
| 167 |
+
transform: rotate(180deg);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.sidebar-menu {
|
| 171 |
+
display: flex;
|
| 172 |
+
flex-direction: column;
|
| 173 |
+
padding: 10px 0;
|
| 174 |
+
flex: 1;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.menu-item {
|
| 178 |
+
padding: 10px 15px;
|
| 179 |
+
display: flex;
|
| 180 |
+
align-items: center;
|
| 181 |
+
cursor: pointer;
|
| 182 |
+
transition: var(--transition);
|
| 183 |
+
white-space: nowrap;
|
| 184 |
+
border-radius: 4px;
|
| 185 |
+
margin: 2px 8px;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.menu-item:hover {
|
| 189 |
+
background-color: var(--sidebar-hover);
|
| 190 |
+
color: var(--primary-color);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.menu-item.active {
|
| 194 |
+
background-color: var(--sidebar-active);
|
| 195 |
+
color: var(--primary-color);
|
| 196 |
+
font-weight: 500;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.menu-item i {
|
| 200 |
+
font-size: 18px;
|
| 201 |
+
margin-right: 12px;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.sidebar.collapsed .menu-item {
|
| 205 |
+
padding: 10px 8px;
|
| 206 |
+
justify-content: center;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.sidebar.collapsed .menu-text {
|
| 210 |
+
display: none;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.sidebar.collapsed .menu-item i {
|
| 214 |
+
margin-right: 0;
|
| 215 |
+
font-size: 20px;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.sidebar.collapsed .sidebar-header {
|
| 219 |
+
justify-content: center;
|
| 220 |
+
padding: 15px 5px;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
/* 主内容区样式优化 */
|
| 224 |
+
.main-content {
|
| 225 |
+
flex: 1;
|
| 226 |
+
height: 100%;
|
| 227 |
+
overflow: hidden;
|
| 228 |
+
display: flex;
|
| 229 |
+
flex-direction: column;
|
| 230 |
+
background-color: var(--card-bg);
|
| 231 |
+
border-radius: var(--radius);
|
| 232 |
+
box-shadow: var(--shadow);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.content-panel {
|
| 236 |
+
padding: 20px;
|
| 237 |
+
display: none;
|
| 238 |
+
flex: 1;
|
| 239 |
+
overflow-y: auto;
|
| 240 |
+
height: 100%;
|
| 241 |
+
flex-direction: column;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.content-panel.active {
|
| 245 |
+
display: flex;
|
| 246 |
+
flex-direction: column;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
/* 添加token列表容器样式,使其可滚动 */
|
| 250 |
+
.token-list-container {
|
| 251 |
+
flex: 1;
|
| 252 |
+
overflow-y: auto;
|
| 253 |
+
margin-bottom: 15px;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/* 面板标题样式优化 */
|
| 257 |
+
.panel-title {
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
margin-bottom: 20px;
|
| 261 |
+
padding-bottom: 12px;
|
| 262 |
+
border-bottom: 1px solid var(--border-color);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.panel-title h2 {
|
| 266 |
+
margin: 0 0 0 10px;
|
| 267 |
+
font-size: 18px;
|
| 268 |
+
font-weight: 600;
|
| 269 |
+
color: var(--text-color);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.panel-title i {
|
| 273 |
+
font-size: 20px;
|
| 274 |
+
color: var(--primary-color);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.panel-actions {
|
| 278 |
+
margin-left: auto;
|
| 279 |
+
display: flex;
|
| 280 |
+
gap: 10px;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
/* 按钮样式统一 */
|
| 284 |
+
button {
|
| 285 |
+
background-color: var(--primary-color);
|
| 286 |
+
color: white;
|
| 287 |
+
border: none;
|
| 288 |
+
border-radius: var(--radius);
|
| 289 |
+
padding: 8px 12px;
|
| 290 |
+
cursor: pointer;
|
| 291 |
+
font-size: 14px;
|
| 292 |
+
transition: var(--transition);
|
| 293 |
+
display: flex;
|
| 294 |
+
align-items: center;
|
| 295 |
+
gap: 5px;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
button:hover {
|
| 299 |
+
background-color: var(--primary-hover);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
button.secondary {
|
| 303 |
+
background-color: transparent;
|
| 304 |
+
color: var(--text-color);
|
| 305 |
+
border: 1px solid var(--border-color);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
button.secondary:hover {
|
| 309 |
+
background-color: #f8f9fa;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
button.secondary i {
|
| 313 |
+
color: var(--text-color);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
/* Token列表样式优化 */
|
| 317 |
+
.token-list {
|
| 318 |
+
display: flex;
|
| 319 |
+
flex-direction: column;
|
| 320 |
+
gap: 10px;
|
| 321 |
+
margin-bottom: 15px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.token-item {
|
| 325 |
+
border: 1px solid var(--border-color);
|
| 326 |
+
border-radius: var(--radius);
|
| 327 |
+
overflow: hidden;
|
| 328 |
+
transition: var(--transition);
|
| 329 |
+
margin-bottom: 10px;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.token-item:hover {
|
| 333 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.token-header {
|
| 337 |
+
display: flex;
|
| 338 |
+
align-items: center;
|
| 339 |
+
padding: 12px 15px;
|
| 340 |
+
background-color: #f8f9fa;
|
| 341 |
+
cursor: pointer;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.token-number {
|
| 345 |
+
width: 30px;
|
| 346 |
+
font-weight: 500;
|
| 347 |
+
color: var(--text-secondary);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.token-summary {
|
| 351 |
+
flex: 1;
|
| 352 |
+
font-family: monospace;
|
| 353 |
+
color: var(--text-color);
|
| 354 |
+
white-space: nowrap;
|
| 355 |
+
overflow: hidden;
|
| 356 |
+
text-overflow: ellipsis;
|
| 357 |
+
font-size: 13px;
|
| 358 |
+
display: flex;
|
| 359 |
+
align-items: center;
|
| 360 |
+
gap: 8px;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.token-remark {
|
| 364 |
+
background-color: #e3f2fd;
|
| 365 |
+
color: #1976d2;
|
| 366 |
+
padding: 2px 8px;
|
| 367 |
+
border-radius: 4px;
|
| 368 |
+
font-size: 12px;
|
| 369 |
+
font-family: system-ui;
|
| 370 |
+
cursor: pointer;
|
| 371 |
+
border: 1px solid transparent;
|
| 372 |
+
transition: all 0.2s;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.token-remark:hover {
|
| 376 |
+
border-color: #1976d2;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.token-remark.empty {
|
| 380 |
+
background-color: #f5f5f5;
|
| 381 |
+
color: #9e9e9e;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.token-remark input {
|
| 385 |
+
background: none;
|
| 386 |
+
border: none;
|
| 387 |
+
outline: none;
|
| 388 |
+
font-size: inherit;
|
| 389 |
+
font-family: inherit;
|
| 390 |
+
color: inherit;
|
| 391 |
+
width: 100%;
|
| 392 |
+
min-width: 100px;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.token-usage-count {
|
| 396 |
+
display: flex;
|
| 397 |
+
align-items: center;
|
| 398 |
+
justify-content: center;
|
| 399 |
+
color: var(--text-color);
|
| 400 |
+
font-size: 13px;
|
| 401 |
+
font-weight: 500;
|
| 402 |
+
margin-left: 10px;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
/* 根据使用次数变化颜色 - 只应用于数字 */
|
| 406 |
+
.token-usage-count .low {
|
| 407 |
+
color: #28a745; /* 绿色 - 使用次数少 */
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.token-usage-count .medium {
|
| 411 |
+
color: #ffc107; /* 黄色 - 使用次数中等 */
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
.token-usage-count .high {
|
| 415 |
+
color: #dc3545; /* 红色 - 使用次数多 */
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.token-toggle {
|
| 419 |
+
margin-left: 10px;
|
| 420 |
+
transition: transform 0.3s;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.token-toggle.open i {
|
| 424 |
+
transform: rotate(180deg);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.token-details {
|
| 428 |
+
padding: 0;
|
| 429 |
+
max-height: 0;
|
| 430 |
+
overflow: hidden;
|
| 431 |
+
transition: all 0.3s ease;
|
| 432 |
+
background-color: #ffffff;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.token-details.open {
|
| 436 |
+
padding: 15px;
|
| 437 |
+
max-height: 200px;
|
| 438 |
+
border-top: 1px solid var(--border-color);
|
| 439 |
+
overflow-y: auto;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.token-label {
|
| 443 |
+
font-weight: 500;
|
| 444 |
+
margin-bottom: 5px;
|
| 445 |
+
color: var(--text-secondary);
|
| 446 |
+
font-size: 13px;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.token-display {
|
| 450 |
+
padding: 8px 10px;
|
| 451 |
+
background-color: #f8f9fa;
|
| 452 |
+
border-radius: 4px;
|
| 453 |
+
font-family: monospace;
|
| 454 |
+
margin-bottom: 10px;
|
| 455 |
+
word-break: break-all;
|
| 456 |
+
font-size: 13px;
|
| 457 |
+
overflow-x: auto;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.token-actions {
|
| 461 |
+
display: flex;
|
| 462 |
+
justify-content: flex-end;
|
| 463 |
+
margin-top: 10px;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.delete-token {
|
| 467 |
+
background-color: var(--error-color);
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.delete-token:hover {
|
| 471 |
+
background-color: #c82333;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/* 分页控件样式优化 */
|
| 475 |
+
.pagination-container {
|
| 476 |
+
display: flex;
|
| 477 |
+
justify-content: center;
|
| 478 |
+
align-items: center;
|
| 479 |
+
padding: 15px 0;
|
| 480 |
+
border-top: 1px solid var(--border-color);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.pagination-btn {
|
| 484 |
+
background-color: transparent;
|
| 485 |
+
border: 1px solid var(--border-color);
|
| 486 |
+
color: var(--text-color);
|
| 487 |
+
border-radius: var(--radius);
|
| 488 |
+
padding: 6px 10px;
|
| 489 |
+
margin: 0 5px;
|
| 490 |
+
cursor: pointer;
|
| 491 |
+
transition: var(--transition);
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.pagination-btn:hover:not([disabled]) {
|
| 495 |
+
background-color: var(--primary-color);
|
| 496 |
+
color: white;
|
| 497 |
+
border-color: var(--primary-color);
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.pagination-btn[disabled] {
|
| 501 |
+
opacity: 0.5;
|
| 502 |
+
cursor: not-allowed;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
#page-info {
|
| 506 |
+
margin: 0 15px;
|
| 507 |
+
font-size: 14px;
|
| 508 |
+
color: var(--text-secondary);
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
.page-size-select {
|
| 512 |
+
margin-left: 15px;
|
| 513 |
+
padding: 6px 8px;
|
| 514 |
+
border-radius: var(--radius);
|
| 515 |
+
border: 1px solid var(--border-color);
|
| 516 |
+
background-color: white;
|
| 517 |
+
color: var(--text-color);
|
| 518 |
+
font-size: 14px;
|
| 519 |
+
cursor: pointer;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
/* 页脚样式优化 */
|
| 523 |
+
footer {
|
| 524 |
+
text-align: center;
|
| 525 |
+
padding: 10px 0;
|
| 526 |
+
background-color: var(--footer-bg);
|
| 527 |
+
color: var(--footer-color);
|
| 528 |
+
font-size: 13px;
|
| 529 |
+
border-top: 1px solid var(--border-color);
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
footer a {
|
| 533 |
+
color: var(--primary-color);
|
| 534 |
+
text-decoration: none;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
footer a:hover {
|
| 538 |
+
text-decoration: underline;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
/* 响应式设计优化 */
|
| 542 |
+
@media (max-width: 768px) {
|
| 543 |
+
.dashboard {
|
| 544 |
+
flex-direction: column;
|
| 545 |
+
height: auto;
|
| 546 |
+
padding: 10px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.sidebar {
|
| 550 |
+
width: 100% !important;
|
| 551 |
+
margin-bottom: 15px;
|
| 552 |
+
max-height: 200px;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.main-content {
|
| 556 |
+
height: calc(100vh - 300px);
|
| 557 |
+
overflow-y: auto;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.content-panel {
|
| 561 |
+
padding: 15px;
|
| 562 |
+
max-height: none;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.sidebar.collapsed .menu-text {
|
| 566 |
+
display: inline;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.toggle-btn {
|
| 570 |
+
display: none;
|
| 571 |
+
}
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
@media (max-width: 1200px) {
|
| 575 |
+
.token-display {
|
| 576 |
+
max-width: 100%;
|
| 577 |
+
white-space: nowrap;
|
| 578 |
+
}
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
@media (min-width: 1201px) {
|
| 582 |
+
.token-display {
|
| 583 |
+
white-space: normal;
|
| 584 |
+
}
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.token-details.open .token-display {
|
| 588 |
+
white-space: normal;
|
| 589 |
+
word-break: break-all;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
/* 添加Token面板样式优化 */
|
| 593 |
+
.auth-steps {
|
| 594 |
+
display: flex;
|
| 595 |
+
flex-direction: column;
|
| 596 |
+
gap: 25px;
|
| 597 |
+
padding-bottom: 20px;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.step {
|
| 601 |
+
background-color: #f8f9fa;
|
| 602 |
+
border-radius: var(--radius);
|
| 603 |
+
padding: 20px;
|
| 604 |
+
border: 1px solid var(--border-color);
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
.step h3 {
|
| 608 |
+
display: flex;
|
| 609 |
+
align-items: center;
|
| 610 |
+
margin-top: 0;
|
| 611 |
+
margin-bottom: 15px;
|
| 612 |
+
font-size: 16px;
|
| 613 |
+
font-weight: 600;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.step-number {
|
| 617 |
+
display: inline-flex;
|
| 618 |
+
align-items: center;
|
| 619 |
+
justify-content: center;
|
| 620 |
+
width: 24px;
|
| 621 |
+
height: 24px;
|
| 622 |
+
background-color: var(--primary-color);
|
| 623 |
+
color: white;
|
| 624 |
+
border-radius: 50%;
|
| 625 |
+
margin-right: 10px;
|
| 626 |
+
font-size: 14px;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.step p {
|
| 630 |
+
margin-bottom: 15px;
|
| 631 |
+
color: var(--text-secondary);
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
textarea {
|
| 635 |
+
width: 100%;
|
| 636 |
+
padding: 10px;
|
| 637 |
+
border: 1px solid var(--border-color);
|
| 638 |
+
border-radius: var(--radius);
|
| 639 |
+
font-family: monospace;
|
| 640 |
+
min-height: 100px;
|
| 641 |
+
margin-bottom: 10px;
|
| 642 |
+
resize: vertical;
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
.error {
|
| 646 |
+
color: var(--error-color);
|
| 647 |
+
margin: 10px 0;
|
| 648 |
+
display: none;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.success {
|
| 652 |
+
color: var(--success-color);
|
| 653 |
+
margin: 10px 0;
|
| 654 |
+
display: none;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
button:not(.secondary):not(.pagination-btn):not(.toggle-btn) i {
|
| 658 |
+
color: white;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
/* 刷新按钮加载动画 */
|
| 662 |
+
.refresh-btn, #check-all-tokens {
|
| 663 |
+
position: relative;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.refresh-btn i, #check-all-tokens i {
|
| 667 |
+
transition: transform 0.3s ease;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.refresh-btn.loading i, #check-all-tokens.loading i {
|
| 671 |
+
animation: spin 1s linear infinite;
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
@keyframes spin {
|
| 675 |
+
0% { transform: rotate(0deg); }
|
| 676 |
+
100% { transform: rotate(360deg); }
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
/* 禁用状态 */
|
| 680 |
+
.refresh-btn.loading, #check-all-tokens.loading {
|
| 681 |
+
pointer-events: none;
|
| 682 |
+
opacity: 0.7;
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
/* 检测结果样式 */
|
| 686 |
+
.check-result {
|
| 687 |
+
color: var(--success-color);
|
| 688 |
+
background-color: rgba(40, 167, 69, 0.1);
|
| 689 |
+
border: 1px solid var(--success-color);
|
| 690 |
+
border-radius: var(--radius);
|
| 691 |
+
padding: 10px 15px;
|
| 692 |
+
margin: 10px 0;
|
| 693 |
+
display: none;
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
|
| 697 |
+
/* 弹出输入框样式 */
|
| 698 |
+
.remark-input-modal {
|
| 699 |
+
position: fixed;
|
| 700 |
+
top: 0;
|
| 701 |
+
left: 0;
|
| 702 |
+
width: 100%;
|
| 703 |
+
height: 100%;
|
| 704 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 705 |
+
display: flex;
|
| 706 |
+
justify-content: center;
|
| 707 |
+
align-items: center;
|
| 708 |
+
z-index: 1000;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
.remark-input-container {
|
| 712 |
+
background-color: white;
|
| 713 |
+
padding: 20px;
|
| 714 |
+
border-radius: var(--radius);
|
| 715 |
+
box-shadow: var(--shadow);
|
| 716 |
+
width: 250px;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.remark-input-container h3 {
|
| 720 |
+
margin: 0 0 15px 0;
|
| 721 |
+
font-size: 16px;
|
| 722 |
+
color: var(--text-color);
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.remark-input-container input {
|
| 726 |
+
width: 100%;
|
| 727 |
+
padding: 8px 10px;
|
| 728 |
+
border: 1px solid var(--border-color);
|
| 729 |
+
border-radius: 4px;
|
| 730 |
+
margin-bottom: 15px;
|
| 731 |
+
font-size: 14px;
|
| 732 |
+
box-sizing: border-box;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.remark-input-container .char-count {
|
| 736 |
+
font-size: 12px;
|
| 737 |
+
color: var(--text-secondary);
|
| 738 |
+
margin-bottom: 15px;
|
| 739 |
+
text-align: right;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
.remark-input-actions {
|
| 743 |
+
display: flex;
|
| 744 |
+
justify-content: flex-end;
|
| 745 |
+
gap: 10px;
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
/* Token模糊化样式 */
|
| 749 |
+
.token-blur {
|
| 750 |
+
filter: blur(3px);
|
| 751 |
+
transition: all 0.3s ease;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.token-blur:hover {
|
| 755 |
+
filter: blur(0);
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
/* 删除背景颜色变化样式 */
|
| 759 |
+
#toggle-token-visibility.active i {
|
| 760 |
+
color: var(--primary-color);
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
/* 冷却状态图标样式 */
|
| 764 |
+
.cool-status {
|
| 765 |
+
color: #1e88e5;
|
| 766 |
+
font-size: 18px;
|
| 767 |
+
margin-left: 5px;
|
| 768 |
+
vertical-align: middle;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.cool-status-tooltip {
|
| 772 |
+
position: relative;
|
| 773 |
+
display: inline-block;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
.cool-status-tooltip .tooltip-text {
|
| 777 |
+
visibility: hidden;
|
| 778 |
+
width: 200px;
|
| 779 |
+
background-color: #333;
|
| 780 |
+
color: #fff;
|
| 781 |
+
text-align: center;
|
| 782 |
+
border-radius: 6px;
|
| 783 |
+
padding: 5px;
|
| 784 |
+
position: absolute;
|
| 785 |
+
z-index: 1;
|
| 786 |
+
bottom: 125%;
|
| 787 |
+
left: 50%;
|
| 788 |
+
margin-left: -100px;
|
| 789 |
+
opacity: 0;
|
| 790 |
+
transition: opacity 0.3s;
|
| 791 |
+
font-size: 12px;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
.cool-status-tooltip:hover .tooltip-text {
|
| 795 |
+
visibility: visible;
|
| 796 |
+
opacity: 1;
|
| 797 |
+
}
|
| 798 |
+
</style>
|
| 799 |
+
<!-- 添加图标 -->
|
| 800 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
| 801 |
+
</head>
|
| 802 |
+
<body>
|
| 803 |
+
<div class="container">
|
| 804 |
+
<header>
|
| 805 |
+
<div class="header-content">
|
| 806 |
+
<h1>Augment面板|v1.0.2</h1>
|
| 807 |
+
<button id="logout-btn" class="logout-btn">
|
| 808 |
+
<i class="bi bi-box-arrow-right"></i> 登出
|
| 809 |
+
</button>
|
| 810 |
+
</div>
|
| 811 |
+
</header>
|
| 812 |
+
|
| 813 |
+
<div class="dashboard">
|
| 814 |
+
<!-- 左侧导航栏 -->
|
| 815 |
+
<div class="sidebar" id="sidebar">
|
| 816 |
+
<div class="sidebar-header">
|
| 817 |
+
<h3>面板功能导航</h3>
|
| 818 |
+
<button id="toggle-sidebar" class="toggle-btn">
|
| 819 |
+
<i class="bi bi-chevron-left"></i>
|
| 820 |
+
</button>
|
| 821 |
+
</div>
|
| 822 |
+
<div class="sidebar-menu">
|
| 823 |
+
<div class="menu-item active" data-target="token-list-panel">
|
| 824 |
+
<i class="bi bi-list-ul"></i>
|
| 825 |
+
<span class="menu-text">Token列表</span>
|
| 826 |
+
</div>
|
| 827 |
+
<div class="menu-item" data-target="token-add-panel">
|
| 828 |
+
<i class="bi bi-plus-circle"></i>
|
| 829 |
+
<span class="menu-text">添加Token</span>
|
| 830 |
+
</div>
|
| 831 |
+
</div>
|
| 832 |
+
</div>
|
| 833 |
+
|
| 834 |
+
<!-- 右侧主内容区 -->
|
| 835 |
+
<div class="main-content" id="main-content">
|
| 836 |
+
<!-- Token列表面板 -->
|
| 837 |
+
<div class="content-panel active" id="token-list-panel">
|
| 838 |
+
<div class="panel-title">
|
| 839 |
+
<i class="bi bi-key-fill"></i>
|
| 840 |
+
<h2>Token列表</h2>
|
| 841 |
+
<div class="panel-actions">
|
| 842 |
+
<button id="toggle-token-visibility" class="secondary">
|
| 843 |
+
<i class="bi bi-eye-slash"></i> 隐藏Token
|
| 844 |
+
</button>
|
| 845 |
+
<button id="refresh-token" class="refresh-btn">
|
| 846 |
+
<i class="bi bi-arrow-clockwise"></i> 刷新列表
|
| 847 |
+
</button>
|
| 848 |
+
<button id="check-all-tokens"><i class="bi bi-shield-check btn-icon"></i> <span class="btn-text">批量检测</span></button>
|
| 849 |
+
</div>
|
| 850 |
+
</div>
|
| 851 |
+
|
| 852 |
+
<!-- 添加可滚动容器 -->
|
| 853 |
+
<div class="token-list-container">
|
| 854 |
+
<div id="token-list">加载中...</div>
|
| 855 |
+
</div>
|
| 856 |
+
|
| 857 |
+
<!-- 分页控件 -->
|
| 858 |
+
<div class="pagination-container" id="pagination-container">
|
| 859 |
+
<button class="pagination-btn" id="prev-page" disabled><i class="bi bi-chevron-left"></i></button>
|
| 860 |
+
<span id="page-info">第 <span id="current-page">1</span> 页,共 <span id="total-pages">1</span> 页</span>
|
| 861 |
+
<button class="pagination-btn" id="next-page"><i class="bi bi-chevron-right"></i></button>
|
| 862 |
+
<select id="page-size" class="page-size-select">
|
| 863 |
+
<option value="10">10条/页</option>
|
| 864 |
+
<option value="20">20条/页</option>
|
| 865 |
+
<option value="50">50条/页</option>
|
| 866 |
+
</select>
|
| 867 |
+
</div>
|
| 868 |
+
</div>
|
| 869 |
+
|
| 870 |
+
<!-- 添加Token面板 -->
|
| 871 |
+
<div class="content-panel" id="token-add-panel">
|
| 872 |
+
<div class="panel-title">
|
| 873 |
+
<i class="bi bi-shield-lock-fill"></i>
|
| 874 |
+
<h2>授权获取Token</h2>
|
| 875 |
+
</div>
|
| 876 |
+
|
| 877 |
+
<div class="auth-steps">
|
| 878 |
+
<div class="step">
|
| 879 |
+
<h3><span class="step-number">1</span> 获取授权地址</h3>
|
| 880 |
+
<p>点击下方按钮获取授权���址,然后在浏览器中打开该地址进行授权。</p>
|
| 881 |
+
<button id="get-auth-url"><i class="bi bi-link-45deg" class="btn-icon"></i> <span class="btn-text">获取授权地址</span></button>
|
| 882 |
+
<div id="auth-url" class="token-display" style="display: none;"></div>
|
| 883 |
+
</div>
|
| 884 |
+
|
| 885 |
+
<div class="step">
|
| 886 |
+
<h3><span class="step-number">2</span> 提交授权响应</h3>
|
| 887 |
+
<p>完成授权后,将获得的授权响应粘贴到下面的文本框中:</p>
|
| 888 |
+
<textarea id="auth-response" placeholder='{"code":"_000baec407c57c4bf9xxxxxxxxxxxxxx","state":"0uXxxxxxxxx","tenant_url":"https://dxx.api.augmentcode.com/"}'></textarea>
|
| 889 |
+
<div id="validation-message" class="error"></div>
|
| 890 |
+
<button id="submit-auth"><i class="bi bi-check2-circle" class="btn-text"></i> <span class="btn-text">获取Token</span></button>
|
| 891 |
+
<div id="submit-result" class="success"></div>
|
| 892 |
+
</div>
|
| 893 |
+
</div>
|
| 894 |
+
</div>
|
| 895 |
+
</div>
|
| 896 |
+
</div>
|
| 897 |
+
|
| 898 |
+
<!-- 添加页脚 -->
|
| 899 |
+
<footer>
|
| 900 |
+
<a href="https://linux.do/u/bifang/summary" target="_blank">开发者:彼方</a> | <a href="https://2api-docs.pages.dev/page/augment2api/func-intro" target="_blank">文档中心</a>
|
| 901 |
+
</footer>
|
| 902 |
+
</div>
|
| 903 |
+
|
| 904 |
+
<script>
|
| 905 |
+
// 检查会话是否有效
|
| 906 |
+
function checkSession() {
|
| 907 |
+
// 从Cookie中获取token
|
| 908 |
+
const cookies = document.cookie.split(';');
|
| 909 |
+
let token = null;
|
| 910 |
+
|
| 911 |
+
for (let i = 0; i < cookies.length; i++) {
|
| 912 |
+
const cookie = cookies[i].trim();
|
| 913 |
+
if (cookie.startsWith('auth_token=')) {
|
| 914 |
+
token = cookie.substring('auth_token='.length);
|
| 915 |
+
break;
|
| 916 |
+
}
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
if (!token) {
|
| 920 |
+
window.location.href = '/login';
|
| 921 |
+
return false;
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
// 将token添加到所有API请求中
|
| 925 |
+
return token;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
// 页面加载时检查会话
|
| 929 |
+
const authToken = checkSession();
|
| 930 |
+
if (!authToken) {
|
| 931 |
+
// 如果没有有效会话,不继续执行后续代码
|
| 932 |
+
throw new Error('No valid session');
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
// 为所有fetch请求添加认证头
|
| 936 |
+
const originalFetch = window.fetch;
|
| 937 |
+
window.fetch = function(url, options = {}) {
|
| 938 |
+
// 创建新的options对象,避免修改原始对象
|
| 939 |
+
const newOptions = { ...options };
|
| 940 |
+
|
| 941 |
+
// 确保headers对象存在
|
| 942 |
+
newOptions.headers = newOptions.headers || {};
|
| 943 |
+
|
| 944 |
+
// 如果是对象形式,转换为Headers对象
|
| 945 |
+
if (!(newOptions.headers instanceof Headers)) {
|
| 946 |
+
const headers = new Headers(newOptions.headers);
|
| 947 |
+
headers.append('X-Auth-Token', authToken);
|
| 948 |
+
newOptions.headers = headers;
|
| 949 |
+
} else {
|
| 950 |
+
newOptions.headers.append('X-Auth-Token', authToken);
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
return originalFetch(url, newOptions);
|
| 954 |
+
};
|
| 955 |
+
|
| 956 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 957 |
+
// 侧边栏切换
|
| 958 |
+
const sidebar = document.getElementById('sidebar');
|
| 959 |
+
const toggleBtn = document.getElementById('toggle-sidebar');
|
| 960 |
+
const menuItems = document.querySelectorAll('.menu-item');
|
| 961 |
+
const contentPanels = document.querySelectorAll('.content-panel');
|
| 962 |
+
|
| 963 |
+
// 分页变量
|
| 964 |
+
let currentPage = 1;
|
| 965 |
+
let pageSize = 10;
|
| 966 |
+
let allTokens = [];
|
| 967 |
+
|
| 968 |
+
// 侧边栏折叠/展开
|
| 969 |
+
toggleBtn.addEventListener('click', function() {
|
| 970 |
+
sidebar.classList.toggle('collapsed');
|
| 971 |
+
});
|
| 972 |
+
|
| 973 |
+
// 菜单项切换
|
| 974 |
+
menuItems.forEach(item => {
|
| 975 |
+
item.addEventListener('click', function() {
|
| 976 |
+
// 移除所有菜单项的active类
|
| 977 |
+
menuItems.forEach(i => i.classList.remove('active'));
|
| 978 |
+
// 为当前点击的菜单项添加active类
|
| 979 |
+
this.classList.add('active');
|
| 980 |
+
|
| 981 |
+
// 获取目标面板ID
|
| 982 |
+
const targetId = this.getAttribute('data-target');
|
| 983 |
+
|
| 984 |
+
// 隐藏所有内容面板
|
| 985 |
+
contentPanels.forEach(panel => {
|
| 986 |
+
panel.classList.remove('active');
|
| 987 |
+
});
|
| 988 |
+
|
| 989 |
+
// 显示目标面板
|
| 990 |
+
document.getElementById(targetId).classList.add('active');
|
| 991 |
+
});
|
| 992 |
+
});
|
| 993 |
+
|
| 994 |
+
// 分页功能
|
| 995 |
+
const prevPageBtn = document.getElementById('prev-page');
|
| 996 |
+
const nextPageBtn = document.getElementById('next-page');
|
| 997 |
+
const currentPageSpan = document.getElementById('current-page');
|
| 998 |
+
const totalPagesSpan = document.getElementById('total-pages');
|
| 999 |
+
const pageSizeSelect = document.getElementById('page-size');
|
| 1000 |
+
|
| 1001 |
+
// 页面大小变化
|
| 1002 |
+
pageSizeSelect.addEventListener('change', function() {
|
| 1003 |
+
pageSize = parseInt(this.value);
|
| 1004 |
+
currentPage = 1;
|
| 1005 |
+
fetchCurrentToken();
|
| 1006 |
+
});
|
| 1007 |
+
|
| 1008 |
+
// 上一页
|
| 1009 |
+
prevPageBtn.addEventListener('click', function() {
|
| 1010 |
+
if (currentPage > 1) {
|
| 1011 |
+
currentPage--;
|
| 1012 |
+
fetchCurrentToken();
|
| 1013 |
+
}
|
| 1014 |
+
});
|
| 1015 |
+
|
| 1016 |
+
// 下一页
|
| 1017 |
+
nextPageBtn.addEventListener('click', function() {
|
| 1018 |
+
currentPage++;
|
| 1019 |
+
fetchCurrentToken();
|
| 1020 |
+
});
|
| 1021 |
+
|
| 1022 |
+
// 修改获取当前Token列表函数,使用后端分页
|
| 1023 |
+
function fetchCurrentToken() {
|
| 1024 |
+
// 显示加载动画
|
| 1025 |
+
const refreshBtn = document.getElementById('refresh-token');
|
| 1026 |
+
const tokenListElement = document.getElementById('token-list');
|
| 1027 |
+
refreshBtn.classList.add('loading');
|
| 1028 |
+
|
| 1029 |
+
// 设置超时处理
|
| 1030 |
+
const timeoutId = setTimeout(() => {
|
| 1031 |
+
refreshBtn.classList.remove('loading');
|
| 1032 |
+
tokenListElement.innerHTML = '<div class="error" style="display:block;">请求超时,请重试</div>';
|
| 1033 |
+
}, 10000); // 10秒超时
|
| 1034 |
+
|
| 1035 |
+
fetch(`/api/tokens?page=${currentPage}&page_size=${pageSize}`)
|
| 1036 |
+
.then(response => {
|
| 1037 |
+
if (!response.ok) {
|
| 1038 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 1039 |
+
}
|
| 1040 |
+
return response.json();
|
| 1041 |
+
})
|
| 1042 |
+
.then(data => {
|
| 1043 |
+
clearTimeout(timeoutId); // 清除超时定时器
|
| 1044 |
+
|
| 1045 |
+
if (data.status === 'success') {
|
| 1046 |
+
// 使用后端返回的token列表
|
| 1047 |
+
allTokens = data.tokens || [];
|
| 1048 |
+
|
| 1049 |
+
// 更新分页信息
|
| 1050 |
+
const totalItems = data.total || 0;
|
| 1051 |
+
const totalPages = data.total_pages || 1;
|
| 1052 |
+
currentPage = data.page || 1;
|
| 1053 |
+
|
| 1054 |
+
// 渲染token列表和分页控件
|
| 1055 |
+
renderTokenList(totalItems, totalPages);
|
| 1056 |
+
} else {
|
| 1057 |
+
tokenListElement.innerHTML =
|
| 1058 |
+
'<div class="error" style="display:block;">获取Token列表失败: ' + (data.error || '未知错误') + '</div>';
|
| 1059 |
+
}
|
| 1060 |
+
})
|
| 1061 |
+
.catch(error => {
|
| 1062 |
+
clearTimeout(timeoutId); // 清除超时定时器
|
| 1063 |
+
tokenListElement.innerHTML =
|
| 1064 |
+
'<div class="error" style="display:block;">请求失败: ' + error.message + '</div>';
|
| 1065 |
+
})
|
| 1066 |
+
.finally(() => {
|
| 1067 |
+
refreshBtn.classList.remove('loading');
|
| 1068 |
+
});
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
// 修改渲染token列表函数,使用后端返回的分页信息
|
| 1072 |
+
function renderTokenList(totalItems, totalPages) {
|
| 1073 |
+
const tokenListElement = document.getElementById('token-list');
|
| 1074 |
+
|
| 1075 |
+
// 如果没有token
|
| 1076 |
+
if (totalItems === 0) {
|
| 1077 |
+
tokenListElement.innerHTML = '<div class="no-tokens">暂无可用Token,请点击"添加Token"获取</div>';
|
| 1078 |
+
|
| 1079 |
+
// 隐藏分页控件
|
| 1080 |
+
document.getElementById('pagination-container').style.display = 'none';
|
| 1081 |
+
return;
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
// 显示分页控件
|
| 1085 |
+
document.getElementById('pagination-container').style.display = 'flex';
|
| 1086 |
+
|
| 1087 |
+
// 更新分页信息显示
|
| 1088 |
+
totalPagesSpan.textContent = totalPages;
|
| 1089 |
+
currentPageSpan.textContent = currentPage;
|
| 1090 |
+
|
| 1091 |
+
// 更新分页按钮状态
|
| 1092 |
+
prevPageBtn.disabled = currentPage === 1;
|
| 1093 |
+
nextPageBtn.disabled = currentPage === totalPages;
|
| 1094 |
+
|
| 1095 |
+
// 创建token列表容器
|
| 1096 |
+
tokenListElement.innerHTML = '<div class="token-list"></div>';
|
| 1097 |
+
const listContainer = tokenListElement.querySelector('.token-list');
|
| 1098 |
+
|
| 1099 |
+
// 添加每个token项
|
| 1100 |
+
allTokens.forEach((tokenInfo, index) => {
|
| 1101 |
+
// 计算在当前页中���索引
|
| 1102 |
+
const displayIndex = index + 1 + (currentPage - 1) * pageSize;
|
| 1103 |
+
|
| 1104 |
+
// 获取使用次数并设置样式类
|
| 1105 |
+
const usageCount = tokenInfo.usage_count || 0;
|
| 1106 |
+
const chatUsageCount = tokenInfo.chat_usage_count || 0;
|
| 1107 |
+
const agentUsageCount = tokenInfo.agent_usage_count || 0;
|
| 1108 |
+
let usageClass = '';
|
| 1109 |
+
|
| 1110 |
+
// 根据CHAT和AGENT模式的使用次数来确定样式类
|
| 1111 |
+
if (chatUsageCount < 1000 && agentUsageCount < 20) {
|
| 1112 |
+
usageClass = 'low';
|
| 1113 |
+
} else if (chatUsageCount < 2000 && agentUsageCount < 40) {
|
| 1114 |
+
usageClass = 'medium';
|
| 1115 |
+
} else {
|
| 1116 |
+
usageClass = 'high';
|
| 1117 |
+
}
|
| 1118 |
+
|
| 1119 |
+
// 显示完整token,不再截断
|
| 1120 |
+
const tokenItem = document.createElement('div');
|
| 1121 |
+
tokenItem.className = 'token-item';
|
| 1122 |
+
tokenItem.innerHTML = `
|
| 1123 |
+
<div class="token-header">
|
| 1124 |
+
<div class="token-number">${displayIndex}</div>
|
| 1125 |
+
<div class="token-summary">
|
| 1126 |
+
${tokenInfo.token}
|
| 1127 |
+
<span class="token-remark${!tokenInfo.remark ? ' empty' : ''}" data-token="${tokenInfo.token}" data-remark="${tokenInfo.remark || ''}">${tokenInfo.remark || '添加备注'}</span>
|
| 1128 |
+
${tokenInfo.in_cool ? `
|
| 1129 |
+
<span class="cool-status-tooltip">
|
| 1130 |
+
<i class="bi bi-snow cool-status"></i>
|
| 1131 |
+
<span class="tooltip-text">冷却中,直到: ${new Date(tokenInfo.cool_end).toLocaleString()}</span>
|
| 1132 |
+
</span>` : ''}
|
| 1133 |
+
</div>
|
| 1134 |
+
<div class="token-usage-count">
|
| 1135 |
+
CHAT使用: <span class="${usageClass}">${chatUsageCount}</span> 次 | AGENT使用: <span class="${usageClass}">${agentUsageCount}</span> 次
|
| 1136 |
+
</div>
|
| 1137 |
+
<div class="token-toggle"><i class="bi bi-chevron-down"></i></div>
|
| 1138 |
+
</div>
|
| 1139 |
+
<div class="token-details">
|
| 1140 |
+
<div class="token-label">Token:</div>
|
| 1141 |
+
<div class="token-display">${tokenInfo.token}</div>
|
| 1142 |
+
<div class="token-label">租户URL:</div>
|
| 1143 |
+
<div class="token-display">${tokenInfo.tenant_url}</div>
|
| 1144 |
+
<div class="token-actions">
|
| 1145 |
+
<button class="delete-token" data-token="${tokenInfo.token}">
|
| 1146 |
+
<i class="bi bi-trash"></i> 删除
|
| 1147 |
+
</button>
|
| 1148 |
+
</div>
|
| 1149 |
+
</div>
|
| 1150 |
+
`;
|
| 1151 |
+
|
| 1152 |
+
listContainer.appendChild(tokenItem);
|
| 1153 |
+
|
| 1154 |
+
// 添加事件处理
|
| 1155 |
+
const header = tokenItem.querySelector('.token-header');
|
| 1156 |
+
const details = tokenItem.querySelector('.token-details');
|
| 1157 |
+
const toggle = tokenItem.querySelector('.token-toggle');
|
| 1158 |
+
const remarkSpan = tokenItem.querySelector('.token-remark');
|
| 1159 |
+
|
| 1160 |
+
// 为备注添加点击事件
|
| 1161 |
+
remarkSpan.addEventListener('click', function(e) {
|
| 1162 |
+
e.stopPropagation(); // 阻止事件冒泡到header
|
| 1163 |
+
|
| 1164 |
+
const token = this.dataset.token;
|
| 1165 |
+
const currentRemark = this.dataset.remark;
|
| 1166 |
+
|
| 1167 |
+
// 创建弹出层
|
| 1168 |
+
const modal = document.createElement('div');
|
| 1169 |
+
modal.className = 'remark-input-modal';
|
| 1170 |
+
|
| 1171 |
+
modal.innerHTML = `
|
| 1172 |
+
<div class="remark-input-container">
|
| 1173 |
+
<h3>编辑备注</h3>
|
| 1174 |
+
<input type="text" maxlength="30" placeholder="请输入备注(30字以内)" value="${currentRemark}">
|
| 1175 |
+
<div class="char-count"><span>${currentRemark.length}</span>/30</div>
|
| 1176 |
+
<div class="remark-input-actions">
|
| 1177 |
+
<button class="secondary" onclick="this.closest('.remark-input-modal').remove()">取消</button>
|
| 1178 |
+
<button class="save-remark" data-token="${token}">保存</button>
|
| 1179 |
+
</div>
|
| 1180 |
+
</div>
|
| 1181 |
+
`;
|
| 1182 |
+
|
| 1183 |
+
document.body.appendChild(modal);
|
| 1184 |
+
|
| 1185 |
+
// 获取输入框并聚焦
|
| 1186 |
+
const input = modal.querySelector('input');
|
| 1187 |
+
input.focus();
|
| 1188 |
+
|
| 1189 |
+
// 更新字符计数
|
| 1190 |
+
input.addEventListener('input', () => {
|
| 1191 |
+
const count = input.value.length;
|
| 1192 |
+
modal.querySelector('.char-count span').textContent = count;
|
| 1193 |
+
});
|
| 1194 |
+
|
| 1195 |
+
// 点击背景关闭弹窗
|
| 1196 |
+
modal.addEventListener('click', (e) => {
|
| 1197 |
+
if (e.target === modal) {
|
| 1198 |
+
modal.remove();
|
| 1199 |
+
}
|
| 1200 |
+
});
|
| 1201 |
+
|
| 1202 |
+
// 处理保存按钮点击
|
| 1203 |
+
modal.querySelector('.save-remark').addEventListener('click', async function() {
|
| 1204 |
+
const token = this.dataset.token;
|
| 1205 |
+
const newRemark = input.value.trim();
|
| 1206 |
+
|
| 1207 |
+
try {
|
| 1208 |
+
const response = await fetch(`/api/token/${token}/remark`, {
|
| 1209 |
+
method: 'PUT',
|
| 1210 |
+
headers: {
|
| 1211 |
+
'Content-Type': 'application/json'
|
| 1212 |
+
},
|
| 1213 |
+
body: JSON.stringify({ remark: newRemark })
|
| 1214 |
+
});
|
| 1215 |
+
|
| 1216 |
+
const data = await response.json();
|
| 1217 |
+
if (data.status === 'success') {
|
| 1218 |
+
// 关闭弹窗
|
| 1219 |
+
modal.remove();
|
| 1220 |
+
// 刷新列表
|
| 1221 |
+
fetchCurrentToken();
|
| 1222 |
+
} else {
|
| 1223 |
+
alert('更新备注失败: ' + (data.error || '未知错误'));
|
| 1224 |
+
}
|
| 1225 |
+
} catch (error) {
|
| 1226 |
+
alert('请求失败: ' + error.message);
|
| 1227 |
+
}
|
| 1228 |
+
});
|
| 1229 |
+
});
|
| 1230 |
+
|
| 1231 |
+
// 为header添加点击事件(展开/折叠详情)
|
| 1232 |
+
header.addEventListener('click', function() {
|
| 1233 |
+
details.classList.toggle('open');
|
| 1234 |
+
toggle.classList.toggle('open');
|
| 1235 |
+
});
|
| 1236 |
+
});
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
// 为token列表添加事件委托,只处理删除按钮
|
| 1240 |
+
document.getElementById('token-list').addEventListener('click', function(e) {
|
| 1241 |
+
// 检查点击的是否是删除按钮
|
| 1242 |
+
if (e.target.closest('.delete-token')) {
|
| 1243 |
+
const deleteBtn = e.target.closest('.delete-token');
|
| 1244 |
+
const token = deleteBtn.getAttribute('data-token');
|
| 1245 |
+
|
| 1246 |
+
if (confirm('确定要删除此Token吗?')) {
|
| 1247 |
+
// 发送删除请求
|
| 1248 |
+
fetch(`/api/token/${encodeURIComponent(token)}`, {
|
| 1249 |
+
method: 'DELETE'
|
| 1250 |
+
})
|
| 1251 |
+
.then(response => response.json())
|
| 1252 |
+
.then(data => {
|
| 1253 |
+
if (data.status === 'success') {
|
| 1254 |
+
// 刷新token列表
|
| 1255 |
+
fetchCurrentToken();
|
| 1256 |
+
} else {
|
| 1257 |
+
alert('删除失败: ' + (data.error || '未知错误'));
|
| 1258 |
+
}
|
| 1259 |
+
})
|
| 1260 |
+
.catch(error => {
|
| 1261 |
+
alert('请求失败: ' + error.message);
|
| 1262 |
+
});
|
| 1263 |
+
}
|
| 1264 |
+
}
|
| 1265 |
+
});
|
| 1266 |
+
|
| 1267 |
+
// 刷新Token按钮事件
|
| 1268 |
+
document.getElementById('refresh-token').addEventListener('click', function() {
|
| 1269 |
+
fetchCurrentToken();
|
| 1270 |
+
});
|
| 1271 |
+
|
| 1272 |
+
// 添加登出处理逻辑
|
| 1273 |
+
document.getElementById('logout-btn').addEventListener('click', function() {
|
| 1274 |
+
if(confirm('确定要登出吗?')) {
|
| 1275 |
+
fetch('/api/logout', {
|
| 1276 |
+
method: 'POST',
|
| 1277 |
+
headers: {
|
| 1278 |
+
'X-Auth-Token': authToken
|
| 1279 |
+
}
|
| 1280 |
+
})
|
| 1281 |
+
.then(response => response.json())
|
| 1282 |
+
.then(data => {
|
| 1283 |
+
if(data.status === 'success') {
|
| 1284 |
+
// 清除Cookie
|
| 1285 |
+
document.cookie = "auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
| 1286 |
+
// 重定向到登录页
|
| 1287 |
+
window.location.href = '/login';
|
| 1288 |
+
} else {
|
| 1289 |
+
alert('登出失败: ' + (data.error || '未知错误'));
|
| 1290 |
+
}
|
| 1291 |
+
})
|
| 1292 |
+
.catch(error => {
|
| 1293 |
+
alert('登出请求失败: ' + error.message);
|
| 1294 |
+
});
|
| 1295 |
+
}
|
| 1296 |
+
});
|
| 1297 |
+
|
| 1298 |
+
// 批量检测token
|
| 1299 |
+
document.getElementById('check-all-tokens').addEventListener('click', function() {
|
| 1300 |
+
const button = this;
|
| 1301 |
+
button.classList.add('loading');
|
| 1302 |
+
|
| 1303 |
+
fetch('/api/check-tokens')
|
| 1304 |
+
.then(response => response.json())
|
| 1305 |
+
.then(data => {
|
| 1306 |
+
if(data.status === 'success') {
|
| 1307 |
+
// 创建或获取检测结果显示元素
|
| 1308 |
+
let checkResult = document.querySelector('.check-result');
|
| 1309 |
+
if(!checkResult) {
|
| 1310 |
+
checkResult = document.createElement('div');
|
| 1311 |
+
checkResult.className = 'check-result';
|
| 1312 |
+
document.querySelector('.panel-title').after(checkResult);
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
// 显示检测结果
|
| 1316 |
+
checkResult.textContent = `检测完成! 共检测 ${data.total} 个Token,更新 ${data.updated} 个Token租户地址,禁用 ${data.disabled} 个无效Token`;
|
| 1317 |
+
checkResult.style.display = 'block';
|
| 1318 |
+
|
| 1319 |
+
// 如果有更新或禁用,则刷新token列表
|
| 1320 |
+
if(data.updated > 0 || data.disabled > 0) {
|
| 1321 |
+
fetchCurrentToken();
|
| 1322 |
+
}
|
| 1323 |
+
|
| 1324 |
+
// 5秒后隐藏提示
|
| 1325 |
+
setTimeout(() => {
|
| 1326 |
+
checkResult.style.display = 'none';
|
| 1327 |
+
}, 5000);
|
| 1328 |
+
} else {
|
| 1329 |
+
alert('检测失败: ' + (data.error || '未知错误'));
|
| 1330 |
+
}
|
| 1331 |
+
})
|
| 1332 |
+
.catch(error => {
|
| 1333 |
+
alert('请求失败: ' + error.message);
|
| 1334 |
+
})
|
| 1335 |
+
.finally(() => {
|
| 1336 |
+
button.classList.remove('loading');
|
| 1337 |
+
});
|
| 1338 |
+
});
|
| 1339 |
+
|
| 1340 |
+
// 获取授权地址按钮事件
|
| 1341 |
+
document.getElementById('get-auth-url').addEventListener('click', function() {
|
| 1342 |
+
const button = this;
|
| 1343 |
+
const btnText = button.querySelector('.btn-text');
|
| 1344 |
+
const originalText = btnText.textContent;
|
| 1345 |
+
|
| 1346 |
+
// 显示加载状态
|
| 1347 |
+
button.disabled = true;
|
| 1348 |
+
btnText.textContent = '获取中...';
|
| 1349 |
+
|
| 1350 |
+
// 请求授权地址
|
| 1351 |
+
fetch('/auth')
|
| 1352 |
+
.then(response => response.json())
|
| 1353 |
+
.then(data => {
|
| 1354 |
+
if (data.authorize_url) {
|
| 1355 |
+
// 显示授权地址
|
| 1356 |
+
const authUrlElement = document.getElementById('auth-url');
|
| 1357 |
+
authUrlElement.textContent = data.authorize_url;
|
| 1358 |
+
authUrlElement.style.display = 'block';
|
| 1359 |
+
} else {
|
| 1360 |
+
alert('获取授权地址失败: ' + (data.error || '未知错误'));
|
| 1361 |
+
}
|
| 1362 |
+
})
|
| 1363 |
+
.catch(error => {
|
| 1364 |
+
alert('请求失败: ' + error.message);
|
| 1365 |
+
})
|
| 1366 |
+
.finally(() => {
|
| 1367 |
+
// 恢复按钮状态
|
| 1368 |
+
button.disabled = false;
|
| 1369 |
+
btnText.textContent = originalText;
|
| 1370 |
+
});
|
| 1371 |
+
});
|
| 1372 |
+
|
| 1373 |
+
// 验证JSON格式
|
| 1374 |
+
function isValidJSON(str) {
|
| 1375 |
+
try {
|
| 1376 |
+
JSON.parse(str);
|
| 1377 |
+
return true;
|
| 1378 |
+
} catch (e) {
|
| 1379 |
+
return false;
|
| 1380 |
+
}
|
| 1381 |
+
}
|
| 1382 |
+
|
| 1383 |
+
// 验证授权响应格式
|
| 1384 |
+
function validateAuthResponse(response) {
|
| 1385 |
+
try {
|
| 1386 |
+
const data = JSON.parse(response);
|
| 1387 |
+
// 检查必要字段
|
| 1388 |
+
if (!data.code || !data.state || !data.tenant_url) {
|
| 1389 |
+
return { valid: false, message: '缺少必要字段 (code, state, tenant_url)' };
|
| 1390 |
+
}
|
| 1391 |
+
return { valid: true, data: data };
|
| 1392 |
+
} catch (e) {
|
| 1393 |
+
return { valid: false, message: 'JSON格式无效: ' + e.message };
|
| 1394 |
+
}
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
+
// 提��授权响应按钮事件
|
| 1398 |
+
document.getElementById('submit-auth').addEventListener('click', function() {
|
| 1399 |
+
const button = this;
|
| 1400 |
+
const btnText = button.querySelector('.btn-text');
|
| 1401 |
+
const originalText = btnText.textContent;
|
| 1402 |
+
const responseText = document.getElementById('auth-response').value.trim();
|
| 1403 |
+
const validationMessage = document.getElementById('validation-message');
|
| 1404 |
+
const submitResult = document.getElementById('submit-result');
|
| 1405 |
+
|
| 1406 |
+
// 重置消息
|
| 1407 |
+
validationMessage.style.display = 'none';
|
| 1408 |
+
submitResult.style.display = 'none';
|
| 1409 |
+
|
| 1410 |
+
// 验证输入
|
| 1411 |
+
if (!responseText) {
|
| 1412 |
+
validationMessage.textContent = '请输入授权响应';
|
| 1413 |
+
validationMessage.style.display = 'block';
|
| 1414 |
+
return;
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
// 验证JSON格式
|
| 1418 |
+
if (!isValidJSON(responseText)) {
|
| 1419 |
+
validationMessage.textContent = 'JSON格式无效,请检查输入';
|
| 1420 |
+
validationMessage.style.display = 'block';
|
| 1421 |
+
return;
|
| 1422 |
+
}
|
| 1423 |
+
|
| 1424 |
+
// 验证授权响应格式
|
| 1425 |
+
const validation = validateAuthResponse(responseText);
|
| 1426 |
+
if (!validation.valid) {
|
| 1427 |
+
validationMessage.textContent = validation.message;
|
| 1428 |
+
validationMessage.style.display = 'block';
|
| 1429 |
+
return;
|
| 1430 |
+
}
|
| 1431 |
+
|
| 1432 |
+
// 显示加载状态
|
| 1433 |
+
button.disabled = true;
|
| 1434 |
+
btnText.textContent = '处理中...';
|
| 1435 |
+
|
| 1436 |
+
// 提交授权响应
|
| 1437 |
+
fetch('/callback', {
|
| 1438 |
+
method: 'POST',
|
| 1439 |
+
headers: {
|
| 1440 |
+
'Content-Type': 'application/json'
|
| 1441 |
+
},
|
| 1442 |
+
body: responseText
|
| 1443 |
+
})
|
| 1444 |
+
.then(response => response.json())
|
| 1445 |
+
.then(data => {
|
| 1446 |
+
if (data.status === 'success') {
|
| 1447 |
+
// 显示成功消息
|
| 1448 |
+
submitResult.textContent = 'Token获取成功!';
|
| 1449 |
+
submitResult.style.display = 'block';
|
| 1450 |
+
|
| 1451 |
+
// 清空输入框
|
| 1452 |
+
document.getElementById('auth-response').value = '';
|
| 1453 |
+
|
| 1454 |
+
// 刷新Token列表
|
| 1455 |
+
setTimeout(() => {
|
| 1456 |
+
fetchCurrentToken();
|
| 1457 |
+
|
| 1458 |
+
// 切换到Token列表面板
|
| 1459 |
+
document.querySelector('.menu-item[data-target="token-list-panel"]').click();
|
| 1460 |
+
}, 1000);
|
| 1461 |
+
} else {
|
| 1462 |
+
// 显示错误消息但不影响样式
|
| 1463 |
+
validationMessage.textContent = '获取Token失败: ' + (data.error || '未知错误');
|
| 1464 |
+
validationMessage.style.display = 'block';
|
| 1465 |
+
}
|
| 1466 |
+
})
|
| 1467 |
+
.catch(error => {
|
| 1468 |
+
// 显示错误消息但不影响样式
|
| 1469 |
+
validationMessage.textContent = '请求失败: ' + error.message;
|
| 1470 |
+
validationMessage.style.display = 'block';
|
| 1471 |
+
})
|
| 1472 |
+
.finally(() => {
|
| 1473 |
+
// 恢复按钮状态
|
| 1474 |
+
button.disabled = false;
|
| 1475 |
+
btnText.textContent = originalText;
|
| 1476 |
+
});
|
| 1477 |
+
});
|
| 1478 |
+
|
| 1479 |
+
// 初始加载token列表
|
| 1480 |
+
fetchCurrentToken();
|
| 1481 |
+
|
| 1482 |
+
// 添加Token隐藏/显示功能
|
| 1483 |
+
let tokenVisible = true;
|
| 1484 |
+
const toggleTokenVisibilityBtn = document.getElementById('toggle-token-visibility');
|
| 1485 |
+
|
| 1486 |
+
toggleTokenVisibilityBtn.addEventListener('click', function() {
|
| 1487 |
+
tokenVisible = !tokenVisible;
|
| 1488 |
+
|
| 1489 |
+
// 更新按钮状态和文本
|
| 1490 |
+
if (tokenVisible) {
|
| 1491 |
+
this.innerHTML = '<i class="bi bi-eye-slash"></i> 隐藏Token';
|
| 1492 |
+
this.classList.remove('active');
|
| 1493 |
+
} else {
|
| 1494 |
+
this.innerHTML = '<i class="bi bi-eye"></i> 显示Token';
|
| 1495 |
+
this.classList.add('active');
|
| 1496 |
+
}
|
| 1497 |
+
|
| 1498 |
+
// 应用模糊效果到所有Token展示区域
|
| 1499 |
+
applyTokenVisibility();
|
| 1500 |
+
});
|
| 1501 |
+
|
| 1502 |
+
// 应用Token可见性设置到列表中
|
| 1503 |
+
function applyTokenVisibility() {
|
| 1504 |
+
// 获取所有Token显示元素
|
| 1505 |
+
const tokenElements = document.querySelectorAll('.token-summary, .token-display');
|
| 1506 |
+
|
| 1507 |
+
tokenElements.forEach(element => {
|
| 1508 |
+
if (tokenVisible) {
|
| 1509 |
+
element.classList.remove('token-blur');
|
| 1510 |
+
} else {
|
| 1511 |
+
element.classList.add('token-blur');
|
| 1512 |
+
}
|
| 1513 |
+
});
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
// 在渲染Token列表后应用可见性设置
|
| 1517 |
+
const originalRenderTokenList = renderTokenList;
|
| 1518 |
+
renderTokenList = function(totalItems, totalPages) {
|
| 1519 |
+
originalRenderTokenList(totalItems, totalPages);
|
| 1520 |
+
|
| 1521 |
+
// 如果当前状态是隐藏的,则应用模糊效果
|
| 1522 |
+
if (!tokenVisible) {
|
| 1523 |
+
applyTokenVisibility();
|
| 1524 |
+
}
|
| 1525 |
+
};
|
| 1526 |
+
});
|
| 1527 |
+
</script>
|
| 1528 |
+
</body>
|
| 1529 |
+
</html>
|
templates/login.html
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Augment2Api - 登录</title>
|
| 7 |
+
<link rel="icon" href="../static/augment.svg" type="image/svg+xml">
|
| 8 |
+
<link rel="alternate icon" href="../static/augment.svg" type="image/x-icon">
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--primary-color: #4361ee;
|
| 12 |
+
--secondary-color: #3f37c9;
|
| 13 |
+
--success-color: #4caf50;
|
| 14 |
+
--error-color: #f44336;
|
| 15 |
+
--bg-color: #f8f9fa;
|
| 16 |
+
--card-bg: #ffffff;
|
| 17 |
+
--text-color: #333333;
|
| 18 |
+
--border-color: #e0e0e0;
|
| 19 |
+
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 20 |
+
--radius: 12px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
box-sizing: border-box;
|
| 25 |
+
margin: 0;
|
| 26 |
+
padding: 0;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
| 31 |
+
background-color: var(--bg-color);
|
| 32 |
+
color: var(--text-color);
|
| 33 |
+
line-height: 1.6;
|
| 34 |
+
padding: 20px;
|
| 35 |
+
min-height: 100vh;
|
| 36 |
+
display: flex;
|
| 37 |
+
flex-direction: column;
|
| 38 |
+
justify-content: center;
|
| 39 |
+
align-items: center;
|
| 40 |
+
position: relative;
|
| 41 |
+
overflow: hidden;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
#bg-canvas {
|
| 45 |
+
position: fixed;
|
| 46 |
+
top: 0;
|
| 47 |
+
left: 0;
|
| 48 |
+
width: 100%;
|
| 49 |
+
height: 100%;
|
| 50 |
+
z-index: -1;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.login-container {
|
| 54 |
+
width: 100%;
|
| 55 |
+
max-width: 400px;
|
| 56 |
+
background: var(--card-bg);
|
| 57 |
+
border-radius: var(--radius);
|
| 58 |
+
box-shadow: var(--shadow);
|
| 59 |
+
padding: 30px;
|
| 60 |
+
margin-bottom: 20px;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
h1 {
|
| 64 |
+
font-size: 24px;
|
| 65 |
+
font-weight: 600;
|
| 66 |
+
color: black;
|
| 67 |
+
text-align: center;
|
| 68 |
+
margin-bottom: 20px;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.form-group {
|
| 72 |
+
margin-bottom: 20px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
label {
|
| 76 |
+
display: block;
|
| 77 |
+
margin-bottom: 8px;
|
| 78 |
+
font-weight: 500;
|
| 79 |
+
text-align: center;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
input[type="password"] {
|
| 83 |
+
width: 100%;
|
| 84 |
+
padding: 10px 12px;
|
| 85 |
+
border: 1px solid var(--border-color);
|
| 86 |
+
border-radius: 8px;
|
| 87 |
+
font-size: 16px;
|
| 88 |
+
transition: border-color 0.3s;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
input[type="password"]:focus {
|
| 92 |
+
border-color: var(--primary-color);
|
| 93 |
+
outline: none;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
button {
|
| 97 |
+
display: block;
|
| 98 |
+
width: 100%;
|
| 99 |
+
padding: 12px;
|
| 100 |
+
background-color: var(--primary-color);
|
| 101 |
+
color: white;
|
| 102 |
+
border: none;
|
| 103 |
+
border-radius: 8px;
|
| 104 |
+
font-size: 16px;
|
| 105 |
+
font-weight: 500;
|
| 106 |
+
cursor: pointer;
|
| 107 |
+
transition: background-color 0.3s;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
button:hover {
|
| 111 |
+
background-color: var(--secondary-color);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.error-message {
|
| 115 |
+
color: var(--error-color);
|
| 116 |
+
margin-top: 15px;
|
| 117 |
+
text-align: center;
|
| 118 |
+
display: none;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
footer {
|
| 122 |
+
text-align: center;
|
| 123 |
+
margin-top: 20px;
|
| 124 |
+
color: #666;
|
| 125 |
+
font-size: 14px;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
footer a {
|
| 129 |
+
color: var(--primary-color);
|
| 130 |
+
text-decoration: none;
|
| 131 |
+
transition: color 0.3s;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
footer a:hover {
|
| 135 |
+
color: var(--secondary-color);
|
| 136 |
+
text-decoration: underline;
|
| 137 |
+
}
|
| 138 |
+
</style>
|
| 139 |
+
</head>
|
| 140 |
+
<body>
|
| 141 |
+
<canvas id="bg-canvas"></canvas>
|
| 142 |
+
<div class="login-container">
|
| 143 |
+
<h1>Augment面板登录</h1>
|
| 144 |
+
<form id="login-form">
|
| 145 |
+
<div class="form-group">
|
| 146 |
+
<label for="password">访问密码</label>
|
| 147 |
+
<input type="password" id="password" name="password" placeholder="请输入访问密码" required>
|
| 148 |
+
</div>
|
| 149 |
+
<button type="submit">登录</button>
|
| 150 |
+
<div id="error-message" class="error-message">密码错误,请重试</div>
|
| 151 |
+
</form>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<script>
|
| 155 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 156 |
+
const loginForm = document.getElementById('login-form');
|
| 157 |
+
const errorMessage = document.getElementById('error-message');
|
| 158 |
+
|
| 159 |
+
// 检查是否有错误消息参数
|
| 160 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 161 |
+
if (urlParams.get('error') === 'invalid_password') {
|
| 162 |
+
errorMessage.style.display = 'block';
|
| 163 |
+
errorMessage.textContent = '密码错误,请重试';
|
| 164 |
+
} else if (urlParams.get('error') === 'token_expired') {
|
| 165 |
+
errorMessage.style.display = 'block';
|
| 166 |
+
errorMessage.textContent = '会话已过期,请重新登录';
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// 背景动画
|
| 170 |
+
const canvas = document.getElementById('bg-canvas');
|
| 171 |
+
const ctx = canvas.getContext('2d');
|
| 172 |
+
let width = window.innerWidth;
|
| 173 |
+
let height = window.innerHeight;
|
| 174 |
+
|
| 175 |
+
canvas.width = width;
|
| 176 |
+
canvas.height = height;
|
| 177 |
+
|
| 178 |
+
// 线条数量
|
| 179 |
+
const lineCount = 15;
|
| 180 |
+
const lines = [];
|
| 181 |
+
|
| 182 |
+
// 创建线条
|
| 183 |
+
for (let i = 0; i < lineCount; i++) {
|
| 184 |
+
lines.push({
|
| 185 |
+
x: Math.random() * width,
|
| 186 |
+
y: Math.random() * height,
|
| 187 |
+
length: Math.random() * 100 + 50,
|
| 188 |
+
angle: Math.random() * Math.PI * 2,
|
| 189 |
+
speed: Math.random() * 0.5 + 0.1,
|
| 190 |
+
opacity: Math.random() * 0.2 + 0.1
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function drawLines() {
|
| 195 |
+
ctx.clearRect(0, 0, width, height);
|
| 196 |
+
|
| 197 |
+
for (let i = 0; i < lineCount; i++) {
|
| 198 |
+
const line = lines[i];
|
| 199 |
+
|
| 200 |
+
ctx.beginPath();
|
| 201 |
+
ctx.moveTo(line.x, line.y);
|
| 202 |
+
ctx.lineTo(
|
| 203 |
+
line.x + Math.cos(line.angle) * line.length,
|
| 204 |
+
line.y + Math.sin(line.angle) * line.length
|
| 205 |
+
);
|
| 206 |
+
|
| 207 |
+
ctx.strokeStyle = `rgba(67, 97, 238, ${line.opacity})`;
|
| 208 |
+
ctx.lineWidth = 1;
|
| 209 |
+
ctx.stroke();
|
| 210 |
+
|
| 211 |
+
// 更新线条位置
|
| 212 |
+
line.x += Math.cos(line.angle) * line.speed;
|
| 213 |
+
line.y += Math.sin(line.angle) * line.speed;
|
| 214 |
+
|
| 215 |
+
// 如果线条移出画布,重新放置
|
| 216 |
+
if (line.x < -line.length || line.x > width + line.length ||
|
| 217 |
+
line.y < -line.length || line.y > height + line.length) {
|
| 218 |
+
line.x = Math.random() * width;
|
| 219 |
+
line.y = Math.random() * height;
|
| 220 |
+
line.angle = Math.random() * Math.PI * 2;
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
requestAnimationFrame(drawLines);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
drawLines();
|
| 228 |
+
|
| 229 |
+
// 窗口大小变化时重新设置画布尺寸
|
| 230 |
+
window.addEventListener('resize', function() {
|
| 231 |
+
width = window.innerWidth;
|
| 232 |
+
height = window.innerHeight;
|
| 233 |
+
canvas.width = width;
|
| 234 |
+
canvas.height = height;
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
loginForm.addEventListener('submit', function(e) {
|
| 238 |
+
e.preventDefault();
|
| 239 |
+
const password = document.getElementById('password').value;
|
| 240 |
+
|
| 241 |
+
// 发送登录请求
|
| 242 |
+
fetch('/api/login', {
|
| 243 |
+
method: 'POST',
|
| 244 |
+
headers: {
|
| 245 |
+
'Content-Type': 'application/json'
|
| 246 |
+
},
|
| 247 |
+
body: JSON.stringify({ password: password })
|
| 248 |
+
})
|
| 249 |
+
.then(response => response.json())
|
| 250 |
+
.then(data => {
|
| 251 |
+
if (data.status === 'success') {
|
| 252 |
+
// 登录成功,保存会话到Cookie并跳转到管理页面
|
| 253 |
+
// 设置安全的Cookie,确保路径正确
|
| 254 |
+
document.cookie = "auth_token=" + data.token + "; path=/; max-age=86400;";
|
| 255 |
+
console.log("设置Cookie成功: auth_token=" + data.token);
|
| 256 |
+
|
| 257 |
+
setTimeout(() => {
|
| 258 |
+
window.location.href = '/admin';
|
| 259 |
+
}, 300);
|
| 260 |
+
} else {
|
| 261 |
+
// 显示错误消息
|
| 262 |
+
errorMessage.style.display = 'block';
|
| 263 |
+
errorMessage.textContent = data.error || '登录失败,请重试';
|
| 264 |
+
}
|
| 265 |
+
})
|
| 266 |
+
.catch(error => {
|
| 267 |
+
console.error('登录请求失败:', error);
|
| 268 |
+
errorMessage.style.display = 'block';
|
| 269 |
+
errorMessage.textContent = '网络错误,请重试';
|
| 270 |
+
});
|
| 271 |
+
});
|
| 272 |
+
});
|
| 273 |
+
</script>
|
| 274 |
+
</body>
|
| 275 |
+
</html>
|