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:
2026-05-16 22:23:12 +08:00
parent 937742df02
commit 7f2961e63e
5 changed files with 584 additions and 200 deletions
@@ -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>
);
}