Files
Cyrene/backend/ai-core/internal/subsession/review_provider.go
T
AskaEth 71f0a1abdb feat: Go模块路径迁移 + Docker生产部署适配 + ethend Docker兼容
- 所有Go模块路径从 github.com/yourname/cyrene-ai 迁移到 git.yeij.top/AskaEth/Cyrene
- 5个Go Dockerfile添加 GOPROXY=https://goproxy.cn,direct 解决国内构建问题
- ai-core go.mod 添加 pkg/plugins replace 指令
- Caddyfile 简化为 http:// 通配 + handle 保留 /api 前缀
- ethend Dockerfile 适配 (npm install + 仅 COPY package.json)
- ethend 新增 RUNNING_IN_DOCKER 环境变量,健康检查改用Docker服务名
- ethend 数据库状态检查支持Docker hostname (postgres/redis/qdrant/minio)
- process-manager 新增 CONTAINER_SVC_MAP + Docker模式自动检测
- 统一 docker-compose.dev.db.yml 卷名 (pg_data/redis_data/qdrant_data/minio_data)
- docker-compose.yml ethend服务挂载docker.sock + 端口变量化
- 清理 .env 统一后的残留文件与提示信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:43:22 +08:00

277 lines
7.1 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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
}