@@ -3,6 +3,7 @@ package app
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@@ -371,21 +372,15 @@ func (s *ConversationService) SendMessage(userID, conversationID uint, req *requ
return nil , err
}
// 获取对话历史(最近10条 )
// 获取完整 对话历史(context 管理由 callAIService 内部处理 )
var messages [ ] app . Message
err = global . GVA_DB . Where ( "conversation_id = ?" , conversationID ) .
Order ( "created_at DE SC" ) .
Limit ( 10 ) .
Order ( "created_at A SC" ) .
Find ( & messages ) . Error
if err != nil {
return nil , err
}
// 反转消息顺序(从旧到新)
for i , j := 0 , len ( messages ) - 1 ; i < j ; i , j = i + 1 , j - 1 {
messages [ i ] , messages [ j ] = messages [ j ] , messages [ i ]
}
// 调用 AI 服务获取回复
aiResponse , err := s . callAIService ( conversation , character , messages )
if err != nil {
@@ -527,47 +522,26 @@ func (s *ConversationService) callAIService(conversation app.Conversation, chara
}
}
// 构建系统提示词(如果预设有系统提示词,则追加到角色卡提示词后 )
systemPrompt := s . buildSystemPrompt ( character )
if preset != nil && preset . SystemPrompt != "" {
system Prompt = systemPrompt + "\n\n" + preset . SystemPrompt
global . GVA_LOG . Info ( "已追加预设的系统提示词" )
// 构建消息列表(含 context 预算管理 )
var presetSysPrompt string
if preset != nil {
presetSys Prompt = preset . SystemPrompt
}
wbEngine := & WorldbookEngine { }
apiMessages := s . buildAPIMessagesWithContextManagement (
messages , character , presetSysPrompt , wbEngine , conversation , & aiConfig , preset ,
)
// 集成世界书触发引擎
if conversation . WorldbookEnabled && conversation . WorldbookID ! = nil {
global . GVA_LOG . Info ( fmt . Sprintf ( "世界书已启用, ID: %d" , * conversation . WorldbookID ) )
// 提取消息内容用于扫描
var messageContents [ ] string
for _ , msg := range messages {
messageContents = append ( messageContents , msg . Content )
}
// 使用世界书引擎扫描并触发条目
engine := & WorldbookEngine { }
triggered , err := engine . ScanAndTrigger ( * conversation . WorldbookID , messageContents )
if err != nil {
global . GVA_LOG . Warn ( fmt . Sprintf ( "世界书触发失败: %v" , err ) )
} else if len ( triggered ) > 0 {
global . GVA_LOG . Info ( fmt . Sprintf ( "触发了 %d 个世界书条目" , len ( triggered ) ) )
// 将触发的世界书内容注入到系统提示词
systemPrompt = engine . BuildPromptWithWorldbook ( systemPrompt , triggered )
} else {
global . GVA_LOG . Info ( "没有触发任何世界书条目" )
}
// 从 apiMessages 中提取 systemPrompt, 供 Anthropic 独立参数使用
systemPrompt : = ""
if len ( apiMessages ) > 0 && apiMessages [ 0 ] [ "role" ] == "system" {
systemPrompt = apiMessages [ 0 ] [ "content" ]
}
// 构建消息列表
apiMessages := s . buildAPIMessages ( messages , systemPrompt )
// 打印发送给AI的完整内容
global . GVA_LOG . Info ( "========== 发送给AI的完整内容 ==========" )
global . GVA_LOG . Info ( fmt . Sprintf ( "系统提示词: %s " , systemPrompt ) )
global . GVA_LOG . Info ( "消息列表:" )
for i , msg := range apiMessages {
global . GVA_LOG . Info ( fmt . Sprintf ( " [%d] Role: %s, Content: %s" , i , msg [ "role" ] , msg [ "content" ] ) )
}
global . GVA_LOG . Info ( fmt . Sprintf ( "系统提示词长度 : %d 字符 " , len ( systemPrompt ) ) )
global . GVA_LOG . Info ( fmt . Sprintf ( "历史消息条数: %d" , len ( apiMessages ) - 1 ) )
global . GVA_LOG . Info ( "==========================================" )
// 确定使用的模型: 如果用户在设置中指定了AI配置, 则使用该配置的默认模型
@@ -735,7 +709,7 @@ func (s *ConversationService) getWeekdayInChinese(weekday time.Weekday) string {
}
// SendMessageStream 流式发送消息并获取 AI 回复
func ( s * ConversationService ) SendMessageStream ( userID , conversationID uint , req * request . SendMessageRequest , streamChan chan string , doneChan chan bool ) error {
func ( s * ConversationService ) SendMessageStream ( ctx context . Context , userID, conversationID uint , req * request . SendMessageRequest , streamChan chan string , doneChan chan bool ) error {
defer close ( streamChan )
defer close ( doneChan )
@@ -796,21 +770,15 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
return err
}
// 获取对话历史(最近10条 )
// 获取完整 对话历史(context 管理由 buildAPIMessagesWithContextManagement 处理 )
var messages [ ] app . Message
err = global . GVA_DB . Where ( "conversation_id = ?" , conversationID ) .
Order ( "created_at DE SC" ) .
Limit ( 10 ) .
Order ( "created_at A SC" ) .
Find ( & messages ) . Error
if err != nil {
return err
}
// 反转消息顺序(从旧到新)
for i , j := 0 , len ( messages ) - 1 ; i < j ; i , j = i + 1 , j - 1 {
messages [ i ] , messages [ j ] = messages [ j ] , messages [ i ]
}
// 获取 AI 配置
var aiConfig app . AIConfig
var configID uint
@@ -857,42 +825,26 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
}
}
// 构建系统提示词(应用预设 )
systemPrompt := s . buildSystemPrompt ( character )
if streamPreset != nil && streamPreset . SystemPrompt != "" {
systemPrompt = systemPrompt + "\n\n" + streamPreset . SystemPrompt
// 构建消息列表(含 context 预算管理 )
var streamPresetSysPrompt string
if streamPreset != nil {
streamPresetSysPrompt = streamPreset . SystemPrompt
}
streamWbEngine := & WorldbookEngine { }
apiMessages := s . buildAPIMessagesWithContextManagement (
messages , character , streamPresetSysPrompt , streamWbEngine , conversation , & aiConfig , streamPreset ,
)
// 集成世界书触发引擎(流式传输)
if conversation . WorldbookEnabled && conversation . WorldbookID ! = nil {
global . GVA_LOG . Info ( fmt . Sprintf ( "[流式传输] 世界书已启用, ID: %d" , * conversation . WorldbookID ) )
var messageContents [ ] string
for _ , msg := range messages {
messageContents = append ( messageContents , msg . Content )
}
engine := & WorldbookEngine { }
triggeredEntries , wbErr := engine . ScanAndTrigger ( * conversation . WorldbookID , messageContents )
if wbErr != nil {
global . GVA_LOG . Warn ( fmt . Sprintf ( "[流式传输] 世界书触发失败: %v" , wbErr ) )
} else if len ( triggeredEntries ) > 0 {
global . GVA_LOG . Info ( fmt . Sprintf ( "[流式传输] 触发了 %d 个世界书条目" , len ( triggeredEntries ) ) )
systemPrompt = engine . BuildPromptWithWorldbook ( systemPrompt , triggeredEntries )
} else {
global . GVA_LOG . Info ( "[流式传输] 没有触发任何世界书条目" )
}
// 从 apiMessages 中提取 systemPrompt, 供 Anthropic 独立参数使用
systemPrompt : = ""
if len ( apiMessages ) > 0 && apiMessages [ 0 ] [ "role" ] == "system" {
systemPrompt = apiMessages [ 0 ] [ "content" ]
}
apiMessages := s . buildAPIMessages ( messages , systemPrompt )
// 打印发送给AI的完整内容( 流式传输)
global . GVA_LOG . Info ( "========== [流式传输] 发送给AI的完整内容 ==========" )
global . GVA_LOG . Info ( fmt . Sprintf ( "系统提示词: %s " , systemPrompt ) )
global . GVA_LOG . Info ( "消息列表:" )
for i , msg := range apiMessages {
global . GVA_LOG . Info ( fmt . Sprintf ( " [%d] Role: %s, Content: %s" , i , msg [ "role" ] , msg [ "content" ] ) )
}
global . GVA_LOG . Info ( fmt . Sprintf ( "[流式传输] 系统提示词长度 : %d 字符 " , len ( systemPrompt ) ) )
global . GVA_LOG . Info ( fmt . Sprintf ( "[流式传输] 历史消息条数: %d" , len ( apiMessages ) - 1 ) )
global . GVA_LOG . Info ( "==========================================" )
// 确定使用的模型
@@ -910,15 +862,21 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
var fullContent string
switch aiConfig . Provider {
case "openai" , "custom" :
fullContent , err = s . callOpenAIAPIStream ( & aiConfig , model , apiMessages , streamPreset , streamChan )
fullContent , err = s . callOpenAIAPIStream ( ctx , & aiConfig , model , apiMessages , streamPreset , streamChan )
case "anthropic" :
fullContent , err = s . callAnthropicAPIStream ( & aiConfig , model , apiMessages , systemPrompt , streamPreset , streamChan )
fullContent , err = s . callAnthropicAPIStream ( ctx , & aiConfig , model , apiMessages , systemPrompt , streamPreset , streamChan )
default :
return fmt . Errorf ( "不支持的 AI 提供商: %s" , aiConfig . Provider )
}
if err != nil {
global . GVA_LOG . Error ( fmt . Sprintf ( "========== [流式传输] AI返回错误 ==========\n%v\n==========================================" , err ) )
// AI 调用失败,回滚已写入的用户消息,避免孤立记录残留在数据库
if delErr := global . GVA_DB . Delete ( & userMessage ) . Error ; delErr != nil {
global . GVA_LOG . Error ( fmt . Sprintf ( "[流式传输] 回滚用户消息失败: %v" , delErr ) )
} else {
global . GVA_LOG . Info ( "[流式传输] 已回滚用户消息" )
}
return err
}
@@ -962,8 +920,9 @@ func (s *ConversationService) SendMessageStream(userID, conversationID uint, req
}
// callOpenAIAPIStream 调用 OpenAI API 流式传输
func ( s * ConversationService ) callOpenAIAPIStream ( config * app . AIConfig , model string , messages [ ] map [ string ] string , preset * app . AIPreset , streamChan chan string ) ( string , error ) {
client := & http . Client { Timeout : 120 * time . Second }
func ( s * ConversationService ) callOpenAIAPIStream ( ctx context . Context , config * app . AIConfig , model string , messages [ ] map [ string ] string , preset * app . AIPreset , streamChan chan string ) ( string , error ) {
// 不设 Timeout: 生命周期由调用方传入的 ctx 控制(客户端断连时自动取消)
client := & http . Client { }
if model == "" {
model = config . DefaultModel
@@ -1025,7 +984,7 @@ func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model st
}
endpoint := config . BaseURL + "/chat/completions"
req , err := http . NewRequest( "POST" , endpoint , bytes . NewBuffer ( bodyBytes ) )
req , err := http . NewRequestWithContext ( ctx , "POST" , endpoint , bytes . NewBuffer ( bodyBytes ) )
if err != nil {
return "" , fmt . Errorf ( "创建请求失败: %v" , err )
}
@@ -1035,6 +994,10 @@ func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model st
resp , err := client . Do ( req )
if err != nil {
// 客户端主动断开时 ctx 被取消,不算真正的错误
if ctx . Err ( ) != nil {
return "" , nil
}
return "" , fmt . Errorf ( "请求失败: %v" , err )
}
defer resp . Body . Close ( )
@@ -1050,49 +1013,51 @@ func (s *ConversationService) callOpenAIAPIStream(config *app.AIConfig, model st
for {
line , err := reader . ReadString ( '\n' )
// 先处理本次读到的数据( EOF 时可能仍携带最后一行内容)
if line != "" {
trimmed := strings . TrimSpace ( line )
if trimmed != "" && trimmed != "data: [DONE]" && strings . HasPrefix ( trimmed , "data: " ) {
data := strings . TrimPrefix ( trimmed , "data: " )
var streamResp struct {
Choices [ ] struct {
Delta struct {
Content string ` json:"content" `
} ` json:"delta" `
} ` json:"choices" `
}
if jsonErr := json . Unmarshal ( [ ] byte ( data ) , & streamResp ) ; jsonErr == nil {
if len ( streamResp . Choices ) > 0 {
content := streamResp . Choices [ 0 ] . Delta . Content
if content != "" {
fullContent . WriteString ( content )
streamChan <- content
}
}
}
}
}
// 再检查读取错误
if err != nil {
if err == io . EOF {
break
}
// ctx 被取消(客户端断开)时不算真正的流读取错误
if ctx . Err ( ) != nil {
return fullContent . String ( ) , nil
}
return "" , fmt . Errorf ( "读取流失败: %v" , err )
}
line = strings . TrimSpace ( line )
if line == "" || line == "data: [DONE]" {
continue
}
if strings . HasPrefix ( line , "data: " ) {
data := strings . TrimPrefix ( line , "data: " )
var streamResp struct {
Choices [ ] struct {
Delta struct {
Content string ` json:"content" `
} ` json:"delta" `
} ` json:"choices" `
}
if err := json . Unmarshal ( [ ] byte ( data ) , & streamResp ) ; err != nil {
continue
}
if len ( streamResp . Choices ) > 0 {
content := streamResp . Choices [ 0 ] . Delta . Content
if content != "" {
fullContent . WriteString ( content )
streamChan <- content
}
}
}
}
return fullContent . String ( ) , nil
}
// callAnthropicAPIStream 调用 Anthropic API 流式传输
func ( s * ConversationService ) callAnthropicAPIStream ( config * app . AIConfig , model string , messages [ ] map [ string ] string , systemPrompt string , preset * app . AIPreset , streamChan chan string ) ( string , error ) {
client := & http . Client { Timeout : 120 * time . Second }
func ( s * ConversationService ) callAnthropicAPIStream ( ctx context . Context , config * app . AIConfig , model string , messages [ ] map [ string ] string , systemPrompt string , preset * app . AIPreset , streamChan chan string ) ( string , error ) {
// 不设 Timeout: 生命周期由调用方传入的 ctx 控制(客户端断连时自动取消)
client := & http . Client { }
if model == "" {
model = config . DefaultModel
@@ -1152,7 +1117,7 @@ func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model
}
endpoint := config . BaseURL + "/messages"
req , err := http . NewRequest( "POST" , endpoint , bytes . NewBuffer ( bodyBytes ) )
req , err := http . NewRequestWithContext ( ctx , "POST" , endpoint , bytes . NewBuffer ( bodyBytes ) )
if err != nil {
return "" , fmt . Errorf ( "创建请求失败: %v" , err )
}
@@ -1163,6 +1128,10 @@ func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model
resp , err := client . Do ( req )
if err != nil {
// 客户端主动断开时 ctx 被取消,不算真正的错误
if ctx . Err ( ) != nil {
return "" , nil
}
return "" , fmt . Errorf ( "请求失败: %v" , err )
}
defer resp . Body . Close ( )
@@ -1178,38 +1147,39 @@ func (s *ConversationService) callAnthropicAPIStream(config *app.AIConfig, model
for {
line , err := reader . ReadString ( '\n' )
// 先处理本次读到的数据( EOF 时可能仍携带最后一行内容)
if line != "" {
trimmed := strings . TrimSpace ( line )
if trimmed != "" && strings . HasPrefix ( trimmed , "data: " ) {
data := strings . TrimPrefix ( trimmed , "data: " )
var streamResp struct {
Type string ` json:"type" `
Delta struct {
Type string ` json:"type" `
Text string ` json:"text" `
} ` json:"delta" `
}
if jsonErr := json . Unmarshal ( [ ] byte ( data ) , & streamResp ) ; jsonErr == nil {
if streamResp . Type == "content_block_delta" && streamResp . Delta . Text != "" {
fullContent . WriteString ( streamResp . Delta . Text )
streamChan <- streamResp . Delta . Text
}
}
}
}
// 再检查读取错误
if err != nil {
if err == io . EOF {
break
}
// ctx 被取消(客户端断开)时不算真正的流读取错误
if ctx . Err ( ) != nil {
return fullContent . String ( ) , nil
}
return "" , fmt . Errorf ( "读取流失败: %v" , err )
}
line = strings . TrimSpace ( line )
if line == "" {
continue
}
if strings . HasPrefix ( line , "data: " ) {
data := strings . TrimPrefix ( line , "data: " )
var streamResp struct {
Type string ` json:"type" `
Delta struct {
Type string ` json:"type" `
Text string ` json:"text" `
} ` json:"delta" `
}
if err := json . Unmarshal ( [ ] byte ( data ) , & streamResp ) ; err != nil {
continue
}
if streamResp . Type == "content_block_delta" && streamResp . Delta . Text != "" {
fullContent . WriteString ( streamResp . Delta . Text )
streamChan <- streamResp . Delta . Text
}
}
}
return fullContent . String ( ) , nil
@@ -1243,19 +1213,16 @@ func (s *ConversationService) RegenerateMessage(userID, conversationID uint) (*r
} )
}
// 获取删除后的消息历史
// 获取删除后的完整 消息历史( context 管理由 callAIService 内部处理)
var messages [ ] app . Message
err = global . GVA_DB . Where ( "conversation_id = ?" , conversationID ) .
Order ( "created_at DE SC" ) . Limit ( 10 ) . Find( & messages ) . Error
Order ( "created_at A SC" ) . Find ( & messages ) . Error
if err != nil {
return nil , err
}
if len ( messages ) == 0 {
return nil , errors . New ( "没有可用的消息历史" )
}
for i , j := 0 , len ( messages ) - 1 ; i < j ; i , j = i + 1 , j - 1 {
messages [ i ] , messages [ j ] = messages [ j ] , messages [ i ]
}
aiResponse , err := s . callAIService ( conversation , character , messages )
if err != nil {
@@ -1282,7 +1249,7 @@ func (s *ConversationService) RegenerateMessage(userID, conversationID uint) (*r
}
// RegenerateMessageStream 流式重新生成最后一条 AI 回复
func ( s * ConversationService ) RegenerateMessageStream ( userID , conversationID uint , streamChan chan string , doneChan chan bool ) error {
func ( s * ConversationService ) RegenerateMessageStream ( ctx context . Context , userID, conversationID uint , streamChan chan string , doneChan chan bool ) error {
defer close ( streamChan )
defer close ( doneChan )
@@ -1312,19 +1279,16 @@ func (s *ConversationService) RegenerateMessageStream(userID, conversationID uin
} )
}
// 获取删除后的消息历史
// 获取删除后的完整 消息历史( context 管理由 buildAPIMessagesWithContextManagement 处理)
var messages [ ] app . Message
err = global . GVA_DB . Where ( "conversation_id = ?" , conversationID ) .
Order ( "created_at DE SC" ) . Limit ( 10 ) . Find( & messages ) . Error
Order ( "created_at A SC" ) . Find ( & messages ) . Error
if err != nil {
return err
}
if len ( messages ) == 0 {
return errors . New ( "没有可用的消息历史" )
}
for i , j := 0 , len ( messages ) - 1 ; i < j ; i , j = i + 1 , j - 1 {
messages [ i ] , messages [ j ] = messages [ j ] , messages [ i ]
}
// 获取 AI 配置
var aiConfig app . AIConfig
@@ -1367,11 +1331,21 @@ func (s *ConversationService) RegenerateMessageStream(userID, conversationID uin
}
}
systemPrompt := s . buildSystemPrompt ( character )
if p reset != nil && preset . SystemPrompt != "" {
systemPrompt = systemPrompt + "\n\n" + preset . SystemPrompt
// 构建消息列表(含 context 预算管理)
var regenPresetSysPrompt string
if preset != nil {
regenPresetSysPrompt = preset . SystemPrompt
}
regenWbEngine := & WorldbookEngine { }
apiMessages := s . buildAPIMessagesWithContextManagement (
messages , character , regenPresetSysPrompt , regenWbEngine , conversation , & aiConfig , preset ,
)
// 从 apiMessages 中提取 systemPrompt, 供 Anthropic 独立参数使用
systemPrompt := ""
if len ( apiMessages ) > 0 && apiMessages [ 0 ] [ "role" ] == "system" {
systemPrompt = apiMessages [ 0 ] [ "content" ]
}
apiMessages := s . buildAPIMessages ( messages , systemPrompt )
model := aiConfig . DefaultModel
if model == "" {
@@ -1384,14 +1358,27 @@ func (s *ConversationService) RegenerateMessageStream(userID, conversationID uin
var fullContent string
switch aiConfig . Provider {
case "openai" , "custom" :
fullContent , err = s . callOpenAIAPIStream ( & aiConfig , model , apiMessages , preset , streamChan )
fullContent , err = s . callOpenAIAPIStream ( ctx , & aiConfig , model , apiMessages , preset , streamChan )
case "anthropic" :
fullContent , err = s . callAnthropicAPIStream ( & aiConfig , model , apiMessages , systemPrompt , preset , streamChan )
fullContent , err = s . callAnthropicAPIStream ( ctx , & aiConfig , model , apiMessages , systemPrompt , preset , streamChan )
default :
return fmt . Errorf ( "不支持的 AI 提供商: %s" , aiConfig . Provider )
}
if err != nil {
// AI 调用失败,恢复刚才删除的 assistant 消息,避免数据永久丢失
if lastAssistantMsg . ID > 0 {
if restoreErr := global . GVA_DB . Unscoped ( ) . Model ( & lastAssistantMsg ) . Update ( "deleted_at" , nil ) . Error ; restoreErr != nil {
global . GVA_LOG . Error ( fmt . Sprintf ( "[重新生成] 恢复 assistant 消息失败: %v" , restoreErr ) )
} else {
// 回滚 conversation 统计
global . GVA_DB . Model ( & conversation ) . Updates ( map [ string ] interface { } {
"message_count" : gorm . Expr ( "message_count + 1" ) ,
"token_count" : gorm . Expr ( "token_count + ?" , lastAssistantMsg . TokenCount ) ,
} )
global . GVA_LOG . Info ( "[重新生成] 已恢复 assistant 消息" )
}
}
return err
}
@@ -1437,9 +1424,333 @@ func (s *ConversationService) buildAPIMessages(messages []app.Message, systemPro
return apiMessages
}
// estimateTokens 粗略估算文本的 token 数(字符数 / 3, 适用于中英混合文本)
func estimateTokens ( text string ) int {
if text == "" {
return 0
}
// 中文字符约 1 char = 1 token, 英文约 4 chars = 1 token
// 取中间值 1 char ≈ 0.75 token, 即 chars * 4 / 3 的倒数 ≈ chars / 1.5
// 保守估算用 chars / 2 防止超出
n := len ( [ ] rune ( text ) )
return ( n + 1 ) / 2
}
// contextConfig 保存从 AIConfig.Settings 中解析出的上下文配置
type contextConfig struct {
contextLength int // 模型上下文窗口大小( token 数)
maxTokens int // 最大输出 token 数
}
// getContextConfig 从 AIConfig 中读取上下文配置,如果没有配置则使用默认值
func getContextConfig ( aiConfig * app . AIConfig , preset * app . AIPreset ) contextConfig {
cfg := contextConfig {
contextLength : 200000 , // 保守默认值
maxTokens : 2000 ,
}
// 从 preset 读取 max_tokens
if preset != nil && preset . MaxTokens > 0 {
cfg . maxTokens = preset . MaxTokens
}
// 从 AIConfig.Settings 读取 context_length
if len ( aiConfig . Settings ) > 0 {
var settings map [ string ] interface { }
if err := json . Unmarshal ( aiConfig . Settings , & settings ) ; err == nil {
if cl , ok := settings [ "context_length" ] . ( float64 ) ; ok && cl > 0 {
cfg . contextLength = int ( cl )
}
}
}
return cfg
}
// buildContextManagedSystemPrompt 按优先级构建 system prompt, 超出 budget 时截断低优先级内容
// 优先级(从高到低):
// 1. 核心人设( Name/Description/Personality/Scenario/SystemPrompt)
// 2. Preset.SystemPrompt
// 3. Worldbook 触发条目
// 4. CharacterBook 内嵌条目
// 5. MesExample( 对话示例, 最容易被截断)
//
// 返回构建好的 systemPrompt 以及消耗的 token 数
func ( s * ConversationService ) buildContextManagedSystemPrompt (
character app . AICharacter ,
presetSystemPrompt string ,
worldbookEngine * WorldbookEngine ,
conversation app . Conversation ,
messageContents [ ] string ,
budget int ,
) ( string , int ) {
used := 0
// ── 优先级1: 核心人设 ─────────────────────────────────────────────
core := fmt . Sprintf ( "你是 %s。" , character . Name )
if character . Description != "" {
core += fmt . Sprintf ( "\n\n描述: %s" , character . Description )
}
if character . Personality != "" {
core += fmt . Sprintf ( "\n\n性格: %s" , character . Personality )
}
if character . Scenario != "" {
core += fmt . Sprintf ( "\n\n场景: %s" , character . Scenario )
}
if character . SystemPrompt != "" {
core += fmt . Sprintf ( "\n\n系统提示: %s" , character . SystemPrompt )
}
core += "\n\n请根据以上设定进行角色扮演, 保持角色的性格和说话方式。"
core = s . applyMacroVariables ( core , character )
coreTokens := estimateTokens ( core )
if coreTokens >= budget {
// 极端情况:核心人设本身就超出 budget, 截断到 budget
runes := [ ] rune ( core )
limit := budget * 2
if limit > len ( runes ) {
limit = len ( runes )
}
core = string ( runes [ : limit ] )
global . GVA_LOG . Warn ( fmt . Sprintf ( "[context] 核心人设超出 budget, 已截断至 %d chars" , limit ) )
return core , budget
}
used += coreTokens
prompt := core
// ── 优先级2: Preset.SystemPrompt ────────────────────────────────
if presetSystemPrompt != "" {
tokens := estimateTokens ( presetSystemPrompt )
if used + tokens <= budget {
prompt += "\n\n" + presetSystemPrompt
used += tokens
} else {
// 尝试部分插入
remaining := budget - used
if remaining > 50 {
runes := [ ] rune ( presetSystemPrompt )
limit := remaining * 2
if limit > len ( runes ) {
limit = len ( runes )
}
prompt += "\n\n" + string ( runes [ : limit ] )
used = budget
}
global . GVA_LOG . Warn ( fmt . Sprintf ( "[context] Preset.SystemPrompt 因 budget 不足被截断(需要 %d tokens, 剩余 %d) " , tokens , budget - used ) )
}
}
if used >= budget {
return prompt , used
}
// ── 优先级3: 世界书触发条目 ──────────────────────────────────────
if conversation . WorldbookEnabled && conversation . WorldbookID != nil && worldbookEngine != nil {
triggeredEntries , wbErr := worldbookEngine . ScanAndTrigger ( * conversation . WorldbookID , messageContents )
if wbErr != nil {
global . GVA_LOG . Warn ( fmt . Sprintf ( "[context] 世界书触发失败: %v" , wbErr ) )
} else if len ( triggeredEntries ) > 0 {
wbHeader := "\n\n[World Information]"
wbSection := wbHeader
for _ , te := range triggeredEntries {
if te . Entry == nil || te . Entry . Content == "" {
continue
}
line := fmt . Sprintf ( "\n- %s" , te . Entry . Content )
lineTokens := estimateTokens ( line )
if used + estimateTokens ( wbSection ) + lineTokens <= budget {
wbSection += line
used += lineTokens
} else {
global . GVA_LOG . Warn ( fmt . Sprintf ( "[context] 世界书条目 (id=%d) 因 budget 不足被跳过" , te . Entry . ID ) )
break
}
}
if wbSection != wbHeader {
prompt += wbSection
}
}
}
if used >= budget {
return prompt , used
}
// ── 优先级4: CharacterBook 内嵌条目 ──────────────────────────────
if len ( character . CharacterBook ) > 0 {
var characterBook map [ string ] interface { }
if err := json . Unmarshal ( character . CharacterBook , & characterBook ) ; err == nil {
if entries , ok := characterBook [ "entries" ] . ( [ ] interface { } ) ; ok && len ( entries ) > 0 {
cbSection := "\n\n世界设定: "
addedAny := false
for _ , entry := range entries {
entryMap , ok := entry . ( map [ string ] interface { } )
if ! ok {
continue
}
enabled := true
if enabledVal , ok := entryMap [ "enabled" ] . ( bool ) ; ok {
enabled = enabledVal
}
if ! enabled {
continue
}
content , ok := entryMap [ "content" ] . ( string )
if ! ok || content == "" {
continue
}
line := fmt . Sprintf ( "\n- %s" , content )
lineTokens := estimateTokens ( line )
if used + estimateTokens ( cbSection ) + lineTokens <= budget {
cbSection += line
used += lineTokens
addedAny = true
} else {
global . GVA_LOG . Warn ( "[context] CharacterBook 条目因 budget 不足被跳过" )
break
}
}
if addedAny {
prompt += cbSection
}
}
}
}
if used >= budget {
return prompt , used
}
// ── 优先级5: MesExample( 对话示例, 最低优先级) ──────────────────
if character . MesExample != "" {
mesTokens := estimateTokens ( character . MesExample )
prefix := "\n\n对话示例: \n"
prefixTokens := estimateTokens ( prefix )
if used + prefixTokens + mesTokens <= budget {
prompt += prefix + character . MesExample
used += prefixTokens + mesTokens
} else {
// 尝试截断 MesExample
remaining := budget - used - prefixTokens
if remaining > 100 {
runes := [ ] rune ( character . MesExample )
limit := remaining * 2
if limit > len ( runes ) {
limit = len ( runes )
}
prompt += prefix + string ( runes [ : limit ] )
used = budget
global . GVA_LOG . Warn ( fmt . Sprintf ( "[context] MesExample 被截断(原始 %d tokens, 保留约 %d tokens) " , mesTokens , remaining ) )
} else {
global . GVA_LOG . Warn ( "[context] MesExample 因 budget 不足被完全跳过" )
}
}
}
return prompt , used
}
// trimMessagesToBudget 从历史消息中按 token 预算选取最近的消息
// 优先保留最新的消息,从后往前丢弃旧消息直到 token 数在 budget 内
func trimMessagesToBudget ( messages [ ] app . Message , budget int ) [ ] app . Message {
if budget <= 0 {
return nil
}
// messages 已经是从旧到新的顺序
// 从最新消息开始往前累加,直到超出 budget
selected := make ( [ ] app . Message , 0 , len ( messages ) )
used := 0
for i := len ( messages ) - 1 ; i >= 0 ; i -- {
msg := messages [ i ]
if msg . Role == "system" {
continue
}
t := estimateTokens ( msg . Content )
if used + t > budget {
global . GVA_LOG . Warn ( fmt . Sprintf ( "[context] 历史消息已截断,保留最近 %d 条(共 %d 条),使用 %d tokens" , len ( selected ) , len ( messages ) , used ) )
break
}
used += t
selected = append ( [ ] app . Message { msg } , selected ... ) // 保持时序
}
return selected
}
// buildAPIMessagesWithContextManagement 整合 context 管理,构建最终的 messages 列表
// 返回 apiMessages 及各部分 token 统计日志
func ( s * ConversationService ) buildAPIMessagesWithContextManagement (
allMessages [ ] app . Message ,
character app . AICharacter ,
presetSystemPrompt string ,
worldbookEngine * WorldbookEngine ,
conversation app . Conversation ,
aiConfig * app . AIConfig ,
preset * app . AIPreset ,
) [ ] map [ string ] string {
cfg := getContextConfig ( aiConfig , preset )
// 安全边际:为输出保留 max_tokens, 另加 200 token 缓冲
inputBudget := cfg . contextLength - cfg . maxTokens - 200
if inputBudget <= 0 {
inputBudget = cfg . contextLength / 2
}
// 为历史消息分配预算: system prompt 最多占用 60% 的 input budget
systemBudget := inputBudget * 60 / 100
historyBudget := inputBudget - systemBudget
// 提取消息内容用于世界书扫描
var messageContents [ ] string
for _ , msg := range allMessages {
messageContents = append ( messageContents , msg . Content )
}
// 构建 system prompt( 含 worldbook 注入,按优先级截断)
systemPrompt , systemTokens := s . buildContextManagedSystemPrompt (
character ,
presetSystemPrompt ,
worldbookEngine ,
conversation ,
messageContents ,
systemBudget ,
)
// 如果 system prompt 实际用量比预算少,把节省的预算让给历史消息
if systemTokens < systemBudget {
historyBudget += systemBudget - systemTokens
}
global . GVA_LOG . Info ( fmt . Sprintf ( "[context] 配置: context_length=%d, max_tokens=%d, input_budget=%d, system=%d tokens, history_budget=%d" ,
cfg . contextLength , cfg . maxTokens , inputBudget , systemTokens , historyBudget ) )
// 按 token 预算裁剪历史消息
trimmedMessages := trimMessagesToBudget ( allMessages , historyBudget )
// 构建最终 messages
apiMessages := make ( [ ] map [ string ] string , 0 , len ( trimmedMessages ) + 1 )
apiMessages = append ( apiMessages , map [ string ] string {
"role" : "system" ,
"content" : systemPrompt ,
} )
for _ , msg := range trimmedMessages {
if msg . Role == "system" {
continue
}
apiMessages = append ( apiMessages , map [ string ] string {
"role" : msg . Role ,
"content" : msg . Content ,
} )
}
return apiMessages
}
// callOpenAIAPI 调用 OpenAI API
func ( s * ConversationService ) callOpenAIAPI ( config * app . AIConfig , model string , messages [ ] map [ string ] string , preset * app . AIPreset ) ( string , error ) {
client := & http . Client { Timeout : 12 0 * time . Second }
client := & http . Client { Timeout : 10 * time . Minute }
// 使用配置的模型或默认模型
if model == "" {
@@ -1559,7 +1870,7 @@ func (s *ConversationService) callOpenAIAPI(config *app.AIConfig, model string,
// callAnthropicAPI 调用 Anthropic API
func ( s * ConversationService ) callAnthropicAPI ( config * app . AIConfig , model string , messages [ ] map [ string ] string , systemPrompt string , preset * app . AIPreset ) ( string , error ) {
client := & http . Client { Timeout : 12 0 * time . Second }
client := & http . Client { Timeout : 10 * time . Minute }
// 使用配置的模型或默认模型
if model == "" {