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:
2026-05-21 23:10:07 +08:00
parent 8b7d4ec19a
commit a058b0ab8e
53 changed files with 5535 additions and 241 deletions
@@ -27,8 +27,21 @@ func NewIntentAnalyzer(llmAdapter *llm.Adapter) *IntentAnalyzer {
}
// Analyze 分析用户消息意图
// 优先使用 LLM失败时使用关键词规则降级
// 优先使用 LLM对于简单问候使用关键词快速通道(跳过 LLM 调用)
func (a *IntentAnalyzer) Analyze(ctx context.Context, userMessage string) (*model.IntentResult, error) {
// 快速通道:简单问候/闲聊直接返回,跳过 LLM 调用
if a.isSimpleGreeting(userMessage) {
log.Printf("[intent] 快速通道: 检测到简单问候,跳过 LLM 分析")
result := &model.IntentResult{
Primary: "greeting",
NeedsMemory: false,
NeedsIoT: false,
Sentiment: "positive",
Urgency: "low",
}
return result, nil
}
// 如果 LLM 不可用,直接使用关键词匹配
if !a.enabled || a.llmAdapter == nil {
log.Printf("[intent] LLM 不可用,使用关键词规则分析意图")
@@ -67,6 +80,45 @@ func (a *IntentAnalyzer) Analyze(ctx context.Context, userMessage string) (*mode
return intent, nil
}
// isSimpleGreeting 检测是否为简单问候/闲聊,无需复杂子会话分派
func (a *IntentAnalyzer) isSimpleGreeting(userMessage string) bool {
msgLower := strings.TrimSpace(strings.ToLower(userMessage))
// 精确匹配简单问候
simpleGreetings := []string{
"你好", "嗨", "嘿", "哈喽", "hello", "hi", "hey",
"早上好", "下午好", "晚上好", "晚安", "早安", "午安",
"在吗", "在不在", "在么", "在不",
"谢谢", "多谢", "感谢", "thanks", "thank you",
"好的", "ok", "okay", "行", "可以", "没问题",
"再见", "拜拜", "bye", "byebye", "晚安",
"嗯", "哦", "噢", "额",
}
for _, g := range simpleGreetings {
if msgLower == g {
return true
}
}
// 检测极短消息(<=4个字符)且不包含IoT/问题关键词
runes := []rune(msgLower)
if len(runes) <= 4 {
// 检查是否有明显需要处理的关键词
complexKeywords := []string{"灯", "空调", "窗帘", "设备", "开关", "温度", "亮度",
"什么", "怎么", "为什么", "如何", "谁", "哪里",
"打开", "关闭", "调到", "设置", "帮我", "查"}
for _, kw := range complexKeywords {
if strings.Contains(msgLower, kw) {
return false
}
}
return true
}
return false
}
// keywordAnalyze 基于关键词的意图分析(降级方案)
func (a *IntentAnalyzer) keywordAnalyze(userMessage string) *model.IntentResult {
result := &model.IntentResult{
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"strings"
"time"
ctxbuild "github.com/yourname/cyrene-ai/ai-core/internal/context"
@@ -67,12 +68,13 @@ type ProcessResult struct {
// ProcessInput 处理用户输入 — 新的主入口
// 返回流式事件通道
// v2.1: 支持非阻塞子会话分派 + 简单问候快速通道 + 审查子会话
func (o *Orchestrator) ProcessInput(
ctx context.Context,
params ProcessParams,
) (<-chan model.StreamEvent, error) {
eventCh := make(chan model.StreamEvent, 100)
eventCh := make(chan model.StreamEvent, 200)
if params.Mode == "" {
params.Mode = "text"
@@ -87,6 +89,7 @@ func (o *Orchestrator) ProcessInput(
}()
// 1. 意图分析
startTime := time.Now()
intent, err := o.intentAnalyzer.Analyze(ctx, params.Message)
if err != nil || intent == nil {
log.Printf("[orchestrator] 意图分析失败: %v,使用默认值", err)
@@ -97,6 +100,7 @@ func (o *Orchestrator) ProcessInput(
Urgency: "low",
}
}
log.Printf("[orchestrator] 意图分析耗时: %v, primary=%s", time.Since(startTime), intent.Primary)
// 2. 加载人格配置
personaConfig, err := o.personaLoader.Get("cyrene")
@@ -114,7 +118,10 @@ func (o *Orchestrator) ProcessInput(
userName = params.UserID
}
// 3. 分派子会话(并行执行)
// 注入 userID 到 context 供 MemoryProvider 使用
subCtx := context.WithValue(ctx, "userID", params.UserID)
// 3. 分派子会话(并行执行,非阻塞:先启动合成再等待子会话结果)
createParams := subsession.CreateContextParams{
UserID: params.UserID,
SessionID: params.SessionID,
@@ -124,43 +131,99 @@ func (o *Orchestrator) ProcessInput(
Nickname: userName,
}
// 注入 userID 到 context 供 MemoryProvider 使用
subCtx := context.WithValue(ctx, "userID", params.UserID)
resultCh := o.subManager.Dispatch(subCtx, intent, params.Message, createParams)
// 4. 收集子会话结果
var results []model.SubSessionResult
for result := range resultCh {
results = append(results, result)
// 对于 simple greeting,跳过子会话分派,直接合成回复
var resultCh <-chan model.SubSessionResult
skipSubSessions := intent.Primary == "greeting"
if skipSubSessions {
log.Printf("[orchestrator] 快速通道: 简单问候,跳过子会话分派")
// 创建一个已关闭的空通道
emptyCh := make(chan model.SubSessionResult)
close(emptyCh)
resultCh = emptyCh
} else {
resultCh = o.subManager.Dispatch(subCtx, intent, params.Message, createParams)
}
log.Printf("[orchestrator] 子会话全部完成: 收集到 %d 个结果", len(results))
// 5. 汇总子会话结果
agg := AggregateResults(results)
// 6. 构建对话历史
// 4. 先构建基础综合参数(不含子会话结果),开始合成
history := o.contextBuilder.GetHistory(params.SessionID, 20)
// 7. 构建完整人格提示词
systemPrompt := personaConfig.BuildSystemPrompt(userName, 1)
// 8. 构建综合参数
// 构建初始综合参数(无子会话结果)
synthParams := SynthesizeParams{
UserID: params.UserID,
SessionID: params.SessionID,
UserMessage: params.Message,
Nickname: userName,
PersonaPrompt: systemPrompt,
DialogHistory: history,
MemorySummary: agg.MemorySummary,
ThoughtOutline: agg.ThoughtOutline,
IoTSummary: agg.IoTSummary,
Mode: params.Mode,
UserID: params.UserID,
SessionID: params.SessionID,
UserMessage: params.Message,
Nickname: userName,
PersonaPrompt: systemPrompt,
DialogHistory: history,
Mode: params.Mode,
}
// 9. 调用 Synthesizer 流式生成最终回复
// 非阻塞收集子会话结果:使用 goroutine + channel
// 主流程先开始 LLM 合成,子会话结果到达后再逐步注入
type enrichedParams struct {
memorySummary string
thoughtOutline string
iotSummary string
}
enrichedCh := make(chan enrichedParams, 1)
go func() {
defer close(enrichedCh)
var enriched enrichedParams
for result := range resultCh {
if result.Error != "" {
log.Printf("[orchestrator] 子会话 %s 出错: %s", result.Type, result.Error)
continue
}
switch result.Type {
case model.SubSessionMemory:
enriched.memorySummary = result.Summary
if result.Details != "" {
enriched.memorySummary += "\n" + result.Details
}
log.Printf("[orchestrator] 记忆子会话完成: %s", result.Summary)
case model.SubSessionGeneral:
enriched.thoughtOutline = result.Summary
if result.Details != "" {
enriched.thoughtOutline += "\n" + result.Details
}
log.Printf("[orchestrator] 通用对话子会话完成: %s", result.Summary)
case model.SubSessionIoT:
enriched.iotSummary = result.Summary
log.Printf("[orchestrator] IoT 子会话完成: %s", result.Summary)
}
}
enrichedCh <- enriched
log.Printf("[orchestrator] 子会话全部完成: 结果已收集")
}()
// 注入已到达的子会话结果(如果在合成开始前就有结果到达)
// 启动合成(可能此时还没有子会话结果,先带着空上下文开始)
select {
case enriched := <-enrichedCh:
synthParams.MemorySummary = enriched.memorySummary
synthParams.ThoughtOutline = enriched.thoughtOutline
synthParams.IoTSummary = enriched.iotSummary
default:
// 子会话结果还没完成,先带着空上下文开始合成
// 大部分情况下子会话结果会在 LLM 调用前完成
// 等待一小段时间让快速子会话(如 IoT)完成
timeout := time.After(500 * time.Millisecond)
select {
case enriched := <-enrichedCh:
synthParams.MemorySummary = enriched.memorySummary
synthParams.ThoughtOutline = enriched.thoughtOutline
synthParams.IoTSummary = enriched.iotSummary
case <-timeout:
log.Printf("[orchestrator] 子会话超时等待,以当前上下文开始合成")
}
}
// 5. 调用 Synthesizer 流式生成最终回复
chunkCh, err := o.synthesizer.Synthesize(ctx, synthParams)
if err != nil {
log.Printf("[orchestrator] 综合器启动失败: %v", err)
@@ -171,7 +234,7 @@ func (o *Orchestrator) ProcessInput(
return
}
// 10. 流式输出 delta
// 6. 流式输出 delta
var fullContent string
segmenter := llm.NewSegmenter()
var segments []model.Segment
@@ -215,7 +278,19 @@ func (o *Orchestrator) ProcessInput(
}
}
// 11. 发送断句信息
// 7. 审查完整回复文本,生成带类型的消息列表
if fullContent != "" {
reviewMessages := parseReviewMessages(fullContent)
if len(reviewMessages) > 0 {
eventCh <- model.StreamEvent{
Type: model.StreamReview,
ReviewMessages: reviewMessages,
}
log.Printf("[orchestrator] 审查完成: %d 条带类型消息", len(reviewMessages))
}
}
// 8. 发送断句信息
if len(segments) > 0 {
eventCh <- model.StreamEvent{
Type: model.StreamSegments,
@@ -223,17 +298,17 @@ func (o *Orchestrator) ProcessInput(
}
}
// 12. 完成
// 9. 完成
eventCh <- model.StreamEvent{
Type: model.StreamDone,
}
// 13. 后处理:缓存回复
// 10. 后处理:缓存回复
if fullContent != "" {
o.contextBuilder.CacheMessage(params.SessionID, model.RoleAssistant, fullContent)
}
// 14. 异步提取记忆
// 11. 异步提取记忆
if o.memoryExtractor != nil && fullContent != "" {
go o.memoryExtractor.ExtractAndStore(
context.Background(),
@@ -244,13 +319,152 @@ func (o *Orchestrator) ProcessInput(
)
}
log.Printf("[orchestrator] 处理完成: intent=%s, content_len=%d, sub_results=%d",
intent.Primary, len(fullContent), len(results))
log.Printf("[orchestrator] 处理完成: intent=%s, content_len=%d, time=%v",
intent.Primary, len([]rune(fullContent)), time.Since(startTime))
}()
return eventCh, nil
}
// parseReviewMessages 解析完整回复文本,拆分为带类型的消息
// 用于审查子会话的轻量版本(内联到 orchestrator 以减少一次子会话调度开销)
func parseReviewMessages(text string) []model.ReviewMessage {
if text == "" {
return nil
}
var messages []model.ReviewMessage
// 简单状态机:逐行或按括号匹配提取(使用 rune 切片正确处理 Unicode
remaining := text
for len(remaining) > 0 {
// 查找括号动作 xxx)或 (xxx)
actionStart := -1 // byte 位置
actionEnd := -1 // byte 位置(括号之后)
actionContent := ""
runes := []rune(remaining)
for ri, r := range runes {
if r == '(' || r == '' {
actionStart = len(string(runes[:ri]))
closeRune := ')'
if r == '' {
closeRune = ''
}
// 查找匹配的闭合括号
for rj := ri + 1; rj < len(runes); rj++ {
if runes[rj] == closeRune {
actionEnd = len(string(runes[:rj+1]))
actionContent = string(runes[ri+1 : rj])
break
}
}
break
}
}
if actionStart >= 0 {
// 括号前的普通文本
if actionStart > 0 {
prefix := strings.TrimSpace(remaining[:actionStart])
if prefix != "" {
messages = append(messages, splitReviewLongMessage(model.ReviewMessageChat, prefix)...)
}
}
// 括号内作为 action
content := strings.TrimSpace(actionContent)
if content != "" {
messages = append(messages, model.ReviewMessage{
Type: model.ReviewMessageAction,
Content: content,
})
}
remaining = remaining[actionEnd:]
} else {
// 没有括号,剩余全部作为 chat
remaining = strings.TrimSpace(remaining)
if remaining != "" {
messages = append(messages, splitReviewLongMessage(model.ReviewMessageChat, remaining)...)
}
break
}
}
if len(messages) == 0 && text != "" {
messages = append(messages, model.ReviewMessage{
Type: model.ReviewMessageChat,
Content: strings.TrimSpace(text),
})
}
return messages
}
// splitReviewLongMessage 将长消息按句子边界拆分为多条短消息
func splitReviewLongMessage(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)
}
// 尝试在句子边界处分割
if end < len(runes) {
lastBreak := -1
// 先找句号、感叹号、问号
for i := end - 1; i >= start+maxLen/2; i-- {
ch := runes[i]
if ch == '。' || ch == '' || ch == '' || ch == '.' || ch == '!' || ch == '?' || ch == '' || ch == ';' || ch == '\n' {
lastBreak = i
break
}
}
// 再找逗号
if lastBreak < 0 {
for i := end - 1; i >= start+maxLen/2; i-- {
ch := runes[i]
if ch == '' || ch == ',' || ch == ' ' || ch == ' ' {
lastBreak = i
break
}
}
}
if lastBreak > 0 {
end = lastBreak + 1
}
}
chunk := strings.TrimSpace(string(runes[start:end]))
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
}
// ProcessInputSync 同步处理用户输入(兼容旧接口)
func (o *Orchestrator) ProcessInputSync(
ctx context.Context,