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:
2026-05-20 13:30:32 +08:00
parent baaf90fc47
commit 4b35736f73
37 changed files with 556 additions and 118 deletions
+17 -6
View File
@@ -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",
+2 -2
View File
@@ -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,
})