feat: 实现 Eino Graph 构建与 Orchestrator 适配器,切换 main.go

- graph.go: 构建 Graph 拓扑 START→STT→History→ChatModel→Splitter→TTS→Done→END
  - 创建 eino-ext ChatModel 对接 DashScope OpenAI 兼容接口
  - 统一使用值类型(PipelineInput/PipelineOutput)
  - Callback 在运行时通过 Stream option 传入
- adapter.go: EinoOrchestrator 实现 orchestrator.Orchestrator 接口
  - 解码 base64 音频/图片,注入 context 值
  - 调用 Graph.Stream() 触发惰性执行并消费输出
  - 追加用户/助手消息到历史
- main.go: 移除旧 llmService + orchestrator.New()
  替换为 eino.NewPipelineGraph() + eino.NewEinoOrchestrator()
- 各节点统一使用值类型,State 传递请求元数据

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-19 21:58:17 +08:00
parent fd5c7712f8
commit 4b731b5ac0
9 changed files with 395 additions and 98 deletions

View File

@@ -8,49 +8,47 @@ import (
"github.com/hhs/camtalk/internal/logger"
"github.com/hhs/camtalk/internal/models"
"github.com/hhs/camtalk/internal/session"
)
// ctxKeyStartTime 请求开始时间的 context key。
type ctxKeyStartTime struct{}
// WithStartTime 将请求开始时间注入 context。
func WithStartTime(ctx context.Context, t time.Time) context.Context {
return context.WithValue(ctx, ctxKeyStartTime{}, t)
}
// latencyFromCtx 从 context 获取开始时间并计算延迟(毫秒)。
func latencyFromCtx(ctx context.Context) int64 {
if startTime, ok := ctx.Value(ctxKeyStartTime{}).(time.Time); ok {
return time.Since(startTime).Milliseconds()
}
return 0
}
// NewDoneLambda 创建 Done Lambda 节点。
// 输入: struct{}TTS 完成信号)→ 输出: PipelineOutput
// 输入: struct{}TTS 完成信号)→ 输出: *PipelineOutput
//
// 从 PipelineState 读取完整回复和 token 用量,发送 llm_done 到客户端
// 追加助手消息到会话历史,返回 PipelineOutput
func NewDoneLambda(sessionMgr session.Manager, model string) *compose.Lambda {
return compose.InvokableLambda(func(ctx context.Context, _ struct{}) (*PipelineOutput, error) {
// 从 PipelineState 读取完整回复和 token 用量,发送 llm_done 到客户端
// 历史消息追加由适配器负责(避免重复写入)
func NewDoneLambda(defaultModel string) *compose.Lambda {
return compose.InvokableLambda(func(ctx context.Context, _ struct{}) (PipelineOutput, error) {
log := logger.Log
sender := senderFromCtx(ctx)
requestID := requestIDFromCtx(ctx)
state := stateFromCtx(ctx)
if state == nil {
return &PipelineOutput{}, nil
return PipelineOutput{}, nil
}
state.mu.Lock()
fullResponse := state.FullResponse.String()
transcribedText := state.TranscribedText
tokenUsage := state.TokenUsage
modelName := model
requestID := state.RequestID
modelName := defaultModel
state.mu.Unlock()
// 追加助手消息到会话历史
sessionID := ""
if state != nil {
// 从 context 获取 sessionID由适配器注入
if sid, ok := ctx.Value(ctxKeySessionID{}).(string); ok {
sessionID = sid
}
}
if sessionID != "" && sessionMgr != nil && fullResponse != "" {
if err := sessionMgr.AppendMessage(ctx, sessionID, models.Message{
Role: "assistant",
Content: fullResponse,
}); err != nil {
log.Errorw("追加助手消息到历史失败", "error", err)
}
}
// 发送 llm_done
if sender != nil && requestID != "" {
done := models.WsLLMDone{
@@ -58,7 +56,7 @@ func NewDoneLambda(sessionMgr session.Manager, model string) *compose.Lambda {
RequestID: requestID,
FullText: fullResponse,
Model: modelName,
LatencyMs: 0, // 由适配器计算
LatencyMs: latencyFromCtx(ctx),
}
if tokenUsage != nil {
done.TokensUsed = struct {
@@ -78,9 +76,9 @@ func NewDoneLambda(sessionMgr session.Manager, model string) *compose.Lambda {
log.Infow("查询处理完成",
"request_id", requestID,
"text_length", len(fullResponse))
"response_length", len(fullResponse))
return &PipelineOutput{
return PipelineOutput{
TranscribedText: transcribedText,
FullResponse: fullResponse,
Model: modelName,
@@ -88,27 +86,3 @@ func NewDoneLambda(sessionMgr session.Manager, model string) *compose.Lambda {
}, nil
})
}
// ctxKeySessionID sessionID 的 context key。
type ctxKeySessionID struct{}
// WithSessionID 将 sessionID 注入 context。
func WithSessionID(ctx context.Context, sessionID string) context.Context {
return context.WithValue(ctx, ctxKeySessionID{}, sessionID)
}
// latencyFromCtx 从 context 获取开始时间并计算延迟。
func latencyFromCtx(ctx context.Context) int64 {
if startTime, ok := ctx.Value(ctxKeyStartTime{}).(time.Time); ok {
return time.Since(startTime).Milliseconds()
}
return 0
}
// ctxKeyStartTime 请求开始时间的 context key。
type ctxKeyStartTime struct{}
// WithStartTime 将请求开始时间注入 context。
func WithStartTime(ctx context.Context, t time.Time) context.Context {
return context.WithValue(ctx, ctxKeyStartTime{}, t)
}