Files
Cyrene/backend/ai-core/internal/tools/iot_client.go
T
AskaEth 71f0a1abdb feat: Go模块路径迁移 + Docker生产部署适配 + ethend Docker兼容
- 所有Go模块路径从 github.com/yourname/cyrene-ai 迁移到 git.yeij.top/AskaEth/Cyrene
- 5个Go Dockerfile添加 GOPROXY=https://goproxy.cn,direct 解决国内构建问题
- ai-core go.mod 添加 pkg/plugins replace 指令
- Caddyfile 简化为 http:// 通配 + handle 保留 /api 前缀
- ethend Dockerfile 适配 (npm install + 仅 COPY package.json)
- ethend 新增 RUNNING_IN_DOCKER 环境变量,健康检查改用Docker服务名
- ethend 数据库状态检查支持Docker hostname (postgres/redis/qdrant/minio)
- process-manager 新增 CONTAINER_SVC_MAP + Docker模式自动检测
- 统一 docker-compose.dev.db.yml 卷名 (pg_data/redis_data/qdrant_data/minio_data)
- docker-compose.yml ethend服务挂载docker.sock + 端口变量化
- 清理 .env 统一后的残留文件与提示信息

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 13:43:22 +08:00

251 lines
7.4 KiB
Go

package tools
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"git.yeij.top/AskaEth/Cyrene/pkg/logger"
"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 == "" {
// 向后兼容:优先使用 IOT_SERVICE_URL,回退到 IOT_DEBUG_SERVICE_URL
baseURL = getEnv("IOT_SERVICE_URL", "")
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(ctx context.Context) ([]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
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/v1/devices", nil)
if err != nil {
logger.Printf("[IoT客户端] 创建请求失败: %v", err)
return nil, fmt.Errorf("创建请求失败: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
logger.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(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)
}
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 {
logger.Printf("[IoT-client] 🔄 切换设备: id=%s, url=%s", id, c.baseURL+"/api/v1/devices/"+id+"/toggle")
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/v1/devices/"+id+"/toggle", nil)
if err != nil {
logger.Printf("[IoT-client] ❌ 创建切换请求失败: device=%s, err=%v", id, err)
return fmt.Errorf("创建切换请求失败: %w", err)
}
resp, err := c.client.Do(req)
if err != nil {
logger.Printf("[IoT-client] ❌ 切换设备 HTTP 失败: device=%s, err=%v", id, err)
return fmt.Errorf("切换设备 %s 失败: %w", id, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
logger.Printf("[IoT-client] ❌ 设备不存在: %s", id)
return fmt.Errorf("设备 %s 不存在", id)
}
if resp.StatusCode != http.StatusOK {
logger.Printf("[IoT-client] ❌ 切换设备返回非200: device=%s, status=%d", id, resp.StatusCode)
return fmt.Errorf("切换设备 %s 返回状态码 %d", id, resp.StatusCode)
}
// 切换后清除缓存,确保下次查询获取最新状态
c.mu.Lock()
c.cache = nil
c.mu.Unlock()
logger.Printf("[IoT-client] ✅ 切换设备成功: %s", id)
return nil
}
// SetDeviceProperty 设置设备属性(温度、亮度、位置、模式、颜色等)
func (c *IoTClient) SetDeviceProperty(id string, field string, value interface{}) error {
logger.Printf("[IoT-client] 🔧 设置设备属性: device=%s, field=%s, value=%v, url=%s", id, field, value, c.baseURL+"/api/v1/devices/"+id+"/set")
body, err := json.Marshal(map[string]interface{}{
"field": field,
"value": value,
})
if err != nil {
logger.Printf("[IoT-client] ❌ 序列化请求失败: device=%s, err=%v", id, err)
return fmt.Errorf("序列化请求失败: %w", err)
}
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/api/v1/devices/"+id+"/set", nil)
if err != nil {
logger.Printf("[IoT-client] ❌ 创建设置请求失败: device=%s, err=%v", id, err)
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 {
logger.Printf("[IoT-client] ❌ 设置设备属性 HTTP 失败: device=%s, field=%s, err=%v", id, field, err)
return fmt.Errorf("设置设备 %s 属性失败: %w", id, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
logger.Printf("[IoT-client] ❌ 设备不存在: %s", id)
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 != "" {
logger.Printf("[IoT-client] ❌ 设置设备属性失败: device=%s, err=%s", id, errResp.Error)
return fmt.Errorf("设置设备 %s 属性失败: %s", id, errResp.Error)
}
logger.Printf("[IoT-client] ❌ 设置设备属性返回非200: device=%s, status=%d", id, resp.StatusCode)
return fmt.Errorf("设置设备 %s 属性返回状态码 %d", id, resp.StatusCode)
}
// 修改后清除缓存
c.mu.Lock()
c.cache = nil
c.mu.Unlock()
logger.Printf("[IoT-client] ✅ 设置设备属性成功: device=%s, field=%s, value=%v", id, field, value)
return nil
}
// GetDevicesForContext 获取设备状态摘要(供上下文注入使用,失败不报错)
func (c *IoTClient) GetDevicesForContext(ctx context.Context) []IoTDevice {
devices, err := c.GetAllDevices(ctx)
if err != nil {
logger.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
}