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)
This commit is contained in:
2026-05-17 14:37:44 +08:00
parent 5d0bb96abe
commit a80bfd12eb
20 changed files with 1299 additions and 58 deletions
+87 -7
View File
@@ -329,6 +329,76 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
font-family: 'JetBrains Mono', monospace; font-size: 11px; line-height: 1.5;
white-space: pre-wrap; word-break: break-all; color: var(--text2);
}
/* IoT 设备控制面板 */
.iot-device-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 14px; }
.iot-device-card {
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
padding: 16px; transition: border-color .2s;
}
.iot-device-card:hover { border-color: var(--accent); }
.iot-device-card.on { border-color: var(--green); }
.iot-device-card.off { border-color: var(--border2); opacity: .85; }
.iot-device-header {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;
}
.iot-device-name { font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; }
.iot-device-type { font-size: 10px; color: var(--text2); text-transform: uppercase; }
.iot-device-status { display: flex; align-items: center; gap: 6px; }
.iot-status-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
.iot-status-dot.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
.iot-status-dot.off { background: var(--text3); }
.iot-device-props { margin: 10px 0; display: flex; flex-direction: column; gap: 6px; }
.iot-prop-row {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
font-size: 12px; padding: 4px 0;
}
.iot-prop-label { color: var(--text2); min-width: 50px; }
.iot-prop-value {
font-family: 'JetBrains Mono', monospace; font-weight: 600; min-width: 45px; text-align: right;
font-size: 12px;
}
.iot-prop-control { display: flex; align-items: center; gap: 6px; flex: 1; justify-content: flex-end; }
.iot-prop-control input[type="range"] { width: 100px; accent-color: var(--accent); }
.iot-device-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.iot-toggle-btn {
padding: 5px 14px; border-radius: var(--radius-sm); border: 1px solid;
cursor: pointer; font-size: 12px; font-weight: 600; transition: all .15s;
font-family: inherit;
}
.iot-toggle-btn.on { background: var(--green-bg); color: var(--green); border-color: var(--green); }
.iot-toggle-btn.on:hover { background: var(--green); color: #000; }
.iot-toggle-btn.off { background: var(--red-bg); color: var(--red); border-color: var(--red); }
.iot-toggle-btn.off:hover { background: var(--red); color: #fff; }
.iot-mode-btn {
padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border);
cursor: pointer; font-size: 11px; background: var(--bg3); color: var(--text);
transition: all .15s; font-family: inherit;
}
.iot-mode-btn:hover { background: var(--bg4); border-color: var(--text2); }
.iot-mode-btn.active { background: var(--accent-bg); color: var(--accent); border-color: var(--accent); }
.iot-color-btn {
width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--border);
cursor: pointer; transition: all .15s; flex-shrink: 0;
}
.iot-color-btn:hover { border-color: var(--text2); transform: scale(1.15); }
.iot-color-btn.active { border-color: var(--accent); box-shadow: 0 0 8px var(--accent); }
.iot-history-panel {
margin-top: 10px; border-top: 1px solid var(--border); padding-top: 8px;
}
.iot-history-item {
font-size: 11px; color: var(--text2); padding: 3px 0; display: flex; gap: 10px;
font-family: 'JetBrains Mono', monospace;
}
.iot-history-item .iot-hist-time { color: var(--text3); min-width: 60px; }
.iot-history-item .iot-hist-action { color: var(--accent); }
.iot-history-item .iot-hist-detail { color: var(--text2); }
.iot-refresh-bar {
display: flex; align-items: center; gap: 10px; margin-bottom: 14px;
}
.iot-last-update { font-size: 11px; color: var(--text3); }
</style>
</head>
<body>
@@ -356,6 +426,10 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<button class="nav-item" data-panel="performance">
<span class="nav-icon">📊</span><span class="nav-label">性能监控</span>
</button>
<button class="nav-item" data-panel="iot">
<span class="nav-icon">🏠</span><span class="nav-label">IoT 设备</span>
<span class="nav-badge" id="iot-badge" style="display:none">0</span>
</button>
<button class="nav-item" data-panel="database">
<span class="nav-icon">🗄️</span><span class="nav-label">数据库监看</span>
<span class="nav-badge" id="db-badge" style="display:none"></span>
@@ -382,6 +456,8 @@ input[type="range"] { accent-color: var(--accent); padding: 0; }
<div class="panel" id="panel-sessions"></div>
<!-- 服务管理 -->
<div class="panel" id="panel-services"></div>
<!-- IoT 设备控制 -->
<div class="panel" id="panel-iot"></div>
<!-- 性能监控 -->
<div class="panel" id="panel-performance"></div>
<!-- 数据库监看 -->
@@ -565,7 +641,7 @@ function switchPanel(name) {
// 更新标题
const titles = {
dashboard: '🏠 仪表盘', memory: '🧠 记忆管理', sessions: '💬 会话监看',
services: '🖥 服务管理', performance: '📊 性能监控', database: '🗄️ 数据库监看',
services: '🖥 服务管理', iot: '🏠 IoT 设备控制', performance: '📊 性能监控', database: '🗄️ 数据库监看',
};
document.getElementById('panel-title').textContent = titles[name] || name;
@@ -578,12 +654,13 @@ function switchPanel(name) {
// 渲染面板
switch (name) {
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); break;
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); break;
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); break;
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); break;
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); break;
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); break;
case 'dashboard': renderDashboard(); startDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'memory': renderMemoryPanel(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'sessions': renderSessionsPanel(); startSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'services': renderServicesPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'iot': renderIoTPanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); startIoTRefresh(); break;
case 'performance': renderPerformancePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); stopDbAutoRefresh(); stopIoTRefresh(); break;
case 'database': renderDatabasePanel(); stopSessionsAutoRefresh(); stopDashboardAutoRefresh(); startDbAutoRefresh(); stopIoTRefresh(); break;
}
}
@@ -1624,6 +1701,9 @@ async function tunnelAction(action) {
}
}
</script>
<script src="iot-panel.js"></script>
<script>
// ========== 初始化 ==========
connectWS();
refreshStatus();