Files
Cyrene/backend/ai-core/internal/subsession/review_provider.go
T
AskaEth a058b0ab8e fix: 第一轮修复 - 记忆管理/IoT操控/历史消息持久化/动作消息/链路优化/安全配置
- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复)
- 修复IoT子会话工具调用链路日志缺失
- 新增最终审查子会话(review_provider) 支持消息格式解析拆分
- 实现历史消息持久化(后端存储+前端分页加载)
- 前端新增动作消息(ActionMessage)类型和渲染
- 优化对话链路速度(非阻塞子会话+快速问候通道)
- JWT密钥环境变量化(无默认值启动panic)
- Token自动刷新机制(401拦截器+refresh接口)
- WebSocket指数退避重连(jitter+最大10次)
- localStorage清理一致性(cyrene_前缀+版本检查)
- IoT环境变量统一为IOT_SERVICE_URL
2026-05-21 23:10:07 +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"
"log"
"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)
log.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
}