后端 - 新增 proxy/convert 三协议(chat/messages/responses)请求、响应与 SSE 流式互转, 以 Chat 为中间模型;usage.go 统一提取三协议 token 用量(含单测) - gateway: 跨协议调度(渠道未声明客户端协议时转为渠道首选格式), streamResponse 按 \n\n 分块逐行转换直通,bufferResponse 转换失败时剥非 JSON 前缀 - gateway: 新增 SetUsageRecorder 注入异步用量记录器 - auth_llm: 修复 key_prefix 查询长度错配([:8] vs 存储的 [:12])导致全部 401; 修复长度 8-11 的 key 切片越界 panic;统一 unauthorized 响应 - usage: 日报表改为增量累加 upsert,避免多次 flush 互相清零;记录协议/错误码/时延等字段 - channel: 新增渠道并发槽 TryAcquire;健康检查支持可配置参数 - api: 新增 admin 渠道/模型/系统配置管理端点(旧端点保留兼容) 前端 - 新增渠道管理、模型管理、系统配置视图与 ChannelModelsDrawer - 新增 ui 基础组件(Button/Badge/Input/Modal)与 protocol.ts - 调整 Toast 样式、密钥页、路由菜单;dev 代理默认指向 3000 端口
66 lines
1.7 KiB
Go
66 lines
1.7 KiB
Go
package middleware
|
||
|
||
import (
|
||
"net/http"
|
||
"opencatd-open/internal/store"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// keyPrefixLen 是 key_prefix 列的截断长度,必须与 api.go:459 的 keyValue[:12] 一致。
|
||
// 真实 key 为 sk-ot- + 48 位 hex(54 字符),故 12 位足够唯一。
|
||
const keyPrefixLen = 12
|
||
|
||
func AuthLLM(db *gorm.DB) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
key := extractAPIKey(c.GetHeader("Authorization"))
|
||
|
||
// 区分「没传」和「传了但不对」,便于排查客户端配置。
|
||
if strings.TrimSpace(c.GetHeader("Authorization")) == "" {
|
||
unauthorized(c, "未提供认证信息")
|
||
return
|
||
}
|
||
// 长度不足时直接拒绝:避免下方 authToken[:12] 越界 panic 打崩进程。
|
||
if len(key) < keyPrefixLen {
|
||
unauthorized(c, "无效的API密钥")
|
||
return
|
||
}
|
||
|
||
var apiKey store.APIKey
|
||
if err := db.Where("key_prefix = ? AND status = ?", key[:keyPrefixLen], store.KeyStatusActive).First(&apiKey).Error; err != nil {
|
||
unauthorized(c, "无效的API密钥")
|
||
return
|
||
}
|
||
|
||
// Verify full key hash
|
||
if apiKey.KeyHash != store.HashAPIKey(key) {
|
||
unauthorized(c, "无效的API密钥")
|
||
return
|
||
}
|
||
|
||
c.Set("api_key", &apiKey)
|
||
c.Set("user_id", apiKey.UserID)
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// extractAPIKey 从 Authorization 头取 Bearer token,兼容无 "Bearer " 前缀的直传。
|
||
func extractAPIKey(auth string) string {
|
||
auth = strings.TrimSpace(auth)
|
||
if strings.HasPrefix(auth, "Bearer ") {
|
||
return strings.TrimSpace(auth[len("Bearer "):])
|
||
}
|
||
return auth
|
||
}
|
||
|
||
func unauthorized(c *gin.Context, message string) {
|
||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||
"error": map[string]interface{}{
|
||
"message": message,
|
||
"type": "invalid_request_error",
|
||
},
|
||
})
|
||
}
|