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.Debugw("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" }