Files
opencatd-open/backend/middleware/auth_llm.go
T
Sakurasan f81b364436 feat: 三协议互转网关 + 鉴权修复 + 管理端增强
后端
- 新增 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 端口
2026-08-31 22:29:09 +08:00

66 lines
1.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
},
})
}