feat: IoT 知识库 + 设备查询控制方式改造
- cyrene_persona.yaml: 新增 smart_home 配置段,定义全屋智能家居知识库、设备能力、房间布局和控制规则 - loader.go: 新增 SmartHomeConfig/RoomConfig/DeviceConfig 结构体解析 YAML - injector.go: BuildSystemPrompt 自动注入智能家居知识库和控制规则 - 新增 buildSmartHomeKB() 和 buildControlRules() 方法 - 新增 joinStrings() 辅助函数 - main.go: 移除 shouldQueryIoT 关键词门控,始终注入 IoT 设备状态到上下文 - 移除未使用的 strings 导入 - IoTStatusBar.tsx: 对所有用户开放 IoT 状态面板(而非仅 dev 模式)
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { useState } from 'react';
|
||||
import { useChatStore } from '@/store/chatStore';
|
||||
import type { IoTDevice } from '@/types/chat';
|
||||
|
||||
const deviceIcons: Record<string, string> = {
|
||||
light: '💡',
|
||||
ac: '❄️',
|
||||
curtain: '🪟',
|
||||
sensor: '🌡️',
|
||||
lock: '🔒',
|
||||
};
|
||||
|
||||
const deviceTypeLabels: Record<string, string> = {
|
||||
light: '灯光',
|
||||
ac: '空调',
|
||||
curtain: '窗帘',
|
||||
sensor: '传感器',
|
||||
lock: '门锁',
|
||||
};
|
||||
|
||||
function getStatusText(device: IoTDevice): string {
|
||||
switch (device.type) {
|
||||
case 'light':
|
||||
return device.status === 'on' ? `亮度 ${device.brightness}%` : '已关闭';
|
||||
case 'ac':
|
||||
return device.status === 'on' ? `${device.temperature}°C` : '已关闭';
|
||||
case 'curtain':
|
||||
return device.status === 'open' ? '已打开' : '已关闭';
|
||||
case 'sensor':
|
||||
return `${device.value}${device.unit === 'celsius' ? '°C' : '%'}`;
|
||||
case 'lock':
|
||||
return `${device.status === 'locked' ? '已锁定' : '已解锁'} · 🔋${device.battery}%`;
|
||||
default:
|
||||
return device.status;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(device: IoTDevice): string {
|
||||
if (device.type === 'lock') {
|
||||
return device.status === 'locked' ? 'text-green-500' : 'text-yellow-500';
|
||||
}
|
||||
if (device.type === 'sensor') {
|
||||
return 'text-blue-400';
|
||||
}
|
||||
return device.status === 'on' || device.status === 'open'
|
||||
? 'text-green-400'
|
||||
: 'text-gray-400';
|
||||
}
|
||||
|
||||
export function IoTStatusBar() {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const devices = useChatStore((s) => s.iotDevices);
|
||||
const lastUpdated = useChatStore((s) => s.iotDevicesLastUpdated);
|
||||
|
||||
// 对所有用户显示 IoT 状态栏(生产环境也可用)
|
||||
const isEnabled = import.meta.env.VITE_DISABLE_IOT_PANEL !== 'true';
|
||||
if (!isEnabled) return null;
|
||||
|
||||
// 没有设备数据时显示空状态
|
||||
if (devices.length === 0) {
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-4 py-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||||
<span>🔌</span>
|
||||
<span>IoT 设备未连接</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 按类型排序:灯光、空调、窗帘、传感器、门锁
|
||||
const sortedDevices = [...devices].sort((a, b) => {
|
||||
const order: Record<string, number> = { light: 1, ac: 2, curtain: 3, sensor: 4, lock: 5 };
|
||||
return (order[a.type] || 99) - (order[b.type] || 99);
|
||||
});
|
||||
|
||||
// 紧凑模式下显示的关键设备(前4个)
|
||||
const previewDevices = sortedDevices.slice(0, 4);
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||
{/* 紧凑状态栏 */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center justify-between px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title="点击展开 IoT 设备详情"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||
IoT
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{previewDevices.map((device) => (
|
||||
<span
|
||||
key={device.id}
|
||||
className={`text-xs flex items-center gap-1 ${getStatusColor(device)}`}
|
||||
title={`${device.name}: ${getStatusText(device)}`}
|
||||
>
|
||||
<span className="text-sm">{deviceIcons[device.type] || '📦'}</span>
|
||||
</span>
|
||||
))}
|
||||
{sortedDevices.length > 4 && (
|
||||
<span className="text-xs text-gray-400">+{sortedDevices.length - 4}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{lastUpdated && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
{Math.floor((Date.now() - lastUpdated) / 1000)}s ago
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-xs text-gray-400 transition-transform ${expanded ? 'rotate-180' : ''}`}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 展开的设备详情 */}
|
||||
{expanded && (
|
||||
<div className="px-4 pb-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{sortedDevices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className="flex items-center gap-2 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<span className="text-lg">{deviceIcons[device.type] || '📦'}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium text-gray-700 dark:text-gray-200 truncate">
|
||||
{device.name}
|
||||
</div>
|
||||
<div className={`text-[11px] ${getStatusColor(device)}`}>
|
||||
{getStatusText(device)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] text-gray-400 text-center">
|
||||
{devices.length} 台设备 · 模拟调试模式
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user