package subsession import ( "context" "fmt" "git.yeij.top/AskaEth/Cyrene/pkg/logger" "regexp" "strings" "time" "git.yeij.top/AskaEth/Cyrene/ai-core/internal/model" ) // ReviewProvider 最终审查子会话提供者 // 职责:解析编排器输出文本,将其拆分为带类型的消息(action/chat), // 分割长消息为短消息,输出格式化的消息列表供前端渲染。 type ReviewProvider struct{} // NewReviewProvider 创建审查子会话提供者 func NewReviewProvider() *ReviewProvider { return &ReviewProvider{} } func (p *ReviewProvider) Type() model.SubSessionType { return model.SubSessionReview } func (p *ReviewProvider) CanHandle(_ context.Context, _ *model.IntentResult, _ string) bool { // 审查提供者始终可用于处理综合后的文本 return true } func (p *ReviewProvider) Priority() int { return 1 // 最高优先级,最先处理输出 } func (p *ReviewProvider) Timeout() time.Duration { return 5 * time.Second // 审查很快,无需长时间 } func (p *ReviewProvider) CreateContext(_ context.Context, params CreateContextParams) ([]model.LLMMessage, error) { // Review 不依赖 LLM 上下文,直接处理文本 return []model.LLMMessage{ {Role: model.RoleSystem, Content: "最终审查子会话 - 格式化输出"}, }, nil } func (p *ReviewProvider) Execute(_ context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error) { // 提取待审查的文本(从最后一条 user 消息中获取,由 Orchestrator 注入) text := "" for i := len(subCtx) - 1; i >= 0; i-- { if subCtx[i].Role == model.RoleUser { text = subCtx[i].Content break } } if text == "" { return &model.SubSessionResult{ Type: model.SubSessionReview, Summary: "(无需审查,文本为空)", }, nil } reviewMessages := parseReviewText(text) logger.Printf("[review-provider] 审查完成: 输入 %d 字符 → %d 条消息", len([]rune(text)), len(reviewMessages)) // 构建摘要 var parts []string for _, rm := range reviewMessages { typeLabel := "💬" if rm.Type == model.ReviewMessageAction { typeLabel = "⚡" } runes := []rune(rm.Content) preview := rm.Content if len(runes) > 30 { preview = string(runes[:30]) + "..." } parts = append(parts, fmt.Sprintf("%s %s", typeLabel, preview)) } result := &model.SubSessionResult{ Type: model.SubSessionReview, Summary: fmt.Sprintf("审查完成: %d 条消息", len(reviewMessages)), Details: strings.Join(parts, "\n"), Confidence: 0.95, Metadata: map[string]any{ "review_messages": reviewMessages, }, } return result, nil } // parseReviewText 解析原始文本,提取带类型的消息 // 规则: // - (xxx)或 (xxx) → action 类型消息 // - "xxx" 或 "xxx" → chat 类型消息(提取引号内容) // - 普通文本 → chat 类型消息 // - 长消息 (>80 字符) → 按句子边界拆分为多条 func parseReviewText(text string) []model.ReviewMessage { if text == "" { return nil } var messages []model.ReviewMessage // 模式1: 匹配括号内容作为 action — (...)或 (...) actionPattern := regexp.MustCompile(`[((]([^))]+)[))]`) // 模式2: 匹配引号内容 — "..." quotePattern := regexp.MustCompile(`[""]([^""]+)[""]`) // 模式3: 匹配方括号动作 — 【...】 bracketPattern := regexp.MustCompile(`【([^】]+)】`) // 先收集所有匹配的位置 type matchRange struct { start int end int typ model.ReviewMessageType text string } var matches []matchRange // 收集括号动作 for _, m := range actionPattern.FindAllStringSubmatchIndex(text, -1) { matches = append(matches, matchRange{ start: m[0], end: m[1], typ: model.ReviewMessageAction, text: text[m[2]:m[3]], // 括号内文本 }) } // 收集方括号动作 for _, m := range bracketPattern.FindAllStringSubmatchIndex(text, -1) { matches = append(matches, matchRange{ start: m[0], end: m[1], typ: model.ReviewMessageAction, text: text[m[2]:m[3]], }) } // 收集引号内容 for _, m := range quotePattern.FindAllStringSubmatchIndex(text, -1) { matches = append(matches, matchRange{ start: m[0], end: m[1], typ: model.ReviewMessageChat, text: text[m[2]:m[3]], }) } // 如果没有匹配,整个文本作为 chat if len(matches) == 0 { return splitLongMessage(model.ReviewMessageChat, strings.TrimSpace(text)) } // 简单排序(按出现顺序) for i := 0; i < len(matches); i++ { for j := i + 1; j < len(matches); j++ { if matches[i].start > matches[j].start { matches[i], matches[j] = matches[j], matches[i] } } } // 处理匹配之间的普通文本 pos := 0 for _, m := range matches { // 匹配前的普通文本 if m.start > pos { plainText := strings.TrimSpace(text[pos:m.start]) if plainText != "" { messages = append(messages, splitLongMessage(model.ReviewMessageChat, plainText)...) } } // 添加匹配项 messages = append(messages, model.ReviewMessage{ Type: m.typ, Content: strings.TrimSpace(m.text), }) pos = m.end } // 剩余文本 if pos < len(text) { remaining := strings.TrimSpace(text[pos:]) if remaining != "" { messages = append(messages, splitLongMessage(model.ReviewMessageChat, remaining)...) } } if len(messages) == 0 { messages = append(messages, model.ReviewMessage{ Type: model.ReviewMessageChat, Content: strings.TrimSpace(text), }) } return messages } // splitLongMessage 将长消息按句子边界拆分为多条短消息 func splitLongMessage(msgType model.ReviewMessageType, text string) []model.ReviewMessage { const maxLen = 80 // 最大字符数(按 rune 计数) runes := []rune(text) if len(runes) <= maxLen { return []model.ReviewMessage{{Type: msgType, Content: text}} } var messages []model.ReviewMessage start := 0 for start < len(runes) { end := start + maxLen if end > len(runes) { end = len(runes) } // 尝试在句子边界处分割 chunk := string(runes[start:end]) // 如果这不是最后一个 chunk,在句子边界处切割 if end < len(runes) { // 从后往前找最近的句子分隔符 lastSentenceBreak := -1 for i := len(chunk) - 1; i >= len(chunk)/2; i-- { ch := runes[start+i] if ch == '。' || ch == '!' || ch == '?' || ch == '.' || ch == '!' || ch == '?' || ch == ';' || ch == ';' || ch == '\n' { lastSentenceBreak = i break } } // 如果没有找到句子分隔符,找逗号或空格 if lastSentenceBreak < 0 { for i := len(chunk) - 1; i >= len(chunk)/2; i-- { ch := runes[start+i] if ch == ',' || ch == ',' || ch == ' ' || ch == ' ' { lastSentenceBreak = i break } } } if lastSentenceBreak > 0 { chunk = string(runes[start : start+lastSentenceBreak+1]) end = start + lastSentenceBreak + 1 } } chunk = strings.TrimSpace(chunk) if chunk != "" { messages = append(messages, model.ReviewMessage{ Type: msgType, Content: chunk, }) } start = end } if len(messages) == 0 { messages = append(messages, model.ReviewMessage{ Type: msgType, Content: text, }) } return messages }