feat: 第五轮开发 - 14项未来路线图功能完整实现
W1-W14 全部完成: - W1: 消息搜索 (ILIKE全文检索 + SearchModal) - W2: 对话导出 (JSON/Markdown/TXT三格式) - W3: 记忆时间线 DevTools 可视化 - W4: 通知推送系统 (WebSocket + Browser Notification API) - W5: 定时提醒 (30s轮询 + 重复提醒 + WebSocket推送) - W6: 每日简报 (08:00自动生成: 天气+新闻+提醒+AI摘要) - W7: IoT场景自动化 (规则引擎 10s轮询 + 条件评估 + 场景执行) - W8: 语音输入 (浏览器 Speech Recognition API) - W9: STT服务 (voice-service + whisper.cpp) - W10: TTS服务 (浏览器 Speech Synthesis + edge-tts三档回退) - W11: 文件管理 (上传/下载/缩略图/纯Go bilinear缩放) - W12: 知识库RAG (PostgreSQL tsvector + 文档分块 + 检索) - W13: 多模态 (图片上传+分析: Vision API + 本地Go分析回退) - W14: PWA (Service Worker + 离线页 + install prompt) 总计: 6个Go微服务 + 10+前端组件 + 10+ PostgreSQL表 + 4个后台调度器
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/store"
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/ws"
|
||||
)
|
||||
|
||||
// TriggerConfig 触发器配置
|
||||
type TriggerConfig struct {
|
||||
Cron string `json:"cron,omitempty"`
|
||||
Time string `json:"time,omitempty"`
|
||||
Days []string `json:"days,omitempty"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
Property string `json:"property,omitempty"`
|
||||
Operator string `json:"operator,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Condition 条件定义
|
||||
type Condition struct {
|
||||
Type string `json:"type"`
|
||||
Start string `json:"start,omitempty"`
|
||||
End string `json:"end,omitempty"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
Property string `json:"property,omitempty"`
|
||||
Operator string `json:"operator,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Action 动作定义
|
||||
type Action struct {
|
||||
Type string `json:"type"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
Property string `json:"property,omitempty"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
// RuleEngine 规则引擎
|
||||
type RuleEngine struct {
|
||||
store *store.AutomationStore
|
||||
hub *ws.Hub
|
||||
iotServiceURL string
|
||||
httpClient *http.Client
|
||||
lastTriggered map[string]time.Time
|
||||
mu sync.RWMutex
|
||||
stopCh chan struct{}
|
||||
running bool
|
||||
}
|
||||
|
||||
// NewRuleEngine 创建规则引擎
|
||||
func NewRuleEngine(as *store.AutomationStore, hub *ws.Hub) *RuleEngine {
|
||||
iotServiceURL := os.Getenv("IOT_SERVICE_URL")
|
||||
if iotServiceURL == "" {
|
||||
iotServiceURL = "http://localhost:8083"
|
||||
}
|
||||
|
||||
return &RuleEngine{
|
||||
store: as,
|
||||
hub: hub,
|
||||
iotServiceURL: iotServiceURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
lastTriggered: make(map[string]time.Time),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动后台规则评估 goroutine
|
||||
func (e *RuleEngine) Start() {
|
||||
e.mu.Lock()
|
||||
if e.running {
|
||||
e.mu.Unlock()
|
||||
return
|
||||
}
|
||||
e.running = true
|
||||
e.mu.Unlock()
|
||||
|
||||
go e.loop()
|
||||
log.Printf("[RuleEngine] 规则引擎已启动 (IoT服务地址: %s)", e.iotServiceURL)
|
||||
}
|
||||
|
||||
// Stop 停止规则引擎
|
||||
func (e *RuleEngine) Stop() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if !e.running {
|
||||
return
|
||||
}
|
||||
close(e.stopCh)
|
||||
e.running = false
|
||||
log.Println("[RuleEngine] 规则引擎已停止")
|
||||
}
|
||||
|
||||
// loop 规则引擎主循环
|
||||
func (e *RuleEngine) loop() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// 首次立即评估
|
||||
e.evaluateAllRules()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-e.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
e.evaluateAllRules()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// evaluateAllRules 评估所有启用的规则
|
||||
func (e *RuleEngine) evaluateAllRules() {
|
||||
rules, err := e.store.GetEnabledRules()
|
||||
if err != nil {
|
||||
log.Printf("[RuleEngine] 获取启用的规则失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(rules) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if e.evaluateRule(&rule) {
|
||||
e.ExecuteRuleActions(&rule)
|
||||
e.store.MarkRuleTriggered(rule.ID)
|
||||
e.mu.Lock()
|
||||
e.lastTriggered[rule.ID] = time.Now()
|
||||
e.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// evaluateRule 评估单条规则是否应触发
|
||||
func (e *RuleEngine) evaluateRule(rule *store.AutomationRule) bool {
|
||||
// 防重复触发:同一规则在 1 分钟内不重复触发
|
||||
e.mu.RLock()
|
||||
lastTime, exists := e.lastTriggered[rule.ID]
|
||||
e.mu.RUnlock()
|
||||
if exists && time.Since(lastTime) < 1*time.Minute {
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析 trigger_config
|
||||
var triggerCfg TriggerConfig
|
||||
if rule.TriggerConfig != nil {
|
||||
if err := json.Unmarshal(*rule.TriggerConfig, &triggerCfg); err != nil {
|
||||
log.Printf("[RuleEngine] 解析触发器配置失败: rule=%s err=%v", rule.ID, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 评估触发器
|
||||
triggered := false
|
||||
switch rule.TriggerType {
|
||||
case "schedule":
|
||||
triggered = e.evaluateScheduleTrigger(triggerCfg)
|
||||
case "device_state":
|
||||
triggered = e.evaluateDeviceStateTrigger(triggerCfg)
|
||||
case "manual":
|
||||
// 不自动触发
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if !triggered {
|
||||
return false
|
||||
}
|
||||
|
||||
// 评估 conditions
|
||||
var conditions []Condition
|
||||
if rule.Conditions != nil {
|
||||
if err := json.Unmarshal(*rule.Conditions, &conditions); err != nil {
|
||||
log.Printf("[RuleEngine] 解析条件失败: rule=%s err=%v", rule.ID, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, cond := range conditions {
|
||||
if !e.evaluateCondition(cond) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// evaluateScheduleTrigger 评估定时触发器
|
||||
func (e *RuleEngine) evaluateScheduleTrigger(cfg TriggerConfig) bool {
|
||||
now := time.Now()
|
||||
|
||||
// 检查 days (星期)
|
||||
if len(cfg.Days) > 0 {
|
||||
weekday := strings.ToLower(now.Weekday().String()[:3])
|
||||
found := false
|
||||
for _, d := range cfg.Days {
|
||||
if strings.ToLower(strings.TrimSpace(d)) == weekday {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 time
|
||||
if cfg.Time != "" {
|
||||
currentTime := now.Format("15:04")
|
||||
return currentTime == cfg.Time
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// evaluateDeviceStateTrigger 评估设备状态触发器
|
||||
func (e *RuleEngine) evaluateDeviceStateTrigger(cfg TriggerConfig) bool {
|
||||
if cfg.DeviceID == "" || cfg.Property == "" || cfg.Operator == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 从 IoT 服务获取设备状态
|
||||
devices, err := e.fetchIoTDevices()
|
||||
if err != nil {
|
||||
log.Printf("[RuleEngine] 获取设备状态失败: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// 查找目标设备
|
||||
for _, d := range devices {
|
||||
if d.ID != cfg.DeviceID {
|
||||
continue
|
||||
}
|
||||
|
||||
var actualValue float64
|
||||
switch cfg.Property {
|
||||
case "temperature":
|
||||
actualValue = d.Temperature
|
||||
case "value":
|
||||
actualValue = d.Value
|
||||
case "brightness":
|
||||
actualValue = float64(d.Brightness)
|
||||
case "position":
|
||||
actualValue = float64(d.Position)
|
||||
case "battery":
|
||||
actualValue = float64(d.Battery)
|
||||
default:
|
||||
// 尝试从 properties 中获取
|
||||
if props, ok := d.Properties[cfg.Property]; ok {
|
||||
if v, ok := props.(float64); ok {
|
||||
actualValue = v
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return compareValues(actualValue, cfg.Operator, cfg.Value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// evaluateCondition 评估单个条件
|
||||
func (e *RuleEngine) evaluateCondition(cond Condition) bool {
|
||||
switch cond.Type {
|
||||
case "time_range":
|
||||
if cond.Start == "" || cond.End == "" {
|
||||
return true
|
||||
}
|
||||
now := time.Now()
|
||||
currentTime := now.Format("15:04")
|
||||
return currentTime >= cond.Start && currentTime <= cond.End
|
||||
|
||||
case "device_state":
|
||||
if cond.DeviceID == "" || cond.Property == "" || cond.Operator == "" {
|
||||
return true
|
||||
}
|
||||
devices, err := e.fetchIoTDevices()
|
||||
if err != nil {
|
||||
return true // 无法获取设备状态时不阻塞
|
||||
}
|
||||
|
||||
for _, d := range devices {
|
||||
if d.ID != cond.DeviceID {
|
||||
continue
|
||||
}
|
||||
|
||||
var actualValue float64
|
||||
switch cond.Property {
|
||||
case "temperature":
|
||||
actualValue = d.Temperature
|
||||
case "value":
|
||||
actualValue = d.Value
|
||||
case "brightness":
|
||||
actualValue = float64(d.Brightness)
|
||||
case "position":
|
||||
actualValue = float64(d.Position)
|
||||
case "battery":
|
||||
actualValue = float64(d.Battery)
|
||||
default:
|
||||
if props, ok := d.Properties[cond.Property]; ok {
|
||||
if v, ok := props.(float64); ok {
|
||||
actualValue = v
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return compareValues(actualValue, cond.Operator, cond.Value)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ExecuteRuleActions 执行规则的动作
|
||||
func (e *RuleEngine) ExecuteRuleActions(rule *store.AutomationRule) {
|
||||
var actions []Action
|
||||
if rule.Actions != nil {
|
||||
if err := json.Unmarshal(*rule.Actions, &actions); err != nil {
|
||||
log.Printf("[RuleEngine] 解析动作失败: rule=%s err=%v", rule.ID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[RuleEngine] 执行规则 %s (%s) 的 %d 个动作", rule.ID, rule.Name, len(actions))
|
||||
|
||||
for _, action := range actions {
|
||||
switch action.Type {
|
||||
case "set_device":
|
||||
e.executeSetDevice(action)
|
||||
case "notify":
|
||||
e.executeNotify(action, rule.UserID)
|
||||
default:
|
||||
log.Printf("[RuleEngine] 未知动作类型: %s", action.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteScene 手动触发场景
|
||||
func (e *RuleEngine) ExecuteScene(sceneID, userID string) error {
|
||||
rules, err := e.store.GetSceneRules(sceneID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取场景规则失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[RuleEngine] 执行场景 %s,共 %d 条关联规则", sceneID, len(rules))
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.Enabled {
|
||||
e.ExecuteRuleActions(&rule)
|
||||
e.store.MarkRuleTriggered(rule.ID)
|
||||
e.mu.Lock()
|
||||
e.lastTriggered[rule.ID] = time.Now()
|
||||
e.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeSetDevice 执行设备控制动作
|
||||
func (e *RuleEngine) executeSetDevice(action Action) {
|
||||
url := fmt.Sprintf("%s/api/v1/devices/%s/set", e.iotServiceURL, action.DeviceID)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"property": action.Property,
|
||||
"value": action.Value,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
resp, err := e.httpClient.Post(url, "application/json", bytes.NewReader(bodyBytes))
|
||||
if err != nil {
|
||||
log.Printf("[RuleEngine] 设备控制请求失败: device=%s property=%s err=%v",
|
||||
action.DeviceID, action.Property, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
log.Printf("[RuleEngine] 设备控制成功: device=%s property=%s value=%v",
|
||||
action.DeviceID, action.Property, action.Value)
|
||||
} else {
|
||||
log.Printf("[RuleEngine] 设备控制失败: device=%s property=%s status=%d",
|
||||
action.DeviceID, action.Property, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// executeNotify 执行通知动作
|
||||
func (e *RuleEngine) executeNotify(action Action, userID string) {
|
||||
notif := ws.NotificationInfo{
|
||||
ID: "notif_" + randomID(),
|
||||
Type: "info",
|
||||
Title: action.Title,
|
||||
Body: action.Body,
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
msg := ws.ServerMessage{
|
||||
Type: "notification",
|
||||
MessageID: "notif_" + randomID(),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
Notification: ¬if,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
log.Printf("[RuleEngine] 序列化通知失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
e.hub.SendToUser(userID, data)
|
||||
log.Printf("[RuleEngine] 通知已发送: user=%s title=%s", userID, action.Title)
|
||||
}
|
||||
|
||||
// ========== 辅助方法 ==========
|
||||
|
||||
// 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"`
|
||||
Properties map[string]interface{} `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// fetchIoTDevices 从 IoT 调试服务获取设备列表
|
||||
func (e *RuleEngine) fetchIoTDevices() ([]IotDevice, error) {
|
||||
resp, err := e.httpClient.Get(e.iotServiceURL + "/api/v1/devices")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求IoT服务失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("IoT服务返回状态码 %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("解析IoT设备列表失败: %w", err)
|
||||
}
|
||||
|
||||
return result.Devices, nil
|
||||
}
|
||||
|
||||
// compareValues 比较两个值
|
||||
func compareValues(actual float64, operator string, expected float64) bool {
|
||||
switch operator {
|
||||
case "eq":
|
||||
return actual == expected
|
||||
case "neq":
|
||||
return actual != expected
|
||||
case "gt":
|
||||
return actual > expected
|
||||
case "gte":
|
||||
return actual >= expected
|
||||
case "lt":
|
||||
return actual < expected
|
||||
case "lte":
|
||||
return actual <= expected
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// randomID 使用 crypto/rand 生成随机 ID
|
||||
func randomID() string {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
Reference in New Issue
Block a user