Files
Cyrene/backend/ai-core/internal/subsession/review_provider.go
T
AskaEth 87214b9441 feat: Phase 1+2 架构进化 — 连续思考链/主动消息决策/情感状态机/离线自主思考 (86文件)
Phase 1 (基础设施):
- ThinkChain 思考链连续性 + 差异化思考提示词 (persistent)
- AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional)
- MessageScheduler 自适应消息节奏 (Idle/Available/Busy)
- SessionEnrichmentStore 渐进式上下文丰富 (5层)
- ConversationBus 事件总线 + ResponseCache (dedup)
- pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf
- NPE 守卫/链路优化/数据库表修复/Go workspace

Phase 2 (人格交互):
- EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减)
- ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验)
- Gateway↔ai-core 在线状态感知链路 (presence notification)
- 离线思考频率控制 + 重连问候 + 离线消息排队

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:25:12 +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"
"github.com/yourname/cyrene-ai/pkg/logger"
"regexp"
"strings"
"time"
"github.com/yourname/cyrene-ai/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
}