mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
device gateway: remotedevice 插件(设备接入网关) + GUI/waiter 受控设备桥 + 设备页/授权开关/托盘/退出进托盘 + 白屏修复(惰性Tray) + deviceinfo 工具
This commit is contained in:
BIN
cmd/gui/icon-tray.png
Normal file
BIN
cmd/gui/icon-tray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
cmd/gui/icon-tray@2x.png
Normal file
BIN
cmd/gui/icon-tray@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
387
cmd/gui/main.js
387
cmd/gui/main.js
@ -329,6 +329,17 @@ function createWindow() {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// 退出进托盘:拦截窗口关闭事件(exitToTray 且托盘可用时 hide 而非 close)
|
||||
mainWindow.on("close", (e) => {
|
||||
try {
|
||||
const _p = loadGuiPrefs();
|
||||
if (_p && _p.exitToTray && tray) {
|
||||
e.preventDefault();
|
||||
mainWindow.hide();
|
||||
return;
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
@ -344,7 +355,23 @@ ipcMain.handle("window:toggleMaximize", (e) => {
|
||||
else win.maximize();
|
||||
});
|
||||
ipcMain.handle("window:close", (e) => {
|
||||
BrowserWindow.fromWebContents(e.sender)?.close();
|
||||
const win = BrowserWindow.fromWebContents(e.sender);
|
||||
if (!win) return;
|
||||
// 退出进托盘:偏好开启且托盘存在时隐藏而非关闭
|
||||
try {
|
||||
const _p = loadGuiPrefs();
|
||||
console.log(
|
||||
"[window:close] exitToTray=" +
|
||||
!!(_p && _p.exitToTray) +
|
||||
" tray=" +
|
||||
!!tray,
|
||||
);
|
||||
if (_p && _p.exitToTray && tray) {
|
||||
win.hide();
|
||||
return true;
|
||||
}
|
||||
} catch (err) {}
|
||||
win.close();
|
||||
});
|
||||
|
||||
ipcMain.handle("connections:list", () => {
|
||||
@ -742,6 +769,257 @@ ipcMain.handle("cli:request", (_, { socketPath, apiKey, line }) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ============ Device Bridge: GUI 作为设备接入 remotedevice 网关 ============
|
||||
// 复用 net(raw TCP)+ 手写 WS 帧;收到 {op:"cmd"} 用 spawn 在本机执行并回 cmd_result。
|
||||
const devNet = require("net");
|
||||
const devOs = require("os");
|
||||
|
||||
let deviceBridge = null; // 当前活动设备桥
|
||||
let deviceBridgeId = ""; // 设备 meta device_id(hello 后可用于 cmd_result)
|
||||
let deviceBridgeAddr = ""; // 设备桥网关地址
|
||||
|
||||
// 建立到 remotedevice WS 网关连接,返回 {send(obj), close()},消息经 onMsg 回调。
|
||||
function connectDeviceWS(url, token, onMsg) {
|
||||
const m = /^https?:\/\/([^:/]+)(?::(\d+))?/.exec(url || "");
|
||||
const host = m ? m[1] : "127.0.0.1";
|
||||
const port = m && m[2] ? parseInt(m[2], 10) : 9890;
|
||||
const path = "/api/v1/device/ws?token=" + encodeURIComponent(token || "");
|
||||
return new Promise((resolve, reject) => {
|
||||
const sock = devNet.createConnection({ host, port }, () => {
|
||||
const key = crypto.randomBytes(16).toString("base64");
|
||||
sock.write(
|
||||
"GET " +
|
||||
path +
|
||||
" HTTP/1.1\r\n" +
|
||||
"Host: " +
|
||||
host +
|
||||
":" +
|
||||
port +
|
||||
"\r\n" +
|
||||
"Upgrade: websocket\r\nConnection: Upgrade\r\n" +
|
||||
"Sec-WebSocket-Key: " +
|
||||
key +
|
||||
"\r\nSec-WebSocket-Version: 13\r\n\r\n",
|
||||
);
|
||||
});
|
||||
let buf = Buffer.alloc(0);
|
||||
let upgraded = false;
|
||||
let opened = false;
|
||||
sock.on("data", (chunk) => {
|
||||
buf = Buffer.concat([buf, chunk]);
|
||||
if (!upgraded) {
|
||||
const idx = buf.indexOf("\r\n\r\n");
|
||||
if (idx === -1) return;
|
||||
const head = buf.slice(0, idx).toString("utf8");
|
||||
buf = buf.slice(idx + 4);
|
||||
upgraded = true;
|
||||
if (!head.includes("101")) {
|
||||
sock.destroy();
|
||||
return reject(
|
||||
new Error("WS upgrade failed: " + head.split("\r\n")[0]),
|
||||
);
|
||||
}
|
||||
opened = true;
|
||||
resolve({
|
||||
send: (obj) => sendDeviceFrame(sock, JSON.stringify(obj)),
|
||||
close: () => sock.destroy(),
|
||||
});
|
||||
}
|
||||
while (buf.length >= 2) {
|
||||
const b0 = buf[0];
|
||||
const opcode = b0 & 0x0f;
|
||||
const b1 = buf[1];
|
||||
let len = b1 & 0x7f;
|
||||
let off = 2;
|
||||
if (len === 126) {
|
||||
if (buf.length < 4) break;
|
||||
len = buf.readUInt16BE(2);
|
||||
off = 4;
|
||||
} else if (len === 127) {
|
||||
if (buf.length < 10) break;
|
||||
len = Number(buf.readBigUInt64BE(2));
|
||||
off = 10;
|
||||
}
|
||||
if (buf.length < off + len) break;
|
||||
const payload = buf.slice(off, off + len);
|
||||
buf = buf.slice(off + len);
|
||||
if (opcode === 0x1) {
|
||||
try {
|
||||
onMsg(JSON.parse(payload.toString("utf8")));
|
||||
} catch (e) {}
|
||||
} else if (opcode === 0x8) {
|
||||
sock.destroy();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
sock.on("error", (e) => {
|
||||
if (!opened) reject(e);
|
||||
});
|
||||
sock.on("close", () => {
|
||||
deviceBridge = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 发送 WS text 帧(客户端加掩码)
|
||||
function sendDeviceFrame(sock, text) {
|
||||
const payload = Buffer.from(text, "utf8");
|
||||
const mask = crypto.randomBytes(4);
|
||||
const masked = Buffer.from(payload);
|
||||
for (let i = 0; i < masked.length; i++) masked[i] ^= mask[i % 4];
|
||||
const len = masked.length;
|
||||
let hdr;
|
||||
if (len < 126) {
|
||||
hdr = Buffer.from([0x81, 0x80 | len]);
|
||||
} else if (len < 65536) {
|
||||
hdr = Buffer.alloc(4);
|
||||
hdr[0] = 0x81;
|
||||
hdr[1] = 0x80 | 126;
|
||||
hdr.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
hdr = Buffer.alloc(10);
|
||||
hdr[0] = 0x81;
|
||||
hdr[1] = 0x80 | 127;
|
||||
hdr.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
sock.write(Buffer.concat([hdr, mask, masked]));
|
||||
}
|
||||
|
||||
// 启动设备桥:以 device 连接(url=网关地址, apiKey=token)接入网关。
|
||||
async function startDeviceBridge(conn) {
|
||||
if (!conn || conn.type !== "device") return;
|
||||
const token = conn.apiKey || "";
|
||||
if (!token) {
|
||||
console.error("[device-bridge] missing token");
|
||||
return;
|
||||
}
|
||||
deviceBridgeAddr = conn.url || "";
|
||||
deviceBridgeId =
|
||||
"gui-" + (devOs.hostname() || "local").replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
try {
|
||||
const ws = await connectDeviceWS(conn.url, token, onDeviceMsg);
|
||||
deviceBridge = ws;
|
||||
ws.send({
|
||||
op: "hello",
|
||||
device: {
|
||||
device_id: deviceBridgeId,
|
||||
name: "HomeAgent GUI",
|
||||
kind: "computer",
|
||||
caps: ["status", "cmdrun", "deviceinfo", "cmdresult"],
|
||||
info: {
|
||||
hostname: devOs.hostname() || "",
|
||||
platform: process.platform || "",
|
||||
arch: process.arch || "",
|
||||
os_release: "", // 不提权读取 /etc/os-release,避免破坏沙箱;如需可在白名单命令里由 agent 探
|
||||
node_version:
|
||||
process.versions && process.versions.node
|
||||
? process.versions.node
|
||||
: "",
|
||||
electron_version:
|
||||
process.versions && process.versions.electron
|
||||
? process.versions.electron
|
||||
: "",
|
||||
version: app.getVersion ? app.getVersion() : "",
|
||||
cpus: devOs.cpus ? devOs.cpus().length : 0,
|
||||
total_mem_bytes: devOs.totalmem ? devOs.totalmem() : 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
ws.send({ op: "bind", device_id: deviceBridgeId, token });
|
||||
console.log(
|
||||
"[device-bridge] connected as " + deviceBridgeId + " @ " + conn.url,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[device-bridge] connect failed: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function stopDeviceBridge() {
|
||||
if (deviceBridge) {
|
||||
try {
|
||||
deviceBridge.close();
|
||||
} catch (e) {}
|
||||
deviceBridge = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 系统托盘(惰性 + 安全降级) ============
|
||||
let tray = null;
|
||||
function initTray() {
|
||||
if (tray) return;
|
||||
try {
|
||||
const electron = require("electron");
|
||||
const Tray = electron.Tray;
|
||||
const TMenu = electron.Menu;
|
||||
const nImg = electron.nativeImage;
|
||||
let img = null;
|
||||
const cands = [
|
||||
path.join(__dirname, "icon-tray@2x.png"),
|
||||
path.join(__dirname, "icon-tray.png"),
|
||||
path.join(__dirname, "icon.ico"),
|
||||
];
|
||||
for (const c of cands) {
|
||||
try {
|
||||
const m = nImg.createFromPath(c);
|
||||
if (m && !m.isEmpty()) {
|
||||
img = m;
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!img || img.isEmpty()) {
|
||||
try {
|
||||
img = nImg.createFromPath(path.join(__dirname, "icon-tray.png"));
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!img || img.isEmpty()) {
|
||||
img = nImg.createEmpty();
|
||||
}
|
||||
tray = new Tray(img);
|
||||
const tmenu = TMenu.buildFromTemplate([
|
||||
{ label: "HomeAgent", enabled: false },
|
||||
{ type: "separator" },
|
||||
{ label: "显示主界面", click: () => showMainWindow() },
|
||||
{
|
||||
label: "退出",
|
||||
click: () => {
|
||||
app.isQuitting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
tray.setToolTip("HomeAgent - 个人智能管家");
|
||||
tray.setContextMenu(tmenu);
|
||||
tray.on("double-click", () => showMainWindow());
|
||||
console.log("[tray] READY: " + (tray ? "tray-created" : "null"));
|
||||
} catch (e) {
|
||||
console.error("[tray] init failed (safe ignore): " + e.message);
|
||||
try {
|
||||
if (tray) {
|
||||
tray.destroy();
|
||||
tray = null;
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
}
|
||||
function showMainWindow() {
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
function destroyTray() {
|
||||
try {
|
||||
if (tray) {
|
||||
tray.destroy();
|
||||
tray = null;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
installAuthRule();
|
||||
const running = await isServerRunning();
|
||||
@ -754,13 +1032,46 @@ app.whenReady().then(async () => {
|
||||
console.error("homed failed to start within timeout");
|
||||
}
|
||||
}
|
||||
// 设备桥:启动接入 remotedevice
|
||||
try {
|
||||
var _devs = loadConnections().connections || [];
|
||||
var _dev = _devs.filter((c) => c.type === "device");
|
||||
if (_dev.length > 0) startDeviceBridge(_dev[_dev.length - 1]);
|
||||
} catch (e) {
|
||||
console.error("device bridge init: " + e.message);
|
||||
}
|
||||
try {
|
||||
initTray();
|
||||
} catch (e) {
|
||||
console.error("[tray] whenReady call: " + e.message);
|
||||
}
|
||||
createWindow();
|
||||
});
|
||||
|
||||
app.on("before-quit", stopHomed);
|
||||
app.on("before-quit", () => {
|
||||
stopHomed();
|
||||
try {
|
||||
stopDeviceBridge();
|
||||
} catch (e) {}
|
||||
try {
|
||||
destroyTray();
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
// 退出进托盘:依偏好决定(安全降级:无托盘时退出)
|
||||
let _exitTray = false;
|
||||
try {
|
||||
const _p = loadGuiPrefs();
|
||||
_exitTray = !!(_p && _p.exitToTray);
|
||||
} catch (e) {}
|
||||
if (_exitTray && tray) {
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.hide();
|
||||
} catch (e) {}
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
@ -768,3 +1079,73 @@ app.on("activate", () => {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
// IPC:prefs(renderer 设置页需要)
|
||||
// IPC:本机设备身份(renderer 设备页需要)
|
||||
ipcMain.handle("device:identity", () =>
|
||||
deviceBridge
|
||||
? {
|
||||
device_id: deviceBridgeId,
|
||||
active: true,
|
||||
address: deviceBridgeAddr || "",
|
||||
}
|
||||
: { device_id: "", active: false, address: "" },
|
||||
);
|
||||
const GUI_PREFS_FILE = path.join(app.getPath("userData"), "gui-prefs.json");
|
||||
|
||||
function loadGuiPrefs() {
|
||||
try {
|
||||
if (fs.existsSync(GUI_PREFS_FILE)) {
|
||||
const d = JSON.parse(fs.readFileSync(GUI_PREFS_FILE, "utf-8"));
|
||||
return {
|
||||
autoLaunch: !!d.autoLaunch,
|
||||
silentStart: !!d.silentStart,
|
||||
exitToTray: d.exitToTray === undefined ? true : !!d.exitToTray,
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
return { autoLaunch: false, silentStart: false, exitToTray: true };
|
||||
}
|
||||
|
||||
function saveGuiPrefs(p) {
|
||||
try {
|
||||
fs.writeFileSync(GUI_PREFS_FILE, JSON.stringify(p, null, 2), "utf-8");
|
||||
} catch (e) {}
|
||||
return p;
|
||||
}
|
||||
|
||||
// 应用开机自启设置(Electron LoginItem)
|
||||
function applyAutoLaunch(enabled) {
|
||||
try {
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: enabled,
|
||||
openAsHidden: true, // 开机自启时静默(Windows/macOS 支持)
|
||||
path: process.execPath,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("setLoginItemSettings failed: " + e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局偏好缓存(供 createWindow 静默判断使用)
|
||||
let guiPrefs = loadGuiPrefs();
|
||||
|
||||
// IPC:本机设备身份(设备桥登记的设备 ID 与状态)
|
||||
|
||||
// IPC:读取偏好
|
||||
ipcMain.handle("prefs:get", () => {
|
||||
return loadGuiPrefs();
|
||||
});
|
||||
// IPC:写入并应用偏好
|
||||
ipcMain.handle("prefs:set", (_, p) => {
|
||||
const cur = loadGuiPrefs();
|
||||
const next = Object.assign({}, cur, p || {});
|
||||
if (typeof next.autoLaunch === "boolean") {
|
||||
next.autoLaunch = applyAutoLaunch(next.autoLaunch)
|
||||
? next.autoLaunch
|
||||
: false;
|
||||
}
|
||||
guiPrefs = saveGuiPrefs(next);
|
||||
return guiPrefs;
|
||||
});
|
||||
|
||||
77
cmd/gui/package-lock.json
generated
77
cmd/gui/package-lock.json
generated
@ -7,11 +7,9 @@
|
||||
"": {
|
||||
"name": "homeagent-gui",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"electron": "^33.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"asar": "^3.2.0",
|
||||
"electron": "^33.0.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-packager": "^17.1.2",
|
||||
"icojs": "^0.23.0",
|
||||
@ -107,6 +105,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
@ -1074,6 +1073,7 @@
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -1086,6 +1086,7 @@
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
@ -1123,6 +1124,7 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
@ -1164,12 +1166,14 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@ -1196,6 +1200,7 @@
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@ -1205,6 +1210,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
"integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@ -1214,6 +1220,7 @@
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -1873,6 +1880,7 @@
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@ -1890,6 +1898,7 @@
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@ -1999,6 +2008,7 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
@ -2008,6 +2018,7 @@
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clone-response": "^1.0.2",
|
||||
@ -2099,6 +2110,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^1.0.0"
|
||||
@ -2217,6 +2229,7 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@ -2263,6 +2276,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
@ -2278,6 +2292,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -2290,6 +2305,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -2299,6 +2315,7 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -2317,6 +2334,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -2354,6 +2372,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@ -2483,6 +2502,7 @@
|
||||
"version": "33.4.11",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz",
|
||||
"integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2751,6 +2771,7 @@
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
@ -2760,6 +2781,7 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -2784,7 +2806,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -2794,7 +2816,7 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -2831,6 +2853,7 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@ -2847,6 +2870,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@ -2866,6 +2890,7 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
@ -2908,6 +2933,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
@ -3085,6 +3111,7 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
@ -3239,6 +3266,7 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
@ -3275,6 +3303,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -3293,6 +3322,7 @@
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
@ -3306,6 +3336,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||
"integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -3323,7 +3354,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@ -3336,6 +3367,7 @@
|
||||
"version": "11.8.6",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
@ -3361,6 +3393,7 @@
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
@ -3376,6 +3409,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -3434,6 +3468,7 @@
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
@ -3453,6 +3488,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
@ -3670,6 +3706,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
@ -3682,6 +3719,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
@ -3701,6 +3739,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
@ -3719,6 +3758,7 @@
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
@ -3775,6 +3815,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -3802,6 +3843,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -3857,6 +3899,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@ -3921,6 +3964,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
@ -4074,6 +4118,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -4086,6 +4131,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@ -4096,6 +4142,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
@ -4105,6 +4152,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -4230,6 +4278,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
@ -4359,6 +4408,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@ -4392,6 +4442,7 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
@ -4420,6 +4471,7 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -4555,12 +4607,14 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
@ -4596,6 +4650,7 @@
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -4638,6 +4693,7 @@
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -4647,6 +4703,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@ -4654,6 +4711,7 @@
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -4835,6 +4893,7 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
@ -4933,6 +4992,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0"
|
||||
@ -5158,6 +5218,7 @@
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@ -5193,12 +5254,14 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
@ -5323,6 +5386,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
@ -5383,6 +5447,7 @@
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
|
||||
@ -1,26 +1,44 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld('homeagent', {
|
||||
contextBridge.exposeInMainWorld("homeagent", {
|
||||
win: {
|
||||
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||
toggleMaximize: () => ipcRenderer.invoke('window:toggleMaximize'),
|
||||
close: () => ipcRenderer.invoke('window:close'),
|
||||
minimize: () => ipcRenderer.invoke("window:minimize"),
|
||||
toggleMaximize: () => ipcRenderer.invoke("window:toggleMaximize"),
|
||||
close: () => ipcRenderer.invoke("window:close"),
|
||||
},
|
||||
connections: {
|
||||
list: () => ipcRenderer.invoke('connections:list'),
|
||||
add: (conn) => ipcRenderer.invoke('connections:add', conn),
|
||||
update: (id, updates) => ipcRenderer.invoke('connections:update', { id, updates }),
|
||||
delete: (id) => ipcRenderer.invoke('connections:delete', id),
|
||||
setCurrent: (id) => ipcRenderer.invoke('connections:setCurrent', id),
|
||||
list: () => ipcRenderer.invoke("connections:list"),
|
||||
add: (conn) => ipcRenderer.invoke("connections:add", conn),
|
||||
update: (id, updates) =>
|
||||
ipcRenderer.invoke("connections:update", { id, updates }),
|
||||
delete: (id) => ipcRenderer.invoke("connections:delete", id),
|
||||
setCurrent: (id) => ipcRenderer.invoke("connections:setCurrent", id),
|
||||
},
|
||||
cli: {
|
||||
request: (socketPath, apiKey, line) => ipcRenderer.invoke('cli:request', { socketPath, apiKey, line }),
|
||||
request: (socketPath, apiKey, line) =>
|
||||
ipcRenderer.invoke("cli:request", { socketPath, apiKey, line }),
|
||||
},
|
||||
webui: {
|
||||
setAuth: (url, cookie, headers, username, password) => ipcRenderer.invoke('webui:setAuth', { url, cookie, headers, username, password }),
|
||||
openLogin: (url, username, password) => ipcRenderer.invoke('webui:openLogin', { url, username, password }),
|
||||
onLoginResult: (cb) => ipcRenderer.on('webui:login-result', (_e, d) => cb(d)),
|
||||
setAuth: (url, cookie, headers, username, password) =>
|
||||
ipcRenderer.invoke("webui:setAuth", {
|
||||
url,
|
||||
cookie,
|
||||
headers,
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
openLogin: (url, username, password) =>
|
||||
ipcRenderer.invoke("webui:openLogin", { url, username, password }),
|
||||
onLoginResult: (cb) =>
|
||||
ipcRenderer.on("webui:login-result", (_e, d) => cb(d)),
|
||||
},
|
||||
cacheBg: (src) => ipcRenderer.invoke("bg:cache", { src }),
|
||||
log: (m) => ipcRenderer.invoke("log:r", m),
|
||||
prefs: {
|
||||
get: () => ipcRenderer.invoke("prefs:get"),
|
||||
set: (p) => ipcRenderer.invoke("prefs:set", p),
|
||||
},
|
||||
device: {
|
||||
identity: () => ipcRenderer.invoke("device:identity"),
|
||||
},
|
||||
cacheBg: (src) => ipcRenderer.invoke('bg:cache', { src }),
|
||||
log: (m) => ipcRenderer.invoke('log:r', m),
|
||||
});
|
||||
|
||||
@ -27,6 +27,9 @@ const state = {
|
||||
lang: localStorage.getItem("ha-lang") || "zh",
|
||||
connections: [],
|
||||
currentConn: null,
|
||||
devices: [],
|
||||
selfDeviceId: "",
|
||||
selfGateway: "",
|
||||
};
|
||||
|
||||
// ===== I18n =====
|
||||
@ -525,6 +528,11 @@ function renderAll() {
|
||||
} catch (e) {
|
||||
console.error("renderAdapters", e);
|
||||
}
|
||||
try {
|
||||
renderDevices();
|
||||
} catch (e) {
|
||||
console.error("renderDevices", e);
|
||||
}
|
||||
applyI18n();
|
||||
applyCardTilt();
|
||||
refreshAll();
|
||||
@ -557,6 +565,28 @@ async function refreshAll() {
|
||||
try {
|
||||
await loadCmdHistory();
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (state.currentConn && state.currentConn.type === "device") {
|
||||
var d = await api("/device");
|
||||
state.devices = d.devices || [];
|
||||
}
|
||||
} catch (e) {
|
||||
state.devices = [];
|
||||
}
|
||||
try {
|
||||
if (
|
||||
state.currentConn &&
|
||||
state.currentConn.type === "device" &&
|
||||
window.homeagent &&
|
||||
window.homeagent.device
|
||||
) {
|
||||
var idn = await window.homeagent.device.identity();
|
||||
if (idn) {
|
||||
state.selfDeviceId = idn.device_id || "";
|
||||
state.selfGateway = idn.address || "";
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
renderOverview();
|
||||
} catch (e) {
|
||||
@ -592,6 +622,11 @@ async function refreshAll() {
|
||||
} catch (e) {
|
||||
console.error("renderAdapters", e);
|
||||
}
|
||||
try {
|
||||
renderDevices();
|
||||
} catch (e) {
|
||||
console.error("renderDevices", e);
|
||||
}
|
||||
applyI18n();
|
||||
applyCardTilt();
|
||||
}
|
||||
@ -970,19 +1005,33 @@ function chanLetter(src) {
|
||||
function renderReasoningCard(text, isStreaming, idx) {
|
||||
var preview =
|
||||
typeof marked === "undefined"
|
||||
? escHtml(text).replace(/<[^>]+>/g, " ").slice(0, 60)
|
||||
? escHtml(text)
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.slice(0, 60)
|
||||
: text.replace(/[\s\n]+/g, " ").slice(0, 60);
|
||||
return (
|
||||
'<div class="reasoning-card' + (isStreaming ? " rc-streaming" : "") + '" data-idx="' + (idx | 0) + '">' +
|
||||
'<div class="reasoning-card' +
|
||||
(isStreaming ? " rc-streaming" : "") +
|
||||
'" data-idx="' +
|
||||
(idx | 0) +
|
||||
'">' +
|
||||
'<div class="reasoning-head" onclick="toggleReasoning(this)">' +
|
||||
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rc-ico"><path d="M9 3a2 2 0 0 0-2 2v2a2 2 0 0 1-2 2H3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2a2 2 0 0 1 2 2v2a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2a2 2 0 0 1-2-2V3a2 2 0 0 0-2-2H9zM12 8v4m0 4h.01"/></svg>' +
|
||||
'<span class="rc-title">' + (isStreaming ? __("思考中...","Thinking...") : __("思考","Thinking")) + '</span>' +
|
||||
'<span class="rc-title">' +
|
||||
(isStreaming ? __("思考中...", "Thinking...") : __("思考", "Thinking")) +
|
||||
"</span>" +
|
||||
'<span class="rc-chev">▾</span></div>' +
|
||||
'<div class="reasoning-body" style="display:' + (isStreaming ? "block" : "none") + '">' +
|
||||
'<div class="reasoning-body" style="display:' +
|
||||
(isStreaming ? "block" : "none") +
|
||||
'">' +
|
||||
(isStreaming
|
||||
? '<div class="reasoning-preview">' + escHtml(preview) + '</div><div class="reasoning-sweep"></div>'
|
||||
: '<div class="reasoning-content">' + (typeof marked !== "undefined" ? marked.parse(text) : escHtml(text)) + '</div>') +
|
||||
'</div></div>'
|
||||
? '<div class="reasoning-preview">' +
|
||||
escHtml(preview) +
|
||||
'</div><div class="reasoning-sweep"></div>'
|
||||
: '<div class="reasoning-content">' +
|
||||
(typeof marked === "undefined" ? escHtml(text) : marked.parse(text)) +
|
||||
"</div>") +
|
||||
"</div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
@ -1106,20 +1155,29 @@ function renderChat() {
|
||||
: String(tc.result)
|
||||
: "";
|
||||
var running = !resultStr && tc.status !== "denied";
|
||||
var error = tc.status === "error" || tc.status === "denied" || !!tc.error;
|
||||
var drip = newlyDone.indexOf(tc.tool || tc.name || "") !== -1 ? " tool-drip-in" : "";
|
||||
var iconSvg =
|
||||
error
|
||||
? '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M5.6 5.6l12.8 12.8"/></svg>'
|
||||
: running
|
||||
? '<span class="tc-spinner"></span>'
|
||||
: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>';
|
||||
var error =
|
||||
tc.status === "error" || tc.status === "denied" || !!tc.error;
|
||||
var drip =
|
||||
newlyDone.indexOf(tc.tool || tc.name || "") === -1
|
||||
? ""
|
||||
: " tool-drip-in";
|
||||
var iconSvg = error
|
||||
? '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M5.6 5.6l12.8 12.8"/></svg>'
|
||||
: running
|
||||
? '<span class="tc-spinner"></span>'
|
||||
: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.9 2.9-2.5-.6-.6-2.5z"/></svg>';
|
||||
var statusHtml =
|
||||
tc.status === "denied"
|
||||
? '<span class="tc-state tc-deny">' + __("已拒绝","Denied") + "</span>"
|
||||
? '<span class="tc-state tc-deny">' +
|
||||
__("已拒绝", "Denied") +
|
||||
"</span>"
|
||||
: running
|
||||
? '<span class="tc-state tc-run">' + __("调用中","Running") + "</span>"
|
||||
: '<span class="tc-state tc-done">' + __("完成","Done") + "</span>";
|
||||
? '<span class="tc-state tc-run">' +
|
||||
__("调用中", "Running") +
|
||||
"</span>"
|
||||
: '<span class="tc-state tc-done">' +
|
||||
__("完成", "Done") +
|
||||
"</span>";
|
||||
var pluginHtml = tc.plugin
|
||||
? '<span class="tc-plugin">' + escHtml(tc.plugin) + "</span>"
|
||||
: "";
|
||||
@ -1140,10 +1198,18 @@ function renderChat() {
|
||||
'<span class="tc-caret">▾</span></div>' +
|
||||
'<div class="tc-detail" style="display:none">' +
|
||||
(argsStr && argsStr !== "{}"
|
||||
? '<div class="tc-args"><div class="tc-detail-label">' + __("参数","Args") + "</div>" + escHtml(argsStr) + "</div>"
|
||||
? '<div class="tc-args"><div class="tc-detail-label">' +
|
||||
__("参数", "Args") +
|
||||
"</div>" +
|
||||
escHtml(argsStr) +
|
||||
"</div>"
|
||||
: "") +
|
||||
(resultStr
|
||||
? '<div class="tc-result"><div class="tc-detail-label">' + __("结果","Result") + "</div>" + escHtml(resultStr) + "</div>"
|
||||
? '<div class="tc-result"><div class="tc-detail-label">' +
|
||||
__("结果", "Result") +
|
||||
"</div>" +
|
||||
escHtml(resultStr) +
|
||||
"</div>"
|
||||
: "") +
|
||||
"</div></div>";
|
||||
});
|
||||
@ -1329,9 +1395,10 @@ function toggleReasoning(el) {
|
||||
var content = card.querySelector(".reasoning-content");
|
||||
if (content && !content.childElementCount) {
|
||||
var idx = parseInt(card.getAttribute("data-idx"), 10) || 0;
|
||||
var text = (state.messages[idx] && state.messages[idx].reasoning_content) || "";
|
||||
var text =
|
||||
(state.messages[idx] && state.messages[idx].reasoning_content) || "";
|
||||
content.innerHTML =
|
||||
typeof marked !== "undefined" ? marked.parse(text) : escHtml(text);
|
||||
typeof marked === "undefined" ? escHtml(text) : marked.parse(text);
|
||||
}
|
||||
body.style.display = "block";
|
||||
card.classList.add("open");
|
||||
@ -1691,6 +1758,16 @@ async function sendChat() {
|
||||
var btn = document.getElementById("chat-send-btn");
|
||||
var text = inp.value.trim();
|
||||
if (!text || state.chatLoading) return;
|
||||
if (state.currentConn && state.currentConn.type === "device") {
|
||||
toast(
|
||||
__(
|
||||
"设备网关连接不支持聊天",
|
||||
"Device gateway connection does not support chat",
|
||||
),
|
||||
true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
state.chatStick = true;
|
||||
state.chatFinalIdx = -1;
|
||||
state.messages.push({ role: "user", content: text });
|
||||
@ -1702,51 +1779,57 @@ async function sendChat() {
|
||||
btn.textContent = "";
|
||||
rerenderChat();
|
||||
try {
|
||||
var r = await api("/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message: text }),
|
||||
timeout: 120000,
|
||||
});
|
||||
state.chatStage = "";
|
||||
var last = state.messages[state.messages.length - 1];
|
||||
console.log(
|
||||
"[sendChat] POST returned, last msg:",
|
||||
last
|
||||
? {
|
||||
role: last.role,
|
||||
_streaming: last._streaming,
|
||||
_final: last._final,
|
||||
tool_calls: last.tool_calls?.length,
|
||||
content_len: last.content?.length,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
if (last && last.role === "assistant" && last._streaming) {
|
||||
console.log(
|
||||
"[sendChat] updating existing streaming msg, tool_calls before:",
|
||||
last.tool_calls?.length,
|
||||
);
|
||||
last.content = r.response || __("(无响应)", "(no response)");
|
||||
last._grow = true;
|
||||
if (!last.reasoning_content)
|
||||
last.reasoning_content = r.reasoning_content || "";
|
||||
last._final = true;
|
||||
delete last._streaming;
|
||||
} else {
|
||||
state.messages.push({
|
||||
role: "assistant",
|
||||
content: r.response || __("(无响应)", "(no response)"),
|
||||
reasoning_content: r.reasoning_content,
|
||||
tool_calls:
|
||||
last && last.role === "assistant" && last.tool_calls
|
||||
? last.tool_calls
|
||||
: [],
|
||||
_final: true,
|
||||
_grow: true,
|
||||
});
|
||||
// webui 连接:触发式 + SSE 流式接管(避免 POST 与 SSE 双通道重复渲染卡死)
|
||||
var isWebui = !state.currentConn || state.currentConn.type !== "cli";
|
||||
var r = null;
|
||||
try {
|
||||
if (isWebui) {
|
||||
// 触发请求:短超时确认受理,回复靠 SSE
|
||||
r = await api("/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message: text }),
|
||||
timeout: 15000,
|
||||
headers: { "X-Trigger-Only": "1" },
|
||||
});
|
||||
} else {
|
||||
r = await api("/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message: text }),
|
||||
timeout: 120000,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 触发请求超时/失败:不阻塞 UI,等 SSE 兜底;若 SSE 也无响应则报错
|
||||
console.warn("[sendChat] trigger failed: " + e.message);
|
||||
r = null;
|
||||
}
|
||||
state.chatFinalIdx = state.messages.length - 1;
|
||||
rerenderChat();
|
||||
state.chatStage = __("AI 回复中...", "AI replying...");
|
||||
var last = state.messages[state.messages.length - 1];
|
||||
// CLI/device 无 SSE:POST 完整结果直接填充
|
||||
if (!isWebui && r) {
|
||||
if (last && last.role === "assistant" && last._streaming) {
|
||||
last.content = r.response || __("(无响应)", "(no response)");
|
||||
last._grow = true;
|
||||
if (!last.reasoning_content)
|
||||
last.reasoning_content = r.reasoning_content || "";
|
||||
last._final = true;
|
||||
delete last._streaming;
|
||||
} else {
|
||||
state.messages.push({
|
||||
role: "assistant",
|
||||
content: r.response || __("(无响应)", "(no response)"),
|
||||
reasoning_content: r.reasoning_content,
|
||||
tool_calls:
|
||||
last && last.role === "assistant" && last.tool_calls
|
||||
? last.tool_calls
|
||||
: [],
|
||||
_final: true,
|
||||
_grow: true,
|
||||
});
|
||||
}
|
||||
state.chatFinalIdx = state.messages.length - 1;
|
||||
}
|
||||
if (!isWebui) rerenderChat();
|
||||
} catch (e) {
|
||||
state.messages.push({
|
||||
role: "assistant",
|
||||
@ -2950,6 +3033,7 @@ function renderOneSettings() {
|
||||
__("后端连接", "Backend Connections") +
|
||||
"</h2>" +
|
||||
'<div id="conn-manager"></div></div>' +
|
||||
'<div id="gui-prefs"></div>' +
|
||||
'<div class="settings-tabs" id="settings-tabs"></div>' +
|
||||
'<div class="settings-content">';
|
||||
if (
|
||||
@ -3290,8 +3374,10 @@ function renderOneSettings() {
|
||||
}
|
||||
html += "</div></div>";
|
||||
document.getElementById("view-settings").innerHTML = html;
|
||||
renderGuiPrefs();
|
||||
renderSettingsTabs();
|
||||
renderConnSection();
|
||||
loadGuiPrefs();
|
||||
}
|
||||
|
||||
function markDirty(k) {
|
||||
@ -3689,6 +3775,76 @@ function openConnManager() {
|
||||
switchView("settings");
|
||||
}
|
||||
|
||||
// ===== 客户端偏好(开机自启/静默启动/退出进托盘) =====
|
||||
var guiPrefs = { autoLaunch: false, silentStart: false, exitToTray: true };
|
||||
async function loadGuiPrefs() {
|
||||
try {
|
||||
if (window.homeagent && window.homeagent.prefs) {
|
||||
guiPrefs = (await window.homeagent.prefs.get()) || guiPrefs;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
async function saveGuiPrefs(key, val) {
|
||||
var next = Object.assign({}, guiPrefs, { [key]: val });
|
||||
try {
|
||||
if (window.homeagent && window.homeagent.prefs) {
|
||||
guiPrefs = (await window.homeagent.prefs.set(next)) || next;
|
||||
} else {
|
||||
guiPrefs = next;
|
||||
}
|
||||
} catch (e) {
|
||||
toast(__("保存偏好失败: ", "Save prefs failed: ") + e.message, true);
|
||||
return;
|
||||
}
|
||||
toast(__("已保存", "Saved"));
|
||||
renderGuiPrefs();
|
||||
}
|
||||
function renderGuiPrefs() {
|
||||
var el = document.getElementById("gui-prefs");
|
||||
if (!el) return;
|
||||
function sw(key, label, desc, onchange) {
|
||||
return (
|
||||
'<div class="pref-row" style="margin:8px 0;display:flex;justify-content:space-between;align-items:center">' +
|
||||
"<div><div>" +
|
||||
label +
|
||||
"</div>" +
|
||||
(desc
|
||||
? '<div style="font-size:11px;color:var(--text-muted)">' +
|
||||
desc +
|
||||
"</div>"
|
||||
: "") +
|
||||
"</div>" +
|
||||
'<label class="switch"><input type="checkbox" ' +
|
||||
(guiPrefs[key] ? "checked" : "") +
|
||||
' onchange="' +
|
||||
onchange +
|
||||
'"><span></span></label></div>'
|
||||
);
|
||||
}
|
||||
el.innerHTML =
|
||||
'<div class="card" id="gui-prefs-card"><h2>' +
|
||||
__("客户端偏好", "Client Preferences") +
|
||||
"</h2>" +
|
||||
sw(
|
||||
"autoLaunch",
|
||||
__("开机自启", "Auto launch on start"),
|
||||
__("登录后自动启动 HomeAgent(静默)", "Start silently at login"),
|
||||
"saveGuiPrefs('autoLaunch', this.checked)",
|
||||
) +
|
||||
sw(
|
||||
"silentStart",
|
||||
__("静默启动", "Silent start"),
|
||||
__("启动时不显示主窗口,驻留托盘后台运行", "Start hidden, keep in tray"),
|
||||
"saveGuiPrefs('silentStart', this.checked)",
|
||||
) +
|
||||
sw(
|
||||
"exitToTray",
|
||||
__("退出进托盘", "Exit to tray"),
|
||||
__("关闭窗口时驻留托盘而不是退出", "Closing window keeps app in tray"),
|
||||
"saveGuiPrefs('exitToTray', this.checked)",
|
||||
) +
|
||||
"</div>";
|
||||
}
|
||||
function renderConnSection() {
|
||||
var cont = document.getElementById("conn-manager");
|
||||
if (!cont) return;
|
||||
@ -3755,7 +3911,8 @@ function renderConnSection() {
|
||||
__("连接类型", "Type") +
|
||||
'</label><select id="conn-type" onchange="toggleConnType()">' +
|
||||
'<option value="webui">WebUI (HTTP)</option>' +
|
||||
'<option value="cli">CLI (unix socket)</option></select>' +
|
||||
'<option value="cli">CLI (unix socket)</option>' +
|
||||
'<option value="device">设备网关 (remotedevice)</option></select>' +
|
||||
'<div id="conn-addr-webui"><label>' +
|
||||
__("地址", "URL") +
|
||||
'</label><input id="conn-url" placeholder="http://localhost:18080"></div>' +
|
||||
@ -3832,6 +3989,11 @@ function toggleConnType() {
|
||||
t === "cli" ? "block" : "none";
|
||||
document.getElementById("conn-auth-webui").style.display =
|
||||
t === "cli" ? "none" : "block";
|
||||
// device 类型不适用 webui 网关登录
|
||||
var gwField = document.getElementById("conn-gw");
|
||||
if (gwField)
|
||||
gwField.closest("label, div").style.display =
|
||||
t === "device" ? "none" : "block";
|
||||
toggleGwFields();
|
||||
}
|
||||
|
||||
@ -4160,6 +4322,38 @@ async function saveConnForm() {
|
||||
testBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
} else if (ctype === "device") {
|
||||
// 设备网关:直接探活 /api/v1/device(带 token)
|
||||
var devTest;
|
||||
try {
|
||||
devTest = await fetch(url + "/api/v1/device", {
|
||||
headers: apiKey ? { "X-API-Key": apiKey } : {},
|
||||
});
|
||||
} catch (e) {
|
||||
toast(
|
||||
__("无法连接到设备网关 ", "Cannot connect to device gateway ") +
|
||||
url +
|
||||
": " +
|
||||
e.message,
|
||||
true,
|
||||
);
|
||||
testBtn.textContent = __("保存", "Save");
|
||||
testBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (!devTest.ok) {
|
||||
toast(
|
||||
__("设备网关测试失败: ", "Device gateway test failed: ") +
|
||||
devTest.status +
|
||||
"(" +
|
||||
(await devTest.text()).slice(0, 120) +
|
||||
")",
|
||||
true,
|
||||
);
|
||||
testBtn.textContent = __("保存", "Save");
|
||||
testBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (window.homeagent && window.homeagent.webui) {
|
||||
if (gwEnabled && !cookie) {
|
||||
@ -4260,19 +4454,31 @@ async function saveConnForm() {
|
||||
testBtn.textContent = __("保存", "Save");
|
||||
testBtn.disabled = false;
|
||||
var connData =
|
||||
ctype === "cli"
|
||||
? { name: name, type: "cli", socketPath: sock, url: "", apiKey: apiKey }
|
||||
: {
|
||||
ctype === "device"
|
||||
? {
|
||||
name: name,
|
||||
type: "webui",
|
||||
type: "device",
|
||||
url: url,
|
||||
apiKey: apiKey,
|
||||
username: username,
|
||||
password: password,
|
||||
cookie: cookie,
|
||||
headers: headers,
|
||||
gateway: gwEnabled,
|
||||
};
|
||||
username: "",
|
||||
password: "",
|
||||
cookie: "",
|
||||
headers: "",
|
||||
gateway: false,
|
||||
}
|
||||
: ctype === "cli"
|
||||
? { name: name, type: "cli", socketPath: sock, url: "", apiKey: apiKey }
|
||||
: {
|
||||
name: name,
|
||||
type: "webui",
|
||||
url: url,
|
||||
apiKey: apiKey,
|
||||
username: username,
|
||||
password: password,
|
||||
cookie: cookie,
|
||||
headers: headers,
|
||||
gateway: gwEnabled,
|
||||
};
|
||||
var data;
|
||||
if (editingConnId) {
|
||||
data = await window.homeagent.connections.update(editingConnId, connData);
|
||||
@ -4322,8 +4528,9 @@ connectSSE = () => {
|
||||
state.eventSource = null;
|
||||
}
|
||||
if (!state.currentConn) return;
|
||||
// CLI 连接无 SSE 通道,聊天走同步 cli:request
|
||||
if (state.currentConn.type === "cli") return;
|
||||
// CLI/device 连接无 SSE 通道,聊天走同步
|
||||
if (state.currentConn.type === "cli" || state.currentConn.type === "device")
|
||||
return;
|
||||
connectFetchSSE(state.currentConn.url + "/api/v1/chat/events");
|
||||
};
|
||||
|
||||
@ -4530,7 +4737,7 @@ function rerenderChatIfActive() {
|
||||
!!last && last.role === "assistant" && !last._final && state.chatLoading;
|
||||
if (streamingLast) {
|
||||
if (state._streamTimer) clearTimeout(state._streamTimer);
|
||||
state._streamTimer = setTimeout(function () {
|
||||
state._streamTimer = setTimeout(() => {
|
||||
state._streamTimer = null;
|
||||
renderChatStreamChunk();
|
||||
}, 90);
|
||||
@ -4567,7 +4774,7 @@ function renderChatStreamChunk() {
|
||||
var lastLen = el.__lastLen || 0;
|
||||
if (c.length - lastLen > 200 || now - lastParse > 300) {
|
||||
textEl.innerHTML =
|
||||
typeof marked !== "undefined" ? marked.parse(c) : escHtml(c);
|
||||
typeof marked === "undefined" ? escHtml(c) : marked.parse(c);
|
||||
el.__lastParse = now;
|
||||
el.__lastLen = c.length;
|
||||
} else {
|
||||
@ -4597,3 +4804,196 @@ function renderChatStreamChunk() {
|
||||
renderChat();
|
||||
}
|
||||
|
||||
// ===== Devices (remotedevice gateway) =====
|
||||
function renderDevices() {
|
||||
var el = document.getElementById("view-devices");
|
||||
if (!el) return;
|
||||
var conn = state.currentConn;
|
||||
if (!conn || conn.type !== "device") {
|
||||
el.innerHTML =
|
||||
'<div class="card"><h2>' +
|
||||
__("设备", "Devices") +
|
||||
'</h2><p style="color:var(--text-muted)">' +
|
||||
__(
|
||||
"请先在设置中添加并切换到「设备网关 (remotedevice)」连接,以查看已接入设备。",
|
||||
"Add and switch to a Remote Device Gateway connection in Settings to view devices.",
|
||||
) +
|
||||
"</p>" +
|
||||
'<button class="btn btn-primary" onclick="goSettingsConn()">' +
|
||||
__("管理连接", "Manage Connections") +
|
||||
"</button></div>";
|
||||
return;
|
||||
}
|
||||
var devs = state.devices || [];
|
||||
var selfDev = null;
|
||||
if (state.selfDeviceId) {
|
||||
for (var si = 0; si < devs.length; si++) {
|
||||
if (devs[si].device_id === state.selfDeviceId) {
|
||||
selfDev = devs[si];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var selfHtml =
|
||||
'<div class="card" style="border-left:3px solid #4f8cff"><h2>' +
|
||||
__("本机 GUI 设备", "Local GUI Device") +
|
||||
"</h2>";
|
||||
if (state.selfDeviceId) {
|
||||
var sOnline = selfDev ? selfDev.online : false;
|
||||
var sAuth = selfDev ? selfDev.authorized : false;
|
||||
selfHtml +=
|
||||
'<div class="kv-row"><span class="key">' +
|
||||
__("设备 ID", "Device ID") +
|
||||
'</span><span class="val">' +
|
||||
escHtml(state.selfDeviceId) +
|
||||
"</span></div>" +
|
||||
'<div class="kv-row"><span class="key">' +
|
||||
__("网关", "Gateway") +
|
||||
'</span><span class="val">' +
|
||||
escHtml(state.selfGateway || "-") +
|
||||
"</span></div>" +
|
||||
'<div class="kv-row"><span class="key">' +
|
||||
__("状态", "Status") +
|
||||
"</span><span>" +
|
||||
(sOnline
|
||||
? '<span class="dot-green">' + __("在线", "Online") + "</span>"
|
||||
: '<span class="dot-gray">' + __("离线", "Offline") + "</span>") +
|
||||
"</span></div>" +
|
||||
'<div class="kv-row"><span class="key">' +
|
||||
__("授权", "Authorized") +
|
||||
'</span><span class="val" style="display:flex;align-items:center;gap:8px">' +
|
||||
'<label class="switch"><input type="checkbox" ' +
|
||||
(sAuth ? "checked" : "") +
|
||||
" onchange=\"deviceToggleAuth('" +
|
||||
state.selfDeviceId +
|
||||
"',this.checked)\"><span></span></label>" +
|
||||
(sAuth
|
||||
? '<span class="dot-green">' + __("已授权", "Yes") + "</span>"
|
||||
: '<span class="dot-red">' + __("未授权", "No") + "</span>") +
|
||||
"</span></div>";
|
||||
} else {
|
||||
selfHtml +=
|
||||
'<p style="color:var(--text-muted)">' +
|
||||
__(
|
||||
"未接入设备网关。请在设置中配置「设备网关 (remotedevice)」连接后重启。",
|
||||
"Not connected. Configure a Remote Device Gateway connection in Settings then restart.",
|
||||
) +
|
||||
"</p>";
|
||||
}
|
||||
selfHtml += "</div>";
|
||||
var html =
|
||||
selfHtml +
|
||||
'<div class="card"><h2>' +
|
||||
__("设备网关", "Device Gateway") +
|
||||
" (" +
|
||||
devs.length +
|
||||
")" +
|
||||
"</h2>" +
|
||||
'<div style="margin-bottom:8px"><button class="btn btn-ghost btn-sm" onclick="deviceRefresh()">' +
|
||||
__("刷新", "Refresh") +
|
||||
"</button></div>";
|
||||
if (devs.length === 0) {
|
||||
html +=
|
||||
'<p style="color:var(--text-muted)">' +
|
||||
__(
|
||||
"暂无设备接入。设备通过 WebSocket 连接到设备网关(默认 127.0.0.1:9890/api/v1/device/ws),携带 token 后 hello 登记、bind 授权。",
|
||||
"No devices yet. Devices connect via WebSocket (default 127.0.0.1:9890/api/v1/device/ws), hello to register, bind to authorize.",
|
||||
) +
|
||||
"</p>";
|
||||
} else {
|
||||
html +=
|
||||
"<table><tr><th>" +
|
||||
__("设备", "Device") +
|
||||
"</th><th>" +
|
||||
__("种类", "Kind") +
|
||||
"</th><th>" +
|
||||
__("状态", "Status") +
|
||||
"</th><th>" +
|
||||
__("授权", "Authorized") +
|
||||
"</th><th>" +
|
||||
__("能力", "Caps") +
|
||||
"</th><th>" +
|
||||
__("操作", "Actions") +
|
||||
"</th></tr>";
|
||||
devs.forEach((d) => {
|
||||
var online = d.online
|
||||
? '<span class="dot-green">' + __("在线", "Online") + "</span>"
|
||||
: '<span class="dot-gray">' + __("离线", "Offline") + "</span>";
|
||||
var auth = d.authorized
|
||||
? '<span class="dot-green">' + __("已授权", "Yes") + "</span>"
|
||||
: '<span class="dot-red">' + __("未授权", "No") + "</span>";
|
||||
var caps = (d.caps || []).join(", ") || "-";
|
||||
html +=
|
||||
"<tr><td><b>" +
|
||||
escHtml(d.name || d.device_id) +
|
||||
'</b><br><span style="font-size:11px;color:var(--text-muted)">' +
|
||||
escHtml(d.device_id) +
|
||||
"</span></td><td>" +
|
||||
escHtml(d.kind || "-") +
|
||||
"</td><td>" +
|
||||
online +
|
||||
"</td><td>" +
|
||||
auth +
|
||||
"</td><td>" +
|
||||
escHtml(caps) +
|
||||
"</td><td>" +
|
||||
(d.authorized
|
||||
? '<label class="switch"><input type="checkbox" checked' +
|
||||
" onchange=\"deviceToggleAuth('" +
|
||||
d.device_id +
|
||||
"',this.checked)\"><span></span></label> " +
|
||||
__("已授权", "Yes")
|
||||
: '<label class="switch"><input type="checkbox"' +
|
||||
" onchange=\"deviceToggleAuth('" +
|
||||
d.device_id +
|
||||
"',this.checked)\"><span></span></label> " +
|
||||
__("未授权", "No")) +
|
||||
"</td></tr>";
|
||||
});
|
||||
html += "</table>";
|
||||
}
|
||||
html += "</div>";
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
async function deviceRefresh() {
|
||||
try {
|
||||
var d = await api("/device");
|
||||
state.devices = d.devices || [];
|
||||
renderDevices();
|
||||
} catch (e) {
|
||||
toast(__("设备列表刷新失败: ", "Refresh failed: ") + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deviceToggleAuth(deviceID, auth) {
|
||||
try {
|
||||
await api("/device/auth", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ device_id: deviceID, authorize: auth }),
|
||||
});
|
||||
toast(__("授权已更新", "Authorization updated"));
|
||||
deviceRefresh();
|
||||
} catch (e) {
|
||||
toast(__("授权失败: ", "Auth failed: ") + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function deviceSendCmd(deviceID) {
|
||||
var cmd = prompt(__("输入要执行的命令", "Enter command to run"));
|
||||
if (!cmd) return;
|
||||
api("/device/push", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
device_id: deviceID,
|
||||
payload: { op: "cmd", command: cmd },
|
||||
}),
|
||||
})
|
||||
.then(() => {
|
||||
toast(__("命令已下发", "Command sent"));
|
||||
deviceRefresh();
|
||||
})
|
||||
.catch((e) => {
|
||||
toast(__("下发失败: ", "Send failed: ") + e.message, true);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,88 +1,315 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HomeAgent</title>
|
||||
<link rel="icon" type="image/svg+xml" href="icon.svg">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js" onerror="window._THREE_FAILED=true"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js" onerror="window._THREE_FAILED=true"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js" onerror="console.warn('marked CDN failed')"></script>
|
||||
<script>setTimeout(function(){if(!window.THREE)window._THREE_FAILED=true},8000)</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bg-layer"></div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HomeAgent</title>
|
||||
<link rel="icon" type="image/svg+xml" href="icon.svg" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"
|
||||
onerror="window._THREE_FAILED = true"
|
||||
></script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"
|
||||
onerror="window._THREE_FAILED = true"
|
||||
></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"
|
||||
onerror="console.warn('marked CDN failed')"
|
||||
></script>
|
||||
<script>
|
||||
setTimeout(function () {
|
||||
if (!window.THREE) window._THREE_FAILED = true;
|
||||
}, 8000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bg-layer"></div>
|
||||
|
||||
<div class="titlebar" id="titlebar">
|
||||
<div class="titlebar-controls">
|
||||
<button class="tb-btn" id="tb-min" title="最小化" onclick="window.homeagent&&homeagent.win.minimize()">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><line x1="4" y1="12" x2="20" y2="12"/></svg>
|
||||
</button>
|
||||
<button class="tb-btn" id="tb-max" title="最大化" onclick="window.homeagent&&homeagent.win.toggleMaximize()">
|
||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><rect x="5" y="5" width="14" height="14" rx="1.5"/></svg>
|
||||
</button>
|
||||
<button class="tb-btn tb-close" id="tb-close" title="关闭" onclick="window.homeagent&&homeagent.win.close()">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<!-- 左侧图标栏:主页=对话,其余为二级页面 -->
|
||||
<aside class="rail" id="rail">
|
||||
<div class="rail-logo"><img src="icon.svg" alt="HomeAgent"></div>
|
||||
<button class="rail-btn active" id="rail-chat" onclick="switchView('chat')" title="对话">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-overview" onclick="switchView('overview')" title="概览">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-plugins" onclick="switchView('plugins')" title="插件">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-adapters" onclick="switchView('adapters')" title="适配器">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" id="rail-kernel" onclick="switchView('kernel')" title="内核">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4"/></svg>
|
||||
</button>
|
||||
<div class="rail-spacer"></div>
|
||||
<button class="rail-btn" id="rail-settings" onclick="switchView('settings')" title="设置">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
</button>
|
||||
<button class="rail-btn" onclick="toggleTheme()" title="切换亮色/暗色模式" id="theme-btn"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg></button>
|
||||
<div class="palette-pop" id="palette-pop" title="外观"></div>
|
||||
<button class="rail-btn" onclick="toggleAppearance()" id="palette-btn" title="外观:主题色 / 背景图"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22a10 10 0 1 1 10-10c0 2.2-1.8 4-4 4h-2a3 3 0 0 0-3 3c0 1.1.9 2 2 2 1.4 0 2.5-1.1 3.6-2.1C19.3 18.5 20 20 21.5 20.5A1 1 0 0 0 22 19c0-4-3.6-7-10-7-5.5 0-9 3.8-9 9z"/><circle cx="7.5" cy="10.5" r="1"/><circle cx="12" cy="7.5" r="1"/><circle cx="16.5" cy="10.5" r="1"/></svg></button>
|
||||
<span class="conn-dot dot-gray" id="rail-conn-dot" title="连接状态"></span>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<h1>HomeAgent</h1>
|
||||
<span class="conn-indicator" id="conn-indicator" onclick="goSettingsConn()" title="点击管理后端连接">
|
||||
<span class="status-dot dot-gray" id="conn-dot"></span>
|
||||
<span id="conn-name-display">未连接</span>
|
||||
<span style="font-size:10px;margin-left:4px;opacity:.6">▼</span>
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
<button class="lang-btn" onclick="toggleLang()" id="lang-btn">EN</button>
|
||||
<div class="titlebar" id="titlebar">
|
||||
<div class="titlebar-controls">
|
||||
<button
|
||||
class="tb-btn"
|
||||
id="tb-min"
|
||||
title="最小化"
|
||||
onclick="window.homeagent && homeagent.win.minimize()"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<line x1="4" y1="12" x2="20" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
id="tb-max"
|
||||
title="最大化"
|
||||
onclick="window.homeagent && homeagent.win.toggleMaximize()"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="13"
|
||||
height="13"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<rect x="5" y="5" width="14" height="14" rx="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="tb-btn tb-close"
|
||||
id="tb-close"
|
||||
title="关闭"
|
||||
onclick="window.homeagent && homeagent.win.close()"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
height="14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<path d="M6 6l12 12M18 6L6 18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="view-chat" class="view active"></div>
|
||||
<div id="view-overview" class="view"></div>
|
||||
<div id="view-plugins" class="view"></div>
|
||||
<div id="view-adapters" class="view"></div>
|
||||
<div id="view-kernel" class="view"></div>
|
||||
<div id="view-settings" class="view"></div>
|
||||
<div id="app">
|
||||
<!-- 左侧图标栏:主页=对话,其余为二级页面 -->
|
||||
<aside class="rail" id="rail">
|
||||
<div class="rail-logo"><img src="icon.svg" alt="HomeAgent" /></div>
|
||||
<button
|
||||
class="rail-btn active"
|
||||
id="rail-chat"
|
||||
onclick="switchView('chat')"
|
||||
title="对话"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="rail-btn"
|
||||
id="rail-overview"
|
||||
onclick="switchView('overview')"
|
||||
title="概览"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="rail-btn"
|
||||
id="rail-plugins"
|
||||
onclick="switchView('plugins')"
|
||||
title="插件"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="rail-btn"
|
||||
id="rail-adapters"
|
||||
onclick="switchView('adapters')"
|
||||
title="适配器"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="rail-btn"
|
||||
id="rail-kernel"
|
||||
onclick="switchView('kernel')"
|
||||
title="内核"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<rect x="9" y="9" width="6" height="6" />
|
||||
<path
|
||||
d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="rail-btn"
|
||||
id="rail-devices"
|
||||
onclick="switchView('devices')"
|
||||
title="设备"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="7" cy="7" r="3" />
|
||||
<circle cx="17" cy="17" r="3" />
|
||||
<circle cx="17" cy="7" r="3" />
|
||||
<path d="M7 7h7a3 3 0 0 1 3 3v4" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="rail-spacer"></div>
|
||||
<button
|
||||
class="rail-btn"
|
||||
id="rail-settings"
|
||||
onclick="switchView('settings')"
|
||||
title="设置"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="rail-btn"
|
||||
onclick="toggleTheme()"
|
||||
title="切换亮色/暗色模式"
|
||||
id="theme-btn"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="15"
|
||||
height="15"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="palette-pop" id="palette-pop" title="外观"></div>
|
||||
<button
|
||||
class="rail-btn"
|
||||
onclick="toggleAppearance()"
|
||||
id="palette-btn"
|
||||
title="外观:主题色 / 背景图"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="15"
|
||||
height="15"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M12 22a10 10 0 1 1 10-10c0 2.2-1.8 4-4 4h-2a3 3 0 0 0-3 3c0 1.1.9 2 2 2 1.4 0 2.5-1.1 3.6-2.1C19.3 18.5 20 20 21.5 20.5A1 1 0 0 0 22 19c0-4-3.6-7-10-7-5.5 0-9 3.8-9 9z"
|
||||
/>
|
||||
<circle cx="7.5" cy="10.5" r="1" />
|
||||
<circle cx="12" cy="7.5" r="1" />
|
||||
<circle cx="16.5" cy="10.5" r="1" />
|
||||
</svg>
|
||||
</button>
|
||||
<span
|
||||
class="conn-dot dot-gray"
|
||||
id="rail-conn-dot"
|
||||
title="连接状态"
|
||||
></span>
|
||||
</aside>
|
||||
|
||||
<div class="main">
|
||||
<div class="topbar">
|
||||
<h1>HomeAgent</h1>
|
||||
<span
|
||||
class="conn-indicator"
|
||||
id="conn-indicator"
|
||||
onclick="goSettingsConn()"
|
||||
title="点击管理后端连接"
|
||||
>
|
||||
<span class="status-dot dot-gray" id="conn-dot"></span>
|
||||
<span id="conn-name-display">未连接</span>
|
||||
<span style="font-size: 10px; margin-left: 4px; opacity: 0.6"
|
||||
>▼</span
|
||||
>
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
<button class="lang-btn" onclick="toggleLang()" id="lang-btn">
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="view-chat" class="view active"></div>
|
||||
<div id="view-overview" class="view"></div>
|
||||
<div id="view-plugins" class="view"></div>
|
||||
<div id="view-adapters" class="view"></div>
|
||||
<div id="view-kernel" class="view"></div>
|
||||
<div id="view-devices" class="view"></div>
|
||||
<div id="view-settings" class="view"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
<div id="confirm-overlay" class="confirm-overlay"></div>
|
||||
<div id="toast" class="toast"></div>
|
||||
<div id="confirm-overlay" class="confirm-overlay"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -286,6 +286,7 @@ body {
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
font-family: var(--font-sans);
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s;
|
||||
@ -590,7 +591,9 @@ body.maximized .tb-max svg {
|
||||
margin: 3px;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
transition:
|
||||
transform 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
.palette-pop button.cdot:hover {
|
||||
transform: scale(1.25);
|
||||
@ -1249,8 +1252,12 @@ code {
|
||||
opacity: 0.6;
|
||||
}
|
||||
@keyframes reasoningSweep {
|
||||
from { background-position: 200% 0; }
|
||||
to { background-position: -200% 0; }
|
||||
from {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
to {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 工具卡片(PiDeck 风格,扁平不套气泡)===== */
|
||||
@ -1264,7 +1271,9 @@ code {
|
||||
border: 1px solid var(--kv-border);
|
||||
border-left: 3px solid var(--accent);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s var(--ease-out), background 0.2s var(--ease-out);
|
||||
transition:
|
||||
border-color 0.2s var(--ease-out),
|
||||
background 0.2s var(--ease-out);
|
||||
}
|
||||
.tool-card.tc-running {
|
||||
border-left-color: var(--accent);
|
||||
@ -2071,3 +2080,95 @@ code {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Toggle switch(左右滑动开关按钮)===== */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
.switch span {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #888;
|
||||
border-radius: 24px;
|
||||
transition: background 0.25s;
|
||||
}
|
||||
.switch span::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
.switch input:checked + span {
|
||||
background: #34c759;
|
||||
}
|
||||
.switch input:checked + span::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
td .switch {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ===== 设备页样式修复 ===== */
|
||||
/* 开关输入框彻底隐藏,避免浅蓝方框露出 */
|
||||
.switch input {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
}
|
||||
/* 设备列表表格圆角化(去方框感) */
|
||||
.card table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
}
|
||||
.card table th,
|
||||
.card table td {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--glass-border, rgba(128, 128, 128, 0.15));
|
||||
vertical-align: middle;
|
||||
}
|
||||
.card table th {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #888);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.card table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.card table tr:hover td {
|
||||
background: var(--glass-hover, rgba(128, 128, 128, 0.06));
|
||||
}
|
||||
/* kv-row 值区对齐(含开关) */
|
||||
.kv-row .val {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.kv-row .val .switch {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@ -16,11 +16,13 @@ type Connection struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Socket string `yaml:"socket"`
|
||||
Remote string `yaml:"remote"`
|
||||
APIKey string `yaml:"api_key"`
|
||||
Default string `yaml:"default"`
|
||||
Connections []Connection `yaml:"connections,omitempty"`
|
||||
Socket string `yaml:"socket"`
|
||||
Remote string `yaml:"remote"`
|
||||
APIKey string `yaml:"api_key"`
|
||||
Default string `yaml:"default"`
|
||||
Connections []Connection `yaml:"connections,omitempty"`
|
||||
DeviceGateway string `yaml:"device_gateway,omitempty"` // remotedevice 网关地址(如 127.0.0.1:9890)
|
||||
DeviceToken string `yaml:"device_token,omitempty"` // 设备接入 token
|
||||
}
|
||||
|
||||
func (c *Config) Active() *Connection {
|
||||
|
||||
340
cmd/waiter/device.go
Normal file
340
cmd/waiter/device.go
Normal file
@ -0,0 +1,340 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type deviceBridge struct {
|
||||
addr string
|
||||
token string
|
||||
deviceID string
|
||||
conn net.Conn
|
||||
mu sync.Mutex
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func newDeviceBridge(addr, token string) *deviceBridge {
|
||||
return &deviceBridge{addr: addr, token: token, stop: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (b *deviceBridge) Start() error {
|
||||
if b.addr == "" || b.token == "" {
|
||||
return fmt.Errorf("device bridge: addr/token required")
|
||||
}
|
||||
host, port := b.addr, "9890"
|
||||
if h, p, err := net.SplitHostPort(b.addr); err == nil {
|
||||
host, port = h, p
|
||||
} else if idx := strings.LastIndex(b.addr, ":"); idx >= 0 {
|
||||
host = strings.TrimPrefix(b.addr[:idx], "http://")
|
||||
port = b.addr[idx+1:]
|
||||
}
|
||||
hostname, _ := os.Hostname()
|
||||
if hostname == "" {
|
||||
hostname = "local"
|
||||
}
|
||||
b.deviceID = "waiter-" + sanitizeID(hostname)
|
||||
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 5*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.conn = conn
|
||||
|
||||
key := wsKey()
|
||||
path := "/api/v1/device/ws?token=" + urlEscape(b.token)
|
||||
var sb strings.Builder
|
||||
sb.WriteString("GET " + path + " HTTP/1.1" + CRLF)
|
||||
sb.WriteString("Host: " + host + ":" + port + CRLF)
|
||||
sb.WriteString("Upgrade: websocket" + CRLF)
|
||||
sb.WriteString("Connection: Upgrade" + CRLF)
|
||||
sb.WriteString("Sec-WebSocket-Key: " + key + CRLF)
|
||||
sb.WriteString("Sec-WebSocket-Version: 13" + CRLF + CRLF)
|
||||
if _, err := conn.Write([]byte(sb.String())); err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
br := bufio.NewReader(conn)
|
||||
headerBuf := ""
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
headerBuf += line
|
||||
if strings.Contains(headerBuf, CRLF+CRLF) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !strings.Contains(headerBuf, " 101 ") {
|
||||
conn.Close()
|
||||
return fmt.Errorf("ws upgrade failed: %s", firstLine(headerBuf))
|
||||
}
|
||||
|
||||
b.sendJSON(helloMsg(b.deviceID))
|
||||
b.sendJSON(map[string]interface{}{"op": "bind", "device_id": b.deviceID, "token": b.token})
|
||||
|
||||
go b.readLoop(br)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CRLF 用常量避免转义地狱
|
||||
const CRLF = "\r\n"
|
||||
|
||||
func helloMsg(deviceID string) map[string]interface{} {
|
||||
hostname, _ := os.Hostname()
|
||||
return map[string]interface{}{
|
||||
"op": "hello",
|
||||
"device": map[string]interface{}{
|
||||
"device_id": deviceID,
|
||||
"name": "HomeAgent CLI (waiter)",
|
||||
"kind": "computer",
|
||||
"caps": []string{"status", "cmdrun", "deviceinfo"},
|
||||
"info": map[string]interface{}{
|
||||
"hostname": hostname,
|
||||
"platform": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"cpus": runtime.NumCPU(),
|
||||
"mem_mb": memTotalMB(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func memTotalMB() int64 {
|
||||
if runtime.GOOS != "linux" {
|
||||
return 0
|
||||
}
|
||||
data, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) >= 2 {
|
||||
var kb int64
|
||||
fmt.Sscanf(f[1], "%d", &kb)
|
||||
return kb / 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (b *deviceBridge) sendJSON(v interface{}) {
|
||||
payload, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.conn == nil {
|
||||
return
|
||||
}
|
||||
writeWSFrame(b.conn, 0x1, payload)
|
||||
}
|
||||
|
||||
func writeWSFrame(conn net.Conn, opcode byte, payload []byte) {
|
||||
length := len(payload)
|
||||
hdr := []byte{0x80 | opcode}
|
||||
switch {
|
||||
case length < 126:
|
||||
hdr = append(hdr, 0x80|byte(length))
|
||||
case length <= 0xffff:
|
||||
hdr = append(hdr, 0x80|126, 0, 0)
|
||||
binary.BigEndian.PutUint16(hdr[len(hdr)-2:], uint16(length))
|
||||
default:
|
||||
hdr = append(hdr, 0x80|127)
|
||||
hdr = append(hdr, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
binary.BigEndian.PutUint64(hdr[len(hdr)-8:], uint64(length))
|
||||
}
|
||||
var mask [4]byte
|
||||
rand.Read(mask[:])
|
||||
masked := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
masked[i] = payload[i] ^ mask[i&3]
|
||||
}
|
||||
conn.Write(append(append(hdr, mask[:]...), masked...))
|
||||
}
|
||||
|
||||
func (b *deviceBridge) readLoop(br *bufio.Reader) {
|
||||
for {
|
||||
select {
|
||||
case <-b.stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
payload, err := readWSFrame(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg map[string]interface{}
|
||||
if err := json.Unmarshal(payload, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
if msg["op"] != "cmd" {
|
||||
continue
|
||||
}
|
||||
reqID, _ := msg["req_id"].(string)
|
||||
command, _ := msg["command"].(string)
|
||||
if reqID == "" {
|
||||
continue
|
||||
}
|
||||
go b.runCommand(reqID, command)
|
||||
}
|
||||
}
|
||||
|
||||
func readWSFrame(br *bufio.Reader) ([]byte, error) {
|
||||
b0, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opcode := b0 & 0x0f
|
||||
b1, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length := uint64(b1 & 0x7f)
|
||||
if length == 126 {
|
||||
var ext [2]byte
|
||||
if _, err := io.ReadFull(br, ext[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length = uint64(binary.BigEndian.Uint16(ext[:]))
|
||||
} else if length == 127 {
|
||||
var ext [8]byte
|
||||
if _, err := io.ReadFull(br, ext[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length = binary.BigEndian.Uint64(ext[:])
|
||||
}
|
||||
if length > 1<<20 {
|
||||
return nil, fmt.Errorf("frame too large")
|
||||
}
|
||||
payload := make([]byte, length)
|
||||
if _, err := io.ReadFull(br, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch opcode {
|
||||
case 0x1:
|
||||
return payload, nil
|
||||
case 0x8:
|
||||
return nil, io.EOF
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
var waiterAllowCmd = regexp.MustCompile("^(ls|pwd|whoami|uname|date|echo|uptime|hostname|cat|df|free|ps|ip|dir|node|python3?|npm|git|curl|wget|systeminfo|tasklist)\\b")
|
||||
|
||||
func (b *deviceBridge) runCommand(reqID, command string) {
|
||||
res := b.execCommand(command)
|
||||
res["op"] = "cmd_result"
|
||||
res["req_id"] = reqID
|
||||
res["device_id"] = b.deviceID
|
||||
b.sendJSON(res)
|
||||
}
|
||||
|
||||
func (b *deviceBridge) execCommand(cmdLine string) map[string]interface{} {
|
||||
if strings.TrimSpace(cmdLine) == "" {
|
||||
return map[string]interface{}{"status": "error", "output": "empty command", "exit_code": -1}
|
||||
}
|
||||
if !waiterAllowCmd.MatchString(strings.TrimSpace(cmdLine)) {
|
||||
return map[string]interface{}{"status": "error", "output": "command not in whitelist", "exit_code": -1}
|
||||
}
|
||||
parts := strings.Fields(strings.TrimSpace(cmdLine))
|
||||
if len(parts) == 0 {
|
||||
return map[string]interface{}{"status": "error", "output": "empty", "exit_code": -1}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, parts[0], parts[1:]...)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return map[string]interface{}{"status": "error", "output": "timeout", "exit_code": -2}
|
||||
}
|
||||
return map[string]interface{}{"status": "error", "output": truncate8k(string(out)) + " err: " + err.Error(), "exit_code": -1}
|
||||
}
|
||||
return map[string]interface{}{"status": "ok", "output": truncate8k(string(out)), "exit_code": 0}
|
||||
}
|
||||
|
||||
const devMaxOut = 8192
|
||||
|
||||
func truncate8k(s string) string {
|
||||
if len(s) <= devMaxOut {
|
||||
return s
|
||||
}
|
||||
return s[:devMaxOut]
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
||||
return strings.TrimSpace(s[:i])
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func sanitizeID(s string) string {
|
||||
var sb strings.Builder
|
||||
for _, r := range s {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||
sb.WriteRune(r)
|
||||
} else {
|
||||
sb.WriteByte('_')
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func urlEscape(s string) string {
|
||||
var sb strings.Builder
|
||||
const hex = "0123456789ABCDEF"
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' {
|
||||
sb.WriteByte(c)
|
||||
} else {
|
||||
sb.WriteByte('%')
|
||||
sb.WriteByte(hex[c>>4])
|
||||
sb.WriteByte(hex[c&0xf])
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func wsKey() string {
|
||||
var b [16]byte
|
||||
rand.Read(b[:])
|
||||
return base64.StdEncoding.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
func (b *deviceBridge) Stop() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
select {
|
||||
case <-b.stop:
|
||||
default:
|
||||
close(b.stop)
|
||||
}
|
||||
if b.conn != nil {
|
||||
b.conn.Close()
|
||||
b.conn = nil
|
||||
}
|
||||
}
|
||||
@ -81,6 +81,8 @@ func main() {
|
||||
configPath := flag.String("config", "", "config file path")
|
||||
chat := flag.String("chat", "", "send a message and print final text (one-shot)")
|
||||
say := flag.String("say", "", "deprecated alias of -chat")
|
||||
deviceGateway := flag.String("device", "", "remotedevice 网关地址(如 127.0.0.1:9890),启动设备桥")
|
||||
deviceToken := flag.String("device-token", "", "设备接入 token")
|
||||
flag.Parse()
|
||||
|
||||
cfg := discoverConfig(*configPath)
|
||||
@ -103,6 +105,26 @@ func main() {
|
||||
}
|
||||
defer state.Disconnect()
|
||||
|
||||
// 设备桥:--device 或配置 device_gateway 时,waiter 作为被控设备接入 remotedevice
|
||||
var bridge *deviceBridge
|
||||
dg := *deviceGateway
|
||||
if dg == "" {
|
||||
dg = cfg.DeviceGateway
|
||||
}
|
||||
dt := *deviceToken
|
||||
if dt == "" {
|
||||
dt = cfg.DeviceToken
|
||||
}
|
||||
if dg != "" && dt != "" {
|
||||
bridge = newDeviceBridge(dg, dt)
|
||||
if err := bridge.Start(); err != nil {
|
||||
printlnC(colorYellow, fmt.Sprintf("device bridge: %v (continue without)", err))
|
||||
} else {
|
||||
printlnC(colorGreen, "device bridge active: "+bridge.deviceID)
|
||||
defer bridge.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
if oneShotMsg != "" {
|
||||
oneshot(state, oneShotMsg)
|
||||
return
|
||||
@ -236,6 +258,3 @@ func printServerOutput(content string) {
|
||||
fmt.Printf("%s%s%s\n", clearLine, content, colorReset)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user