Files
Cyrene/backend/ai-core/internal/tools/iot_client.go
T
AskaEth a80bfd12eb fix: 修复6个bug + IoT设备控制增强 + DevTools IoT面板
问题1: 刷新后主对话历史不显示,侧边栏子对话列表为空
  - sessionStore: 修复 setCurrentSessionId 用 Map 去重消息
  - AppLayout: 修复 autoLoadNewSession 逻辑
  - useWebSocket: 修复 setMessages 调用时机

问题2: 切换到次级对话后无法切换回主对话
  - Sidebar: 为删除按钮添加 e.stopPropagation()

问题3&4: IoT设备列表展开导致输入栏消失 + 聊天消息无法滚动
  - IoTStatusBar: 从fixed定位改为inline布局
  - ChatContainer: 重构flex布局,MessageList自动撑满

问题5: AI核心无法操作IoT设备 + 无法设置温度等属性
  - 新增 IoTControlTool (iot_control_tool.go)
  - IoTClient: 新增 ToggleDevice/SetProperty/GetHistory
  - 支持 set_temperature/set_brightness/set_position/set_mode/set_color

问题6: DevTools启动时Gateway代理登录异常
  - devtools: 登录失败时静默降级,不阻塞启动

额外修复:
  - iot_tools.go: 修复fmt.Sprintf参数缺失
  - iot-debug-service: 修复并发死锁问题
  - DevTools: 新增IoT设备控制面板(API代理+前端UI)
2026-05-17 14:37:44 +08:00

220 lines
5.5 KiB
Go

package tools
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"sync"
"time"
)
// IoTDevice 设备结构体(与 IoT 调试服务的结构对应)
type IoTDevice struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
Brightness int `json:"brightness,omitempty"`
Color string `json:"color,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
Mode string `json:"mode,omitempty"`
Position int `json:"position,omitempty"`
Value float64 `json:"value,omitempty"`
Unit string `json:"unit,omitempty"`
Battery int `json:"battery,omitempty"`
LastUpdated string `json:"last_updated"`
}
// IoTClient IoT 调试服务 HTTP 客户端
type IoTClient struct {
baseURL string
client *http.Client
// 缓存控制
mu sync.RWMutex
cache []IoTDevice
cacheTime time.Time
cacheTTL time.Duration
}
// NewIoTClient 创建 IoT 客户端
func NewIoTClient(baseURL string) *IoTClient {
if baseURL == "" {
baseURL = getEnv("IOT_DEBUG_SERVICE_URL", "http://localhost:8083")
}
return &IoTClient{
baseURL: baseURL,
client: &http.Client{
Timeout: 5 * time.Second,
},
cacheTTL: 60 * time.Second,
}
}
// GetAllDevices 获取所有设备列表(带缓存)
func (c *IoTClient) GetAllDevices() ([]IoTDevice, error) {
// 检查缓存
c.mu.RLock()
if c.cache != nil && time.Since(c.cacheTime) < c.cacheTTL {
devices := make([]IoTDevice, len(c.cache))
copy(devices, c.cache)
c.mu.RUnlock()
return devices, nil
}
c.mu.RUnlock()
// 请求 API
resp, err := c.client.Get(c.baseURL + "/api/v1/devices")
if err != nil {
log.Printf("[IoT客户端] 请求失败: %v", err)
return nil, fmt.Errorf("获取设备列表失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("获取设备列表返回状态码 %d", resp.StatusCode)
}
var result struct {
Devices []IoTDevice `json:"devices"`
Total int `json:"total"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("解析设备列表失败: %w", err)
}
// 更新缓存
c.mu.Lock()
c.cache = result.Devices
c.cacheTime = time.Now()
c.mu.Unlock()
return result.Devices, nil
}
// GetDevice 获取单个设备详情
func (c *IoTClient) GetDevice(id string) (*IoTDevice, error) {
resp, err := c.client.Get(c.baseURL + "/api/v1/devices/" + id)
if err != nil {
return nil, fmt.Errorf("获取设备 %s 失败: %w", id, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("设备 %s 不存在", id)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("获取设备 %s 返回状态码 %d", id, resp.StatusCode)
}
var result struct {
Device IoTDevice `json:"device"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("解析设备信息失败: %w", err)
}
return &result.Device, nil
}
// ToggleDevice 切换设备开关状态
func (c *IoTClient) ToggleDevice(id string) error {
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/v1/devices/"+id+"/toggle", nil)
if err != nil {
return fmt.Errorf("创建切换请求失败: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("切换设备 %s 失败: %w", id, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("设备 %s 不存在", id)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("切换设备 %s 返回状态码 %d", id, resp.StatusCode)
}
// 切换后清除缓存,确保下次查询获取最新状态
c.mu.Lock()
c.cache = nil
c.mu.Unlock()
return nil
}
// SetDeviceProperty 设置设备属性(温度、亮度、位置、模式、颜色等)
func (c *IoTClient) SetDeviceProperty(id string, field string, value interface{}) error {
body, err := json.Marshal(map[string]interface{}{
"field": field,
"value": value,
})
if err != nil {
return fmt.Errorf("序列化请求失败: %w", err)
}
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/v1/devices/"+id+"/set", nil)
if err != nil {
return fmt.Errorf("创建设置请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Body = io.NopCloser(bytes.NewReader(body))
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("设置设备 %s 属性失败: %w", id, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("设备 %s 不存在", id)
}
if resp.StatusCode != http.StatusOK {
var errResp struct {
Error string `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
if errResp.Error != "" {
return fmt.Errorf("设置设备 %s 属性失败: %s", id, errResp.Error)
}
return fmt.Errorf("设置设备 %s 属性返回状态码 %d", id, resp.StatusCode)
}
// 修改后清除缓存
c.mu.Lock()
c.cache = nil
c.mu.Unlock()
return nil
}
// GetDevicesForContext 获取设备状态摘要(供上下文注入使用,失败不报错)
func (c *IoTClient) GetDevicesForContext() []IoTDevice {
devices, err := c.GetAllDevices()
if err != nil {
log.Printf("[IoT客户端] 获取设备状态摘要失败: %v", err)
return nil
}
return devices
}
// InvalidateCache 使缓存失效
func (c *IoTClient) InvalidateCache() {
c.mu.Lock()
c.cache = nil
c.mu.Unlock()
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}