71f0a1abdb
- 所有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>
277 lines
7.1 KiB
Go
277 lines
7.1 KiB
Go
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
|
||
}
|