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
+115 -17
View File
@@ -73,31 +73,52 @@ let cachedToken = null;
let tokenExpiry = 0;
/**
* 获取 Gateway JWT token (通过 admin 凭据登录,缓存直到过期)
* 获取 Gateway JWT token (通过 admin 凭据登录,缓存直到过期,支持重试)
*/
async function getGatewayToken() {
if (cachedToken && Date.now() < tokenExpiry - 60000) {
return cachedToken;
}
try {
const resp = await fetch(`${GATEWAY_URL}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: ADMIN_USERNAME, password: ADMIN_PASSWORD }),
signal: AbortSignal.timeout(5000),
});
if (!resp.ok) {
console.error('[Gateway代理] 登录失败:', resp.status);
const maxRetries = 3;
const baseDelay = 1000;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const resp = await fetch(`${GATEWAY_URL}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: ADMIN_USERNAME, password: ADMIN_PASSWORD }),
signal: AbortSignal.timeout(5000),
});
if (!resp.ok) {
if (attempt < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, attempt);
console.log(`[Gateway代理] 登录失败 (HTTP ${resp.status})${delay / 1000}s 后重试 (${attempt + 1}/${maxRetries})...`);
await new Promise((r) => setTimeout(r, delay));
continue;
}
console.error('[Gateway代理] 登录失败:', resp.status);
return null;
}
const data = await resp.json();
cachedToken = data.token;
tokenExpiry = data.expires ? data.expires * 1000 : Date.now() + 3600000;
console.log('[Gateway代理] 登录成功,token 已缓存');
return cachedToken;
} catch (err) {
if (attempt < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, attempt);
console.log(`[Gateway代理] 登录异常: ${err.message}${delay / 1000}s 后重试 (${attempt + 1}/${maxRetries})...`);
await new Promise((r) => setTimeout(r, delay));
continue;
}
console.error('[Gateway代理] 登录异常 (已达最大重试次数):', err.message);
return null;
}
const data = await resp.json();
cachedToken = data.token;
tokenExpiry = data.expires ? data.expires * 1000 : Date.now() + 3600000;
return cachedToken;
} catch (err) {
console.error('[Gateway代理] 登录异常:', err.message);
return null;
}
return null;
}
/**
@@ -464,6 +485,83 @@ app.delete('/api/logs/:id', (req, res) => {
}
});
// ---- IoT 设备管理 (代理到 iot-debug-service) ----
const IOT_SERVICE_URL = process.env.IOT_DEBUG_SERVICE_URL || 'http://localhost:8083';
/** 通用 IoT 代理:转发请求到 iot-debug-service */
async function proxyToIoT(path, opts = {}) {
const url = `${IOT_SERVICE_URL}${path}`;
const logPrefix = `[IoT代理]`;
try {
console.log(`${logPrefix} ${opts.method || 'GET'} ${path}`);
const resp = await fetch(url, {
...opts,
headers: { 'Content-Type': 'application/json', ...opts.headers },
signal: AbortSignal.timeout(10000),
});
const body = await resp.json().catch(() => null);
if (!resp.ok) {
console.log(`${logPrefix} 请求失败 (HTTP ${resp.status}): ${path}`);
}
return { status: resp.status, body };
} catch (err) {
const isConnRefused = err.message?.includes('ECONNREFUSED') || err.cause?.code === 'ECONNREFUSED';
console.error(`${logPrefix} 请求异常: ${path} - ${err.message}`);
return {
status: 502,
body: {
error: `IoT 调试服务不可达: ${err.message}`,
errorType: isConnRefused ? 'iot_not_running' : 'iot_unreachable',
hint: isConnRefused
? 'IoT 调试服务未启动,请先在「服务管理」面板中启动 IoT Debug 服务'
: 'IoT 调试服务无响应,请检查网络连接和服务状态',
},
};
}
}
// GET /api/iot/devices — 获取所有模拟设备
app.get('/api/iot/devices', async (_req, res) => {
console.log('[IoT] 获取设备列表');
const result = await proxyToIoT('/api/v1/devices');
res.status(result.status).json(result.body);
});
// GET /api/iot/devices/:id — 获取单个设备
app.get('/api/iot/devices/:id', async (req, res) => {
console.log(`[IoT] 获取设备: ${req.params.id}`);
const result = await proxyToIoT(`/api/v1/devices/${req.params.id}`);
res.status(result.status).json(result.body);
});
// POST /api/iot/devices/:id/toggle — 切换设备开关
app.post('/api/iot/devices/:id/toggle', async (req, res) => {
console.log(`[IoT] 切换设备: ${req.params.id}`);
const result = await proxyToIoT(`/api/v1/devices/${req.params.id}/toggle`, { method: 'POST' });
res.status(result.status).json(result.body);
});
// POST /api/iot/devices/:id/set — 设置设备属性
app.post('/api/iot/devices/:id/set', async (req, res) => {
const { field, value } = req.body;
if (!field) {
return res.status(400).json({ error: '缺少 field 参数' });
}
console.log(`[IoT] 设置设备属性: ${req.params.id} -> ${field} = ${value}`);
const result = await proxyToIoT(`/api/v1/devices/${req.params.id}/set`, {
method: 'POST',
body: JSON.stringify({ field, value }),
});
res.status(result.status).json(result.body);
});
// GET /api/iot/devices/:id/history — 获取设备操作历史
app.get('/api/iot/devices/:id/history', async (req, res) => {
console.log(`[IoT] 获取设备历史: ${req.params.id}`);
const result = await proxyToIoT(`/api/v1/devices/${req.params.id}/history`);
res.status(result.status).json(result.body);
});
// ---- 健康检查代理 ----
app.get('/api/proxy/:id/health', async (req, res) => {
const svc = SERVICES[req.params.id];