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