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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user