feat: 引入 Eino 框架并实现 AI 编排层基础设施与节点

- 引入 cloudwego/eino v0.9.9 和 eino-ext/components/model/openai v0.1.13
- 新增 internal/eino/ 包:
  - types.go: PipelineInput/Output、STTOutput、TokenUsage 类型定义
  - state.go: PipelineState 跨节点状态收集(线程安全)
  - callback.go: ChatModel OnEndWithStreamOutput 回调,逐 token 推送 llm_chunk
  - nodes_stt.go: STT Lambda,支持文本/语音输入模式
  - nodes_history.go: 历史组装 Lambda,含多模态图片支持
  - nodes_splitter.go: 句子分割 Transform Lambda
  - nodes_tts.go: TTS Lambda,逐句合成推送音频
  - nodes_done.go: Done Lambda,发送 llm_done 并追加历史

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-19 21:49:28 +08:00
parent b10a508356
commit fd5c7712f8
11 changed files with 946 additions and 24 deletions

View File

@@ -0,0 +1,117 @@
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/logger"
"github.com/hhs/camtalk/internal/models"
)
// HistoryInput 历史组装节点的输入,包含 STT 输出和原始请求信息。
type HistoryInput struct {
STTOutput *STTOutput
SessionID string
RequestID string
ImageData []byte
Scenario string
DetailLevel string
}
// NewHistoryLambda 创建历史组装 Lambda 节点。
// 输入: HistoryInput → 输出: []*schema.Message
//
// 构建系统提示词,组装历史消息和当前用户输入(含多模态图片)。
func NewHistoryLambda(historyFetcher func(ctx context.Context, sessionID string, maxHistory int) ([]models.Message, error), maxHistory int) *compose.Lambda {
return compose.InvokableLambda(func(ctx context.Context, input *HistoryInput) ([]*schema.Message, error) {
log := logger.Log
requestID := input.RequestID
// 构建系统提示词
scenarioPrompt := llm.GetScenarioPrompt(input.Scenario, input.STTOutput.Language)
systemPrompt := llm.BuildSystemPrompt(input.STTOutput.Language, input.DetailLevel, scenarioPrompt)
// 构建 system message含图片
systemMsg := &schema.Message{
Role: schema.System,
Content: systemPrompt,
}
// 如果有图片,添加到 system message 的多模态内容中
if len(input.ImageData) > 0 {
base64Str := base64.StdEncoding.EncodeToString(input.ImageData)
mimeType := detectImageMimeType(input.ImageData)
systemMsg.UserInputMultiContent = []schema.MessageInputPart{
{
Type: schema.ChatMessagePartTypeImageURL,
Image: &schema.MessageInputImage{
MessagePartCommon: schema.MessagePartCommon{
Base64Data: &base64Str,
MIMEType: mimeType,
},
Detail: schema.ImageURLDetailAuto,
},
},
}
}
messages := []*schema.Message{systemMsg}
// 获取并追加历史消息
if historyFetcher != nil && input.SessionID != "" {
history, err := historyFetcher(ctx, input.SessionID, maxHistory)
if err != nil {
log.Warnw("获取历史消息失败,继续处理", "error", err, "request_id", requestID)
} else {
for _, msg := range history {
messages = append(messages, &schema.Message{
Role: schema.RoleType(msg.Role),
Content: msg.Content,
})
}
}
}
// 追加当前用户输入
messages = append(messages, &schema.Message{
Role: schema.User,
Content: input.STTOutput.Text,
})
log.Infow("历史组装完成",
"request_id", requestID,
"message_count", len(messages),
"has_image", len(input.ImageData) > 0,
"scenario", input.Scenario)
return messages, nil
})
}
// detectImageMimeType 简单检测图片 MIME 类型。
func detectImageMimeType(data []byte) string {
if len(data) < 4 {
return "image/jpeg"
}
// JPEG: FF D8 FF
if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return "image/jpeg"
}
// PNG: 89 50 4E 47
if data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 {
return "image/png"
}
// GIF: 47 49 46 38
if data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46 {
return "image/gif"
}
// WebP: 52 49 46 46
if data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 {
return "image/webp"
}
return "image/jpeg" // 默认
}