Merge remote-tracking branch 'yvan/main'

# Conflicts:
#	server/go.mod
#	server/go.sum
This commit is contained in:
2025-09-10 19:49:22 +08:00
27 changed files with 1522 additions and 685 deletions
+158
View File
@@ -0,0 +1,158 @@
package utils
import (
"sync"
"github.com/flipped-aurora/gin-vue-admin/server/global"
swd "github.com/kirklin/go-swd"
"go.uber.org/zap"
)
type SensitiveWordUtil struct {
detector *swd.SWD
once sync.Once
initErr error
}
// 全局单例实例
var (
sensitiveWordInstance *SensitiveWordUtil
sensitiveWordOnce sync.Once
)
// GetSensitiveWordUtil 获取敏感词工具单例实例
func GetSensitiveWordUtil() *SensitiveWordUtil {
sensitiveWordOnce.Do(func() {
sensitiveWordInstance = &SensitiveWordUtil{}
})
return sensitiveWordInstance
}
// InitDetector 初始化敏感词检测器(单例模式)
func (s *SensitiveWordUtil) InitDetector() error {
s.once.Do(func() {
detector, err := swd.New()
if err != nil {
s.initErr = err
// 只有在global.GVA_LOG不为nil时才记录日志
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Failed to initialize sensitive word detector", zap.Error(err))
}
return
}
s.detector = detector
// 只有在global.GVA_LOG不为nil时才记录日志
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Sensitive word detector initialized successfully")
}
})
return s.initErr
}
// DetectSensitive 检测文本中的敏感词
func (s *SensitiveWordUtil) DetectSensitive(text string) ([]swd.SensitiveWord, error) {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return nil, err
}
}
words := s.detector.MatchAll(text)
return words, nil
}
// HasSensitive 检查文本是否包含敏感词
func (s *SensitiveWordUtil) HasSensitive(text string) (bool, error) {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return false, err
}
}
return s.detector.Detect(text), nil
}
// FilterText 过滤文本中的敏感词,返回过滤后的文本和是否包含敏感词
func (s *SensitiveWordUtil) FilterText(text string) (filtered string, hasSensitive bool, err error) {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return text, false, err
}
}
// 检查是否包含敏感词
hasSensitive = s.detector.Detect(text)
if !hasSensitive {
return text, false, nil
}
// 使用星号替换敏感词
filtered = s.detector.ReplaceWithAsterisk(text)
return filtered, true, nil
}
// FilterTextWithCustomReplace 使用自定义替换策略过滤敏感词
func (s *SensitiveWordUtil) FilterTextWithCustomReplace(text string, replaceFunc func(swd.SensitiveWord) string) (filtered string, hasSensitive bool, err error) {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return text, false, err
}
}
// 检查是否包含敏感词
hasSensitive = s.detector.Detect(text)
if !hasSensitive {
return text, false, nil
}
// 使用自定义替换策略
filtered = s.detector.ReplaceWithStrategy(text, replaceFunc)
return filtered, true, nil
}
// GetFirstSensitiveWord 获取文本中第一个敏感词
func (s *SensitiveWordUtil) GetFirstSensitiveWord(text string) (*swd.SensitiveWord, error) {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return nil, err
}
}
word := s.detector.Match(text)
return word, nil
}
// AddCustomWords 添加自定义敏感词
func (s *SensitiveWordUtil) AddCustomWords(words map[string]swd.Category) error {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return err
}
}
return s.detector.AddWords(words)
}
// RemoveWord 移除指定敏感词
func (s *SensitiveWordUtil) RemoveWord(word string) error {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return err
}
}
return s.detector.RemoveWord(word)
}
// Clear 清空词库
func (s *SensitiveWordUtil) Clear() error {
if s.detector == nil {
if err := s.InitDetector(); err != nil {
return err
}
}
return s.detector.Clear()
}
+379
View File
@@ -0,0 +1,379 @@
package utils
import (
"context"
"fmt"
"sync"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/google/uuid"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"go.uber.org/zap"
)
// LLMMessage 聊天消息结构体
type LLMMessage struct {
Role string `json:"role"` // 角色: system, user, assistant
Content string `json:"content"` // 消息内容
}
// LLMRequest 聊天请求结构体
type LLMRequest struct {
Messages []LLMMessage `json:"messages" binding:"required"` // 对话消息列表
Model string `json:"model"` // 模型名称,如果为空则使用默认模型
Stream bool `json:"stream"` // 是否流式响应,默认false
Temperature float64 `json:"temperature"` // 温度参数,控制随机性,范围0-1
MaxTokens int `json:"max_tokens"` // 最大生成token数
TopP float64 `json:"top_p"` // 核采样参数
RequestID string `json:"request_id,omitempty"` // 请求ID,用于流式响应管理
}
// LLMChoice 响应选择结构体
type LLMChoice struct {
Index int `json:"index"` // 选择索引
Message LLMMessage `json:"message,omitempty"` // 完整响应消息(非流式)
Delta LLMMessage `json:"delta,omitempty"` // 增量消息(流式)
FinishReason string `json:"finish_reason,omitempty"` // 结束原因: stop, length, content_filter
}
// LLMUsage 使用量统计结构体
type LLMUsage struct {
PromptTokens int `json:"prompt_tokens"` // 输入token数
CompletionTokens int `json:"completion_tokens"` // 输出token数
TotalTokens int `json:"total_tokens"` // 总token数
}
// LLMError API错误结构体
type LLMError struct {
Code string `json:"code"` // 错误代码
Message string `json:"message"` // 错误消息
Type string `json:"type"` // 错误类型
}
// LLMResponse 聊天响应结构体
type LLMResponse struct {
ID string `json:"id"` // 响应ID
Object string `json:"object"` // 对象类型: chat.completion 或 chat.completion.chunk
Created int64 `json:"created"` // 创建时间戳
Model string `json:"model"` // 使用的模型
Choices []LLMChoice `json:"choices"` // 响应选择列表
Usage *LLMUsage `json:"usage,omitempty"` // 使用量统计(非流式响应)
Error *LLMError `json:"error,omitempty"` // 错误信息
}
// LLMStreamEvent 流式事件结构体
type LLMStreamEvent struct {
Event string `json:"event"` // 事件类型: message, error, done
Data LLMResponse `json:"data"` // 事件数据
}
// VolcengineLLMUtil 火山引擎LLM工具类
type VolcengineLLMUtil struct {
client *arkruntime.Client
activeSessions sync.Map // requestID -> context.CancelFunc
once sync.Once
initErr error
}
// 全局单例实例
var (
volcengineLLMInstance *VolcengineLLMUtil
volcengineLLMOnce sync.Once
)
// GetVolcengineLLMUtil 获取火山引擎LLM工具单例实例
func GetVolcengineLLMUtil() *VolcengineLLMUtil {
volcengineLLMOnce.Do(func() {
volcengineLLMInstance = &VolcengineLLMUtil{}
})
return volcengineLLMInstance
}
// InitClient 初始化火山引擎客户端(单例模式)
func (v *VolcengineLLMUtil) InitClient() error {
v.once.Do(func() {
config := global.GVA_CONFIG.Volcengine
// 验证配置
if config.ApiKey == "" {
v.initErr = fmt.Errorf("volcengine api key is empty")
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Volcengine configuration error", zap.Error(v.initErr))
}
return
}
// 创建ARK Runtime客户端,使用API Key
v.client = arkruntime.NewClientWithApiKey(config.ApiKey)
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Volcengine LLM client initialized successfully")
}
})
return v.initErr
}
// ChatCompletion 非流式聊天完成
func (v *VolcengineLLMUtil) ChatCompletion(req LLMRequest) (*LLMResponse, error) {
if v.client == nil {
if err := v.InitClient(); err != nil {
return nil, err
}
}
// 生成请求ID
if req.RequestID == "" {
req.RequestID = uuid.New().String()
}
// 转换消息格式
messages := make([]*model.ChatCompletionMessage, len(req.Messages))
for i, msg := range req.Messages {
messages[i] = &model.ChatCompletionMessage{
Role: msg.Role,
Content: &model.ChatCompletionMessageContent{
StringValue: &msg.Content,
},
}
}
// 设置模型
modelName := req.Model
if modelName == "" {
modelName = global.GVA_CONFIG.Volcengine.Model
}
// 创建请求
chatReq := &model.ChatCompletionRequest{
Model: modelName,
Messages: messages,
Stream: false,
}
// 设置可选参数
if req.Temperature > 0 {
chatReq.Temperature = float32(req.Temperature)
}
if req.MaxTokens > 0 {
chatReq.MaxTokens = req.MaxTokens
}
if req.TopP > 0 {
chatReq.TopP = float32(req.TopP)
}
// 调用API
resp, err := v.client.CreateChatCompletion(context.Background(), chatReq)
if err != nil {
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
}
return nil, err
}
// 转换响应格式
choices := make([]LLMChoice, len(resp.Choices))
for i, choice := range resp.Choices {
choices[i] = LLMChoice{
Index: choice.Index,
Message: LLMMessage{
Role: choice.Message.Role,
Content: *choice.Message.Content.StringValue,
},
FinishReason: string(choice.FinishReason),
}
}
usage := &LLMUsage{
PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.TotalTokens,
}
chatResp := &LLMResponse{
ID: resp.ID,
Object: resp.Object,
Created: resp.Created,
Model: resp.Model,
Choices: choices,
Usage: usage,
}
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Chat completion successful", zap.String("requestID", req.RequestID))
}
return chatResp, nil
}
// StreamChatCompletion 流式聊天完成
func (v *VolcengineLLMUtil) StreamChatCompletion(req LLMRequest, eventChan chan<- LLMStreamEvent) error {
if v.client == nil {
if err := v.InitClient(); err != nil {
return err
}
}
// 生成请求ID
if req.RequestID == "" {
req.RequestID = uuid.New().String()
}
// 转换消息格式
messages := make([]*model.ChatCompletionMessage, len(req.Messages))
for i, msg := range req.Messages {
messages[i] = &model.ChatCompletionMessage{
Role: msg.Role,
Content: &model.ChatCompletionMessageContent{
StringValue: &msg.Content,
},
}
}
// 设置模型
modelName := req.Model
if modelName == "" {
modelName = global.GVA_CONFIG.Volcengine.Model
}
// 创建流式请求
chatReq := &model.ChatCompletionRequest{
Model: modelName,
Messages: messages,
Stream: true,
}
// 设置可选参数
if req.Temperature > 0 {
chatReq.Temperature = float32(req.Temperature)
}
if req.MaxTokens > 0 {
chatReq.MaxTokens = req.MaxTokens
}
if req.TopP > 0 {
chatReq.TopP = float32(req.TopP)
}
// 创建上下文和取消函数
ctx, cancel := context.WithCancel(context.Background())
v.activeSessions.Store(req.RequestID, cancel)
defer func() {
v.activeSessions.Delete(req.RequestID)
cancel()
}()
// 调用流式API
stream_resp, err := v.client.CreateChatCompletionStream(ctx, chatReq)
if err != nil {
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Stream chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
}
return err
}
defer stream_resp.Close()
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Stream chat completion started", zap.String("requestID", req.RequestID))
}
// 处理流式响应
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// 接收流式数据
recv, err := stream_resp.Recv()
if err != nil {
if err.Error() == "EOF" {
// 流结束
eventChan <- LLMStreamEvent{
Event: "done",
Data: LLMResponse{
ID: req.RequestID,
Object: "chat.completion.chunk",
},
}
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Stream chat completion finished", zap.String("requestID", req.RequestID))
}
return nil
}
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Stream receive error", zap.Error(err), zap.String("requestID", req.RequestID))
}
eventChan <- LLMStreamEvent{
Event: "error",
Data: LLMResponse{
ID: req.RequestID,
Error: &LLMError{
Code: "stream_error",
Message: err.Error(),
Type: "stream_error",
},
},
}
return err
}
// 转换响应格式
choices := make([]LLMChoice, len(recv.Choices))
for i, choice := range recv.Choices {
choices[i] = LLMChoice{
Index: choice.Index,
Delta: LLMMessage{
Role: choice.Delta.Role,
Content: choice.Delta.Content,
},
FinishReason: string(choice.FinishReason),
}
}
var usage *LLMUsage
if recv.Usage != nil {
usage = &LLMUsage{
PromptTokens: recv.Usage.PromptTokens,
CompletionTokens: recv.Usage.CompletionTokens,
TotalTokens: recv.Usage.TotalTokens,
}
}
// 发送消息事件
eventChan <- LLMStreamEvent{
Event: "message",
Data: LLMResponse{
ID: recv.ID,
Object: recv.Object,
Created: recv.Created,
Model: recv.Model,
Choices: choices,
Usage: usage,
},
}
}
}
}
// StopGeneration 停止生成
func (v *VolcengineLLMUtil) StopGeneration(requestID string) error {
if cancel, ok := v.activeSessions.Load(requestID); ok {
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
cancelFunc()
v.activeSessions.Delete(requestID)
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Generation stopped", zap.String("requestID", requestID))
}
return nil
}
}
return fmt.Errorf("session not found: %s", requestID)
}
// GetActiveSessionsCount 获取活跃会话数量
func (v *VolcengineLLMUtil) GetActiveSessionsCount() int {
count := 0
v.activeSessions.Range(func(key, value interface{}) bool {
count++
return true
})
return count
}