Files
CamTalk/backend/internal/eino/nodes_history.go
hhs 76d331c885 feat: Eino nodes 迁移到 trace 包(Phase 6.2)
- nodes_stt.go 使用 trace.FromContext 替换 logger.Log
- nodes_history.go 使用 trace.FromContext
- nodes_tts.go 使用 trace.FromContext
- nodes_done.go 使用 trace.FromContext
- 移除所有 nodes 中的 request_id 手动字段(自动附加)
- 所有日志消息改为英文
2026-06-21 22:36:15 +08:00

152 lines
4.5 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 eino
import (
"context"
"encoding/base64"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
"github.com/hhs/camtalk/internal/ai/llm"
"github.com/hhs/camtalk/internal/models"
"github.com/hhs/camtalk/internal/store"
"github.com/hhs/camtalk/internal/trace"
)
// NewHistoryLambda 创建历史组装 Lambda 节点。
// 输入: *STTOutput → 输出: []*schema.Message
//
// 从 PipelineState 读取请求元数据SessionID、Scenario、ImageData 等),
// 构建系统提示词,组装历史消息和当前用户输入(含多模态图片)。
func NewHistoryLambda(
historyFetcher func(ctx context.Context, sessionID string, limit int) ([]models.Message, error),
scenarioRepo store.UserScenarioRepository,
maxHistory int,
) *compose.Lambda {
return compose.InvokableLambda(func(ctx context.Context, sttOut STTOutput) ([]*schema.Message, error) {
log := trace.FromContext(ctx)
// 从 State 读取请求元数据
state := stateFromCtx(ctx)
if state == nil {
return []*schema.Message{}, nil
}
state.mu.Lock()
sessionID := state.SessionID
requestID := state.RequestID
imageData := state.ImageData
scenario := state.Scenario
detailLevel := state.DetailLevel
language := sttOut.Language
userID := state.UserID
state.mu.Unlock()
// 加载用户自建情景(如果有 userID 和 scenarioRepo
var customScenarios map[string]string
var customGreetings map[string]string
if userID != "" && scenarioRepo != nil {
scenarios, err := scenarioRepo.FindByUserID(ctx, userID)
if err != nil {
log.Warnw("load user scenarios failed", "user_id", userID, "error", err)
} else if len(scenarios) > 0 {
customScenarios = make(map[string]string, len(scenarios))
customGreetings = make(map[string]string, len(scenarios))
for _, s := range scenarios {
customScenarios[s.ID] = s.Prompt
if s.Greeting != "" {
customGreetings[s.ID] = s.Greeting
}
}
log.Debugw("loaded user scenarios", "user_id", userID, "count", len(scenarios))
}
}
// 构建系统提示词(支持用户自建情景)
scenarioPrompt := llm.GetScenarioPrompt(scenario, language, customScenarios)
systemPrompt := llm.BuildSystemPrompt(language, detailLevel, scenarioPrompt)
// 构建 system message仅文本多模态内容只能放在 user 角色)
systemMsg := &schema.Message{
Role: schema.System,
Content: systemPrompt,
}
messages := []*schema.Message{systemMsg}
// 获取并追加历史消息
if historyFetcher != nil && sessionID != "" {
history, err := historyFetcher(ctx, sessionID, maxHistory)
if err != nil {
log.Warnw("fetch history failed, continuing", "error", err, "request_id", requestID)
} else {
for _, msg := range history {
messages = append(messages, &schema.Message{
Role: schema.RoleType(msg.Role),
Content: msg.Content,
})
}
}
}
// 追加当前用户输入(含图片,多模态内容只能放在 user 角色)
// 注意:不能同时设置 Content 和 UserInputMultiContent需要统一放到 MultiContent 中
if len(imageData) > 0 {
base64Str := base64.StdEncoding.EncodeToString(imageData)
mimeType := detectImageMimeType(imageData)
parts := []schema.MessageInputPart{
{
Type: schema.ChatMessagePartTypeText,
Text: sttOut.Text,
},
{
Type: schema.ChatMessagePartTypeImageURL,
Image: &schema.MessageInputImage{
MessagePartCommon: schema.MessagePartCommon{
Base64Data: &base64Str,
MIMEType: mimeType,
},
Detail: schema.ImageURLDetailAuto,
},
},
}
messages = append(messages, &schema.Message{
Role: schema.User,
UserInputMultiContent: parts,
})
} else {
messages = append(messages, &schema.Message{
Role: schema.User,
Content: sttOut.Text,
})
}
log.Infow("history assembled",
"message_count", len(messages),
"has_image", len(imageData) > 0,
"scenario", scenario)
return messages, nil
})
}
// detectImageMimeType 简单检测图片 MIME 类型。
func detectImageMimeType(data []byte) string {
if len(data) < 4 {
return "image/jpeg"
}
if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return "image/jpeg"
}
if data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 {
return "image/png"
}
if data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46 {
return "image/gif"
}
if data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 {
return "image/webp"
}
return "image/jpeg"
}