Compare commits

..

8 Commits

Author SHA1 Message Date
Eg
65803a846f 🎨 优化正则处理逻辑 2026-03-04 20:01:51 +08:00
Eg
354e01992a 🎨 优化预设注入,删除预设权限认证,优化日志 2026-03-04 18:24:29 +08:00
Eg
cc8d7d6134 🎨 优化正则处理时的日志输出 2026-03-04 18:01:25 +08:00
Eg
8b9f4e97f5 🎨 优化正则处理时的日志输出 2026-03-04 17:58:42 +08:00
Eg
ce6bc8a37b 🎨 优化日志输出 2026-03-04 17:57:44 +08:00
a0d98b4761 🎨 优化上游ai兼容性,修复newapi返回token为空的问题 2026-03-04 02:29:28 +08:00
ae73298bbf 🎨 优化ai调用接口为统一格式 2026-03-04 02:22:31 +08:00
1526acc85f 🎨 ai调用时新增详细日志 2026-03-04 02:15:16 +08:00
8 changed files with 341 additions and 57 deletions

View File

@@ -53,18 +53,6 @@ func (a *AiProxyApi) ChatCompletions(c *gin.Context) {
return
}
// 验证预设权限
if req.PresetName != "" && !aiApiKeyService.CheckPresetPermission(apiKey, req.PresetName) {
c.JSON(http.StatusForbidden, gin.H{
"error": gin.H{
"message": "该API密钥无权使用此预设: " + req.PresetName,
"type": "invalid_request_error",
"code": "preset_not_allowed",
},
})
return
}
// 处理流式响应
if req.Stream {
aiProxyService.ProcessChatCompletionStream(c, &req)
@@ -145,18 +133,6 @@ func (a *AiProxyApi) ClaudeMessages(c *gin.Context) {
return
}
// 验证预设权限
if req.PresetName != "" && !aiApiKeyService.CheckPresetPermission(apiKey, req.PresetName) {
c.JSON(http.StatusForbidden, gin.H{
"error": gin.H{
"message": "该API密钥无权使用此预设: " + req.PresetName,
"type": "invalid_request_error",
"code": "preset_not_allowed",
},
})
return
}
// 处理流式响应
if req.Stream {
aiProxyService.ProcessClaudeMessageStream(c, &req)

View File

@@ -119,6 +119,9 @@ func Routers() *gin.Engine {
// 注册业务路由
initBizRouter(PrivateGroup, PublicGroup)
// 在根路径下注册 AI 通用接口路由(不带 /api 前缀,走 /v1/**
router.RouterGroupApp.App.InitAiProxyRootRouter(Router)
// 前端静态文件服务(放在最后,作为兜底路由)
setupFrontendRoutes(Router)

View File

@@ -10,6 +10,8 @@ type ClaudeMessageResponse struct {
StopReason string `json:"stop_reason,omitempty"` // end_turn, max_tokens, stop_sequence
StopSequence string `json:"stop_sequence,omitempty"`
Usage ClaudeUsage `json:"usage"`
// 统一格式的用量统计(与 OpenAI chat.completions 对齐)
StandardUsage *ChatCompletionUsage `json:"standard_usage,omitempty"`
}
type ClaudeContentBlock struct {

View File

@@ -7,9 +7,9 @@ type ChatCompletionResponse struct {
Created int64 `json:"created"`
Model string `json:"model"`
Choices []ChatCompletionChoice `json:"choices"`
Usage ChatCompletionUsage `json:"usage"`
// 扩展字段:正则脚本执行日志
RegexLogs *RegexExecutionLogs `json:"regex_logs,omitempty"`
Usage *ChatCompletionUsage `json:"usage,omitempty"`
// 统一格式的用量统计(用于各种上游的标准化)
StandardUsage *ChatCompletionUsage `json:"standard_usage,omitempty"`
}
type ChatCompletionChoice struct {

View File

@@ -8,6 +8,7 @@ import (
type AiProxyRouter struct{}
// InitAiProxyRouter 使用业务路由组(通常带有 /api 前缀),供后台管理前端调用
func (s *AiProxyRouter) InitAiProxyRouter(Router *gin.RouterGroup) {
aiProxyRouter := Router.Group("v1").Use(middleware.AiAuth())
aiProxyApi := v1.ApiGroupApp.AppApiGroup.AiProxyApi
@@ -17,3 +18,15 @@ func (s *AiProxyRouter) InitAiProxyRouter(Router *gin.RouterGroup) {
aiProxyRouter.POST("messages", aiProxyApi.ClaudeMessages) // Claude 兼容
}
}
// InitAiProxyRootRouter 在根路径下注册 AI 相关的通用接口,路径为 /v1/**
// 这样可以对外提供标准的 OpenAI/Claude 兼容地址:/v1/models、/v1/chat/completions、/v1/messages
func (s *AiProxyRouter) InitAiProxyRootRouter(engine *gin.Engine) {
aiProxyRouter := engine.Group("/v1").Use(middleware.AiAuth())
aiProxyApi := v1.ApiGroupApp.AppApiGroup.AiProxyApi
{
aiProxyRouter.GET("/models", aiProxyApi.ListModels) // 获取模型列表
aiProxyRouter.POST("/chat/completions", aiProxyApi.ChatCompletions) // OpenAI 兼容
aiProxyRouter.POST("/messages", aiProxyApi.ClaudeMessages) // Claude 兼容
}
}

View File

@@ -21,6 +21,13 @@ import (
// ProcessClaudeMessage 处理 Claude 消息请求
func (s *AiProxyService) ProcessClaudeMessage(ctx context.Context, req *request.ClaudeMessageRequest) (*response.ClaudeMessageResponse, error) {
// 记录请求参数
global.GVA_LOG.Info("收到 Claude Messages 请求",
zap.String("model", req.Model),
zap.Any("messages", req.Messages),
zap.Any("full_request", req),
)
// 1. 根据模型获取配置
if req.Model == "" {
return nil, fmt.Errorf("model 参数不能为空")
@@ -32,8 +39,9 @@ func (s *AiProxyService) ProcessClaudeMessage(ctx context.Context, req *request.
}
// 2. 注入预设
var injector *PresetInjector
if preset != nil {
injector := NewPresetInjector(preset)
injector = NewPresetInjector(preset)
req.Messages = s.convertClaudeMessages(injector.InjectMessages(s.convertToOpenAIMessages(req.Messages)))
}
@@ -43,17 +51,74 @@ func (s *AiProxyService) ProcessClaudeMessage(ctx context.Context, req *request.
return nil, err
}
// 4. 处理响应
if preset != nil && len(resp.Content) > 0 {
injector := NewPresetInjector(preset)
resp.Content[0].Text = injector.ProcessResponse(resp.Content[0].Text)
// 获取 AI 输出内容
aiOutput := ""
if len(resp.Content) > 0 {
aiOutput = resp.Content[0].Text
}
// 4. 处理响应(使用同一个 injector 实例)
if injector != nil && len(resp.Content) > 0 {
resp.Content[0].Text = injector.ProcessResponse(resp.Content[0].Text)
aiOutput = resp.Content[0].Text
}
// 5. 统一填充 standard_usage转换为 OpenAI 风格的用量统计
if resp.Usage.InputTokens > 0 || resp.Usage.OutputTokens > 0 {
resp.StandardUsage = &response.ChatCompletionUsage{
PromptTokens: resp.Usage.InputTokens,
CompletionTokens: resp.Usage.OutputTokens,
TotalTokens: resp.Usage.InputTokens + resp.Usage.OutputTokens,
}
}
// 记录响应内容
logFields := []zap.Field{
zap.String("ai_output", aiOutput),
zap.Any("usage", resp.Usage),
}
// 添加正则脚本执行日志(使用同一个 injector 实例)
if injector != nil {
regexLogs := injector.GetRegexLogs()
if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) {
// 收集触发的脚本名称
triggeredScripts := make([]string, 0)
for _, scriptLog := range regexLogs.InputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
for _, scriptLog := range regexLogs.OutputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
if len(triggeredScripts) > 0 {
logFields = append(logFields,
zap.Strings("triggered_regex_scripts", triggeredScripts),
zap.Int("total_matches", regexLogs.TotalMatches),
)
}
}
}
logFields = append(logFields, zap.Any("full_response", resp))
global.GVA_LOG.Info("Claude Messages 响应", logFields...)
return resp, nil
}
// ProcessClaudeMessageStream 处理 Claude 流式消息请求
func (s *AiProxyService) ProcessClaudeMessageStream(c *gin.Context, req *request.ClaudeMessageRequest) {
// 记录请求参数
global.GVA_LOG.Info("收到 Claude Messages 流式请求",
zap.String("model", req.Model),
zap.Any("messages", req.Messages),
zap.Any("full_request", req),
)
// 1. 根据模型获取配置
if req.Model == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "model 参数不能为空"})
@@ -135,9 +200,22 @@ func (s *AiProxyService) forwardClaudeStreamRequest(c *gin.Context, provider *ap
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(httpResp.Body)
global.GVA_LOG.Error("Claude 流式请求上游返回错误",
zap.Int("status_code", httpResp.StatusCode),
zap.String("response_body", string(body)),
)
return fmt.Errorf("上游返回错误: %d - %s", httpResp.StatusCode, string(body))
}
reader := bufio.NewReader(httpResp.Body)
flusher, _ := c.Writer.(http.Flusher)
// 聚合完整输出用于日志
var fullContent bytes.Buffer
var totalInputTokens, totalOutputTokens int
for {
line, err := reader.ReadBytes('\n')
if err == io.EOF {
@@ -150,10 +228,25 @@ func (s *AiProxyService) forwardClaudeStreamRequest(c *gin.Context, provider *ap
if bytes.HasPrefix(line, []byte("data: ")) {
data := bytes.TrimPrefix(line, []byte("data: "))
var chunk response.ClaudeStreamResponse
if json.Unmarshal(data, &chunk) == nil && chunk.Delta != nil {
if injector != nil {
chunk.Delta.Text = injector.ProcessResponse(chunk.Delta.Text)
if json.Unmarshal(data, &chunk) == nil {
// 收集 usage 信息
if chunk.Usage != nil {
totalInputTokens = chunk.Usage.InputTokens
totalOutputTokens = chunk.Usage.OutputTokens
}
// 收集文本内容并应用正则处理
if chunk.Delta != nil && chunk.Delta.Text != "" {
originalText := chunk.Delta.Text
fullContent.WriteString(originalText)
// 应用输出正则处理
if injector != nil {
chunk.Delta.Text = injector.ProcessResponse(originalText)
}
}
// 重新序列化并转发处理后的响应
processedData, _ := json.Marshal(chunk)
c.Writer.Write([]byte("data: "))
c.Writer.Write(processedData)
@@ -162,6 +255,50 @@ func (s *AiProxyService) forwardClaudeStreamRequest(c *gin.Context, provider *ap
}
}
}
// 流式结束后,对完整内容应用输出正则处理(用于日志记录)
processedContent := fullContent.String()
if injector != nil && processedContent != "" {
processedContent = injector.ProcessResponse(processedContent)
}
// 记录完整的流式响应日志
logFields := []zap.Field{
zap.String("ai_output_original", fullContent.String()),
zap.String("ai_output_processed", processedContent),
zap.Int("input_tokens", totalInputTokens),
zap.Int("output_tokens", totalOutputTokens),
zap.Int("total_tokens", totalInputTokens+totalOutputTokens),
}
// 添加正则脚本执行日志
if injector != nil {
regexLogs := injector.GetRegexLogs()
if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) {
// 收集触发的脚本名称
triggeredScripts := make([]string, 0)
for _, scriptLog := range regexLogs.InputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
for _, scriptLog := range regexLogs.OutputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
if len(triggeredScripts) > 0 {
logFields = append(logFields,
zap.Strings("triggered_regex_scripts", triggeredScripts),
zap.Int("total_matches", regexLogs.TotalMatches),
)
}
}
}
global.GVA_LOG.Info("Claude Messages 流式响应完成", logFields...)
return nil
}

View File

@@ -129,37 +129,61 @@ func (p *PresetInjector) buildInjectedMessages(messages []request.ChatMessage, p
}
// injectRelativePrompts 将相对位置的提示词注入到对话历史中
// 注入深度从前往后计算depth=0 表示索引0最前面depth=1 表示索引1以此类推
func (p *PresetInjector) injectRelativePrompts(messages []request.ChatMessage, prompts []app.PresetPrompt) []request.ChatMessage {
if len(prompts) == 0 {
return messages
}
result := make([]request.ChatMessage, 0, len(messages)+len(prompts))
messageCount := len(messages)
// 按深度分组提示词
// 按深度分组提示词,并按 injection_order 排序
depthMap := make(map[int][]app.PresetPrompt)
for _, prompt := range prompts {
depthMap[prompt.InjectionDepth] = append(depthMap[prompt.InjectionDepth], prompt)
}
// 遍历消息,在指定深度注入提示词
for i, msg := range messages {
// 计算当前位置的深度(从末尾开始计数)
depth := messageCount - i
// 对每个深度的提示词按 injection_order 排序(从大到小,优先级高的在前)
for depth := range depthMap {
sort.Slice(depthMap[depth], func(i, j int) bool {
return depthMap[depth][i].InjectionOrder > depthMap[depth][j].InjectionOrder
})
}
// 在当前消息之前注入对应深度的提示词
result := make([]request.ChatMessage, 0, len(messages)+len(prompts))
totalInserted := 0
// 找出最大深度
maxDepth := 0
for depth := range depthMap {
if depth > maxDepth {
maxDepth = depth
}
}
// 从 depth=0 开始,逐个深度注入
for depth := 0; depth <= maxDepth; depth++ {
// 计算实际注入位置(考虑之前已注入的消息数量)
injectIdx := depth + totalInserted
// 如果注入位置超出当前消息列表,先添加原始消息直到该位置
for len(result) < injectIdx && len(result)-totalInserted < len(messages) {
result = append(result, messages[len(result)-totalInserted])
}
// 注入当前深度的所有提示词
if promptsAtDepth, exists := depthMap[depth]; exists {
for _, prompt := range promptsAtDepth {
result = append(result, request.ChatMessage{
Role: prompt.Role,
Content: p.processPromptContent(prompt.Content),
})
totalInserted++
}
}
}
// 添加当前消息
result = append(result, msg)
// 添加剩余的原始消息
for len(result)-totalInserted < len(messages) {
result = append(result, messages[len(result)-totalInserted])
}
return result

View File

@@ -23,6 +23,13 @@ type AiProxyService struct{}
// ProcessChatCompletion 处理聊天补全请求
func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, req *request.ChatCompletionRequest) (*response.ChatCompletionResponse, error) {
// 记录请求参数
global.GVA_LOG.Info("收到 ChatCompletion 请求",
zap.String("model", req.Model),
zap.Any("messages", req.Messages),
zap.Any("full_request", req),
)
// 1. 根据模型获取配置
if req.Model == "" {
return nil, fmt.Errorf("model 参数不能为空")
@@ -47,22 +54,82 @@ func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, req *request
return nil, err
}
// 4. 处理响应并收集正则日志
if preset != nil && injector != nil && len(resp.Choices) > 0 {
resp.Choices[0].Message.Content = injector.ProcessResponse(resp.Choices[0].Message.Content)
// 获取 AI 输出内容
aiOutput := ""
if len(resp.Choices) > 0 {
aiOutput = resp.Choices[0].Message.Content
}
// 添加正则执行日志到响应
regexLogs := injector.GetRegexLogs()
if regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0 {
resp.RegexLogs = regexLogs
// 应用预设处理(使用同一个 injector 实例)
if injector != nil && len(resp.Choices) > 0 {
resp.Choices[0].Message.Content = injector.ProcessResponse(resp.Choices[0].Message.Content)
aiOutput = resp.Choices[0].Message.Content
}
// 4. 处理响应并收集正则日志
if resp != nil && resp.Usage != nil {
// 统一填充 standard_usage方便上游使用统一格式解析
if resp.Usage.PromptTokens > 0 || resp.Usage.CompletionTokens > 0 || resp.Usage.TotalTokens > 0 {
resp.StandardUsage = &response.ChatCompletionUsage{
PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.TotalTokens,
}
}
}
// 记录响应内容(统一日志输出)
logFields := []zap.Field{
zap.String("ai_output", aiOutput),
}
if resp.Usage != nil {
logFields = append(logFields, zap.Any("usage", resp.Usage))
}
if resp.StandardUsage != nil {
logFields = append(logFields, zap.Any("standard_usage", resp.StandardUsage))
}
// 添加正则脚本执行日志
if injector != nil {
regexLogs := injector.GetRegexLogs()
if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) {
// 收集触发的脚本名称
triggeredScripts := make([]string, 0)
for _, scriptLog := range regexLogs.InputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
for _, scriptLog := range regexLogs.OutputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
if len(triggeredScripts) > 0 {
logFields = append(logFields,
zap.Strings("triggered_regex_scripts", triggeredScripts),
zap.Int("total_matches", regexLogs.TotalMatches),
)
}
}
}
logFields = append(logFields, zap.Any("full_response", resp))
global.GVA_LOG.Info("ChatCompletion 响应", logFields...)
return resp, nil
}
// ProcessChatCompletionStream 处理流式聊天补全请求
func (s *AiProxyService) ProcessChatCompletionStream(c *gin.Context, req *request.ChatCompletionRequest) {
// 记录请求参数
global.GVA_LOG.Info("收到 ChatCompletion 流式请求",
zap.String("model", req.Model),
zap.Any("messages", req.Messages),
zap.Any("full_request", req),
)
// 1. 根据模型获取配置
if req.Model == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "model 参数不能为空"})
@@ -129,6 +196,13 @@ func (s *AiProxyService) forwardRequest(ctx context.Context, provider *app.AiPro
return nil, fmt.Errorf("序列化请求失败: %w", err)
}
// 上游请求参数
global.GVA_LOG.Info("转发 ChatCompletion 请求到上游",
zap.String("provider", provider.Name),
zap.String("model", req.Model),
zap.Any("messages", req.Messages),
)
url := strings.TrimRight(provider.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(reqBody))
if err != nil {
@@ -171,6 +245,12 @@ func (s *AiProxyService) forwardStreamRequest(c *gin.Context, provider *app.AiPr
if err != nil {
return err
}
// 上游请求参数
global.GVA_LOG.Info("转发 ChatCompletion 流式请求到上游",
zap.String("provider", provider.Name),
zap.String("model", req.Model),
zap.Any("messages", req.Messages),
)
url := strings.TrimRight(provider.BaseURL, "/") + "/v1/chat/completions"
httpReq, err := http.NewRequestWithContext(c.Request.Context(), "POST", url, bytes.NewReader(reqBody))
@@ -200,6 +280,9 @@ func (s *AiProxyService) forwardStreamRequest(c *gin.Context, provider *app.AiPr
return fmt.Errorf("不支持流式响应")
}
// 聚合 AI 输出内容用于日志和正则处理
var fullContent bytes.Buffer
for {
line, err := reader.ReadBytes('\n')
if err != nil {
@@ -232,12 +315,18 @@ func (s *AiProxyService) forwardStreamRequest(c *gin.Context, provider *app.AiPr
continue
}
// 应用输出正则处理
if injector != nil && len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
chunk.Choices[0].Delta.Content = injector.ProcessResponse(chunk.Choices[0].Delta.Content)
// 收集原始内容并应用正则处理
if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
originalContent := chunk.Choices[0].Delta.Content
fullContent.WriteString(originalContent)
// 应用输出正则处理
if injector != nil {
chunk.Choices[0].Delta.Content = injector.ProcessResponse(originalContent)
}
}
// 重新序列化并发送
// 重新序列化并转发处理后的响应
processedData, _ := json.Marshal(chunk)
c.Writer.Write([]byte("data: "))
c.Writer.Write(processedData)
@@ -246,6 +335,46 @@ func (s *AiProxyService) forwardStreamRequest(c *gin.Context, provider *app.AiPr
}
}
// 流式结束后,对完整内容应用输出正则处理(仅用于日志记录)
processedContent := fullContent.String()
if injector != nil && processedContent != "" {
processedContent = injector.ProcessResponse(processedContent)
}
// 流式请求结束后记录日志
logFields := []zap.Field{
zap.String("ai_output_original", fullContent.String()),
zap.String("ai_output_processed", processedContent),
}
// 添加正则脚本执行日志
if injector != nil {
regexLogs := injector.GetRegexLogs()
if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) {
// 收集触发的脚本名称
triggeredScripts := make([]string, 0)
for _, scriptLog := range regexLogs.InputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
for _, scriptLog := range regexLogs.OutputScripts {
if scriptLog.MatchCount > 0 {
triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount))
}
}
if len(triggeredScripts) > 0 {
logFields = append(logFields,
zap.Strings("triggered_regex_scripts", triggeredScripts),
zap.Int("total_matches", regexLogs.TotalMatches),
)
}
}
}
global.GVA_LOG.Info("ChatCompletion 流式响应完成", logFields...)
return nil
}