fix: 修复19个Bug (P0-P3) — 持续性调试第7轮发现的问题
P0 (5): crypto/rand session ID, TTS fallback可达性, goroutine defer recover, adminAuth前缀修正 P1 (5): 普通用户密码验证, context传递, priority clamp, 超时重试, 自主思考速率限制 P2 (4): Briefing AI降级, 前端消息类型渲染, Docker Compose补全, PWA 192图标 P3 (5): goroutine错误处理, .gitignore完善, reminder created_at, voice Dockerfile, Go版本更新
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# ========== 构建阶段 ==========
|
||||
FROM golang:1.23-alpine AS builder
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
|
||||
|
||||
@@ -220,6 +220,11 @@ func (t *Thinker) TriggerPostChatThink() {
|
||||
t.wg.Add(1)
|
||||
go func() {
|
||||
defer t.wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[后台思考] 对话后触发 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// 短暂延迟,让对话有个自然的停顿
|
||||
select {
|
||||
@@ -261,6 +266,11 @@ func (t *Thinker) resetSilenceTimer() {
|
||||
t.wg.Add(1)
|
||||
go func() {
|
||||
defer t.wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[后台思考] 静默定时器 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
@@ -316,16 +326,30 @@ func (t *Thinker) HasPendingThoughts() bool {
|
||||
// performThink 执行一次增强版后台思考(支持工具调用和记忆管理)
|
||||
//
|
||||
// triggerReason: "post_chat" (对话后) 或 "silence" (静默超时)
|
||||
//
|
||||
// 防御性速率限制:即使调用方未检查 minThinkGap,performThink 自身也会
|
||||
// 强制执行最小间隔,防止并发调用或 bug 导致 LLM 配额被快速消耗。
|
||||
func (t *Thinker) performThink(triggerReason string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
t.mu.Lock()
|
||||
gapSinceLast := time.Since(t.lastThinkTime)
|
||||
minGap := t.minThinkGap
|
||||
if minGap <= 0 {
|
||||
minGap = 5 * time.Second // 默认最小间隔 5 秒
|
||||
}
|
||||
if gapSinceLast < minGap {
|
||||
t.mu.Unlock()
|
||||
log.Printf("[后台思考] 距上次思考仅 %v,跳过 (最小间隔=%v, 触发原因=%s)", gapSinceLast.Round(time.Second), minGap, triggerReason)
|
||||
return
|
||||
}
|
||||
|
||||
t.lastThinkTime = time.Now()
|
||||
t.thinkCount++
|
||||
currentCount := t.thinkCount
|
||||
t.mu.Unlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
log.Printf("[后台思考] 开始思考周期 (触发原因=%s, 计数=%d)...", triggerReason, currentCount)
|
||||
|
||||
// 1. 加载人格配置
|
||||
@@ -356,7 +380,7 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
// 4. 查询 IoT 设备状态(每次都查询,无间隔限制)
|
||||
var deviceSummary string
|
||||
if t.iotClient != nil {
|
||||
devices := t.iotClient.GetDevicesForContext()
|
||||
devices := t.iotClient.GetDevicesForContext(ctx)
|
||||
if len(devices) > 0 {
|
||||
deviceSummary = formatDeviceContext(devices)
|
||||
}
|
||||
@@ -460,7 +484,14 @@ func (t *Thinker) performThink(triggerReason string) {
|
||||
|
||||
// 9. 从思考结果中提取记忆(异步)
|
||||
if t.memoryExtractor != nil {
|
||||
go t.extractMemoriesFromThinking(finalContent)
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[后台思考] 提取记忆 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
t.extractMemoriesFromThinking(finalContent)
|
||||
}()
|
||||
}
|
||||
|
||||
// 10. 周期性记忆维护(每 10 次思考触发一次,而非按时间)
|
||||
@@ -649,6 +680,11 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
||||
// 异步持久化到 memory-service
|
||||
if t.memClient != nil {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[后台思考] 持久化思考日志 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := t.memClient.SaveThinkingLog(ctx, t.adminUserID, content, toolCallsJSON, toolCallCount, len(content)); err != nil {
|
||||
|
||||
@@ -80,6 +80,11 @@ func (o *Orchestrator) ProcessInput(
|
||||
|
||||
go func() {
|
||||
defer close(eventCh)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[orchestrator] 编排器主循环 panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// 1. 意图分析
|
||||
intent, err := o.intentAnalyzer.Analyze(ctx, params.Message)
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
|
||||
// IoTDeviceProvider IoT 设备查询接口
|
||||
type IoTDeviceProvider interface {
|
||||
GetAllDevices() ([]tools.IoTDevice, error)
|
||||
GetDevice(id string) (*tools.IoTDevice, error)
|
||||
GetAllDevices(ctx context.Context) ([]tools.IoTDevice, error)
|
||||
GetDevice(ctx context.Context, id string) (*tools.IoTDevice, error)
|
||||
ToggleDevice(id string) error
|
||||
SetDeviceProperty(id string, field string, value interface{}) error
|
||||
GetDevicesForContext() []tools.IoTDevice
|
||||
GetDevicesForContext(ctx context.Context) []tools.IoTDevice
|
||||
}
|
||||
|
||||
// IoTProvider IoT 控制子会话提供者
|
||||
@@ -75,7 +75,7 @@ func (p *IoTProvider) CreateContext(ctx context.Context, params CreateContextPar
|
||||
// 获取当前设备状态
|
||||
var deviceStatusText string
|
||||
if p.iotClient != nil {
|
||||
devices := p.iotClient.GetDevicesForContext()
|
||||
devices := p.iotClient.GetDevicesForContext(ctx)
|
||||
if len(devices) > 0 {
|
||||
deviceStatusText = "当前设备状态:\n"
|
||||
for _, d := range devices {
|
||||
@@ -208,7 +208,7 @@ func (p *IoTProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*
|
||||
msgLower := strings.ToLower(userMessage)
|
||||
|
||||
// 尝试获取设备列表进行匹配
|
||||
devices := p.iotClient.GetDevicesForContext()
|
||||
devices := p.iotClient.GetDevicesForContext(ctx)
|
||||
|
||||
for _, dev := range devices {
|
||||
devNameLower := strings.ToLower(dev.Name)
|
||||
|
||||
@@ -89,6 +89,11 @@ func (m *Manager) Dispatch(
|
||||
wg.Add(1)
|
||||
go func(p Provider) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[subsession] dispatch goroutine panic 恢复 (type=%s): %v", p.Type(), r)
|
||||
}
|
||||
}()
|
||||
|
||||
result := model.SubSessionResult{Type: p.Type()}
|
||||
|
||||
@@ -135,6 +140,11 @@ func (m *Manager) Dispatch(
|
||||
|
||||
// 等待所有子会话完成,关闭通道
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[subsession] wait goroutine panic 恢复: %v", r)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
close(resultCh)
|
||||
}()
|
||||
|
||||
@@ -2,6 +2,7 @@ package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -56,7 +57,7 @@ func NewIoTClient(baseURL string) *IoTClient {
|
||||
}
|
||||
|
||||
// GetAllDevices 获取所有设备列表(带缓存)
|
||||
func (c *IoTClient) GetAllDevices() ([]IoTDevice, error) {
|
||||
func (c *IoTClient) GetAllDevices(ctx context.Context) ([]IoTDevice, error) {
|
||||
// 检查缓存
|
||||
c.mu.RLock()
|
||||
if c.cache != nil && time.Since(c.cacheTime) < c.cacheTTL {
|
||||
@@ -68,7 +69,13 @@ func (c *IoTClient) GetAllDevices() ([]IoTDevice, error) {
|
||||
c.mu.RUnlock()
|
||||
|
||||
// 请求 API
|
||||
resp, err := c.client.Get(c.baseURL + "/api/v1/devices")
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/devices", nil)
|
||||
if err != nil {
|
||||
log.Printf("[IoT客户端] 创建请求失败: %v", err)
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("[IoT客户端] 请求失败: %v", err)
|
||||
return nil, fmt.Errorf("获取设备列表失败: %w", err)
|
||||
@@ -97,8 +104,12 @@ func (c *IoTClient) GetAllDevices() ([]IoTDevice, error) {
|
||||
}
|
||||
|
||||
// GetDevice 获取单个设备详情
|
||||
func (c *IoTClient) GetDevice(id string) (*IoTDevice, error) {
|
||||
resp, err := c.client.Get(c.baseURL + "/api/v1/devices/" + id)
|
||||
func (c *IoTClient) GetDevice(ctx context.Context, id string) (*IoTDevice, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/devices/"+id, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取设备 %s 失败: %w", id, err)
|
||||
}
|
||||
@@ -195,8 +206,8 @@ func (c *IoTClient) SetDeviceProperty(id string, field string, value interface{}
|
||||
}
|
||||
|
||||
// GetDevicesForContext 获取设备状态摘要(供上下文注入使用,失败不报错)
|
||||
func (c *IoTClient) GetDevicesForContext() []IoTDevice {
|
||||
devices, err := c.GetAllDevices()
|
||||
func (c *IoTClient) GetDevicesForContext(ctx context.Context) []IoTDevice {
|
||||
devices, err := c.GetAllDevices(ctx)
|
||||
if err != nil {
|
||||
log.Printf("[IoT客户端] 获取设备状态摘要失败: %v", err)
|
||||
return nil
|
||||
|
||||
@@ -149,22 +149,22 @@ func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]inter
|
||||
|
||||
// 先获取设备名用于友好的返回消息(失败不影响后续流程)
|
||||
deviceName := deviceID
|
||||
if dev, err := t.iotClient.GetDevice(deviceID); err == nil {
|
||||
if dev, err := t.iotClient.GetDevice(ctx, deviceID); err == nil {
|
||||
deviceName = dev.Name
|
||||
}
|
||||
|
||||
// 处理属性设置类操作
|
||||
switch action {
|
||||
case "set_temperature":
|
||||
return t.handleSetTemperature(deviceID, arguments)
|
||||
return t.handleSetTemperature(ctx, deviceID, arguments)
|
||||
case "set_brightness":
|
||||
return t.handleSetBrightness(deviceID, arguments)
|
||||
return t.handleSetBrightness(ctx, deviceID, arguments)
|
||||
case "set_position":
|
||||
return t.handleSetPosition(deviceID, arguments)
|
||||
return t.handleSetPosition(ctx, deviceID, arguments)
|
||||
case "set_mode":
|
||||
return t.handleSetMode(deviceID, arguments)
|
||||
return t.handleSetMode(ctx, deviceID, arguments)
|
||||
case "set_color":
|
||||
return t.handleSetColor(deviceID, arguments)
|
||||
return t.handleSetColor(ctx, deviceID, arguments)
|
||||
case "turn_off":
|
||||
// 声明式关闭:使用 SetDeviceProperty status/off 而非 toggle
|
||||
// 即使设备已经关闭,SetProperty 也会幂等处理
|
||||
@@ -204,7 +204,7 @@ func (t *IoTControlTool) Execute(ctx context.Context, arguments map[string]inter
|
||||
}
|
||||
|
||||
// 获取切换后的状态
|
||||
updatedDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
updatedDevice, err := t.iotClient.GetDevice(ctx, deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
@@ -230,7 +230,7 @@ func extractValue(arguments map[string]interface{}) interface{} {
|
||||
}
|
||||
|
||||
// handleSetTemperature 处理设置温度
|
||||
func (t *IoTControlTool) handleSetTemperature(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
func (t *IoTControlTool) handleSetTemperature(ctx context.Context, deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
@@ -241,7 +241,7 @@ func (t *IoTControlTool) handleSetTemperature(deviceID string, arguments map[str
|
||||
}
|
||||
|
||||
// 先获取当前设备信息
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
currentDevice, err := t.iotClient.GetDevice(ctx, deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
@@ -275,7 +275,7 @@ func (t *IoTControlTool) handleSetTemperature(deviceID string, arguments map[str
|
||||
}
|
||||
|
||||
// handleSetBrightness 处理设置亮度
|
||||
func (t *IoTControlTool) handleSetBrightness(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
func (t *IoTControlTool) handleSetBrightness(ctx context.Context, deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
@@ -286,7 +286,7 @@ func (t *IoTControlTool) handleSetBrightness(deviceID string, arguments map[stri
|
||||
}
|
||||
|
||||
// 先获取当前设备信息
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
currentDevice, err := t.iotClient.GetDevice(ctx, deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
@@ -320,7 +320,7 @@ func (t *IoTControlTool) handleSetBrightness(deviceID string, arguments map[stri
|
||||
}
|
||||
|
||||
// handleSetPosition 处理设置窗帘位置
|
||||
func (t *IoTControlTool) handleSetPosition(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
func (t *IoTControlTool) handleSetPosition(ctx context.Context, deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
@@ -330,7 +330,7 @@ func (t *IoTControlTool) handleSetPosition(deviceID string, arguments map[string
|
||||
}, nil
|
||||
}
|
||||
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
currentDevice, err := t.iotClient.GetDevice(ctx, deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
@@ -364,7 +364,7 @@ func (t *IoTControlTool) handleSetPosition(deviceID string, arguments map[string
|
||||
}
|
||||
|
||||
// handleSetMode 处理设置空调模式
|
||||
func (t *IoTControlTool) handleSetMode(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
func (t *IoTControlTool) handleSetMode(ctx context.Context, deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
@@ -383,7 +383,7 @@ func (t *IoTControlTool) handleSetMode(deviceID string, arguments map[string]int
|
||||
}, nil
|
||||
}
|
||||
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
currentDevice, err := t.iotClient.GetDevice(ctx, deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
@@ -408,7 +408,7 @@ func (t *IoTControlTool) handleSetMode(deviceID string, arguments map[string]int
|
||||
}
|
||||
|
||||
// handleSetColor 处理设置灯光颜色
|
||||
func (t *IoTControlTool) handleSetColor(deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
func (t *IoTControlTool) handleSetColor(ctx context.Context, deviceID string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
val := extractValue(arguments)
|
||||
if val == nil {
|
||||
return &ToolResult{
|
||||
@@ -427,7 +427,7 @@ func (t *IoTControlTool) handleSetColor(deviceID string, arguments map[string]in
|
||||
}, nil
|
||||
}
|
||||
|
||||
currentDevice, err := t.iotClient.GetDevice(deviceID)
|
||||
currentDevice, err := t.iotClient.GetDevice(ctx, deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_control",
|
||||
|
||||
@@ -47,7 +47,7 @@ func (t *IoTQueryTool) Execute(ctx context.Context, arguments map[string]interfa
|
||||
|
||||
if deviceID != "" {
|
||||
// 查询单个设备
|
||||
device, err := t.iotClient.GetDevice(deviceID)
|
||||
device, err := t.iotClient.GetDevice(ctx, deviceID)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_query",
|
||||
@@ -63,7 +63,7 @@ func (t *IoTQueryTool) Execute(ctx context.Context, arguments map[string]interfa
|
||||
}
|
||||
|
||||
// 查询所有设备
|
||||
devices, err := t.iotClient.GetAllDevices()
|
||||
devices, err := t.iotClient.GetAllDevices(ctx)
|
||||
if err != nil {
|
||||
return &ToolResult{
|
||||
ToolName: "iot_query",
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -82,7 +83,54 @@ func (c *ToolEngineClient) GetDefinitions(ctx context.Context) ([]ToolDefinition
|
||||
}
|
||||
|
||||
// Execute 通过 tool-engine 执行工具调用
|
||||
// 包含重试逻辑:最多重试 2 次(共 3 次尝试),间隔 100ms
|
||||
func (c *ToolEngineClient) Execute(ctx context.Context, toolName string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
const maxRetries = 2
|
||||
const retryDelay = 100 * time.Millisecond
|
||||
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
log.Printf("[tool-engine-client] 工具 %s 第 %d 次重试 (上次错误: %v)", toolName, attempt, lastErr)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ToolResult{
|
||||
ToolName: toolName,
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("请求被取消: %v", ctx.Err()),
|
||||
}, nil
|
||||
case <-time.After(retryDelay):
|
||||
}
|
||||
}
|
||||
|
||||
result, err := c.executeOnce(ctx, toolName, arguments)
|
||||
if err == nil && result.Success {
|
||||
return result, nil
|
||||
}
|
||||
if result != nil {
|
||||
lastErr = fmt.Errorf("%s", result.Error)
|
||||
} else {
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
// 不可重试的错误:工具不存在、参数序列化失败、创建请求失败
|
||||
if result != nil && (strings.Contains(result.Error, "不存在") ||
|
||||
strings.Contains(result.Error, "序列化") ||
|
||||
strings.Contains(result.Error, "创建请求")) {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[tool-engine-client] 工具 %s 所有重试均失败 (最后错误: %v)", toolName, lastErr)
|
||||
return &ToolResult{
|
||||
ToolName: toolName,
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("请求 tool-engine 失败 (已重试 %d 次): %v", maxRetries, lastErr),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// executeOnce 执行单次工具调用(不含重试逻辑)
|
||||
func (c *ToolEngineClient) executeOnce(ctx context.Context, toolName string, arguments map[string]interface{}) (*ToolResult, error) {
|
||||
body, err := json.Marshal(map[string]interface{}{
|
||||
"arguments": arguments,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user