mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-10 17:44:44 +00:00
merge: 设备桥能力对齐(补 omniparse 能力 + 新增 device-align 门禁 + 删 FFI 死桩)
合入 fix/devicebridge-capability-align(90880b1)。
单人与CI 环境说明:
- remotedevice / devicebridge 单测在 WSL(Arch, go1.25.0) 跑通,接口冻结
检查 third_party/homeagent-sdk 无改动。
- 仓库的 internal/agent/api/{ha_codec.c,...} 是指向 csrc/ 的**符号链接**
(git 模式 120000)。Windows 上 core.symlinks=false 会把它们检出成
文本指针文件(内容就一行路径),导致 cgo 找不到 C 符号、
go build ./internal/agent/api/ 直接失败。需 git config
core.symlinks true 后重新 checkout。本次已在本地修复该配置。
- internal/plugin/proc/process.go:147 的 Setpgid / syscall.Kill 无 build
tag 保护,仅 Unix 可编译 —— CI 全在 ubuntu-latest 上跑,故不受影响;
Windows 本地无法 go test 该链路(既有问题,非本分支引入)。
This commit is contained in:
413
cmd/gui/device-align.test.mjs
Normal file
413
cmd/gui/device-align.test.mjs
Normal file
@ -0,0 +1,413 @@
|
||||
// DeviceBridge 能力对齐门禁:GUI ↔ remotedevice 插件 ↔ devicebridge 客户端库。
|
||||
//
|
||||
// ## 为什么需要这个门禁(endpoint-align.test.mjs 覆盖不到的那一半)
|
||||
//
|
||||
// endpoint-align.test.mjs 守的是 **REST 面**:GUI 的 api("...") 对齐 webui 的
|
||||
// mux.HandleFunc。它明确不碰设备桥 —— 而设备桥是 GUI 作为**设备端**被
|
||||
// agent 驱动的唯一通道,走的是 WS 上行/下行的 op 消息,两端没有任何
|
||||
// 共享契约文件,全靠注释人工对齐。
|
||||
//
|
||||
// 真实漂移实例(2026-10-05 核实):
|
||||
//
|
||||
// 1. GUI main.js 声明 caps 含 "omniparse",且 executeHomeagentCmd 有完整
|
||||
// case "omniparse"(调 PowerShell + ha_omniparse_*.ps1)。但服务端
|
||||
// registry.go 的 capabilityTools 里**没有 omniparse 这个键**,整个
|
||||
// remotedevice 包 grep omniparse 命中 0 次 ⇒ deviceSupportsTool 对它
|
||||
// 永远返回 false ⇒ agent 永远不会下发 omniparse ⇒ 那段实现是死代码。
|
||||
//
|
||||
// 2. cmd/gui/devicebridge_dll.js 声明了 12 个 devicebridge_* 导出,但
|
||||
// start() 无条件 `throw new Error('koffi not fully implemented')`,
|
||||
// 且 loadFFI() 的 ffi-napi 备选分支只 return true 不返回 lib。
|
||||
// 且全仓无人引用它 —— GUI 实际走 main.js:999 手工构造的原生 WebSocket。
|
||||
// 已于 fix/devicebridge-capability-align 删除该文件。
|
||||
// (koffi 依赖保留:main.js:1873 用它 load user32.dll 做 computeruse 注入。)
|
||||
//
|
||||
// 3. GUI caps 里同时声明了 cmdrun/cmdresult(compatFullCaps,历史全能力
|
||||
// 标记)与完整的 computeruse/screensue/... 精确能力。初判为「精确声明
|
||||
// 被绕过」,**实为误判**:device_ctl_cmdrun 是所有能力的统一下发通道,
|
||||
// agent 调 computeruse/screensue 最终都由它下发 homeagent-*,
|
||||
// SupportsTool 查的是具体能力名而非 cmdrun。声明 cmdrun 是对的。
|
||||
//
|
||||
// 4. GUI caps 里的 status / deviceinfo 不在 capabilityTools 矩阵里。初判为
|
||||
// 「服务端不认识的漂移」,**实为误判**:二者是元信息通道而非设备工具 ——
|
||||
// status 是心跳上报(registry.go:831),deviceinfo 的实现(device.go:338)
|
||||
// 直接回显 meta.Caps 原始值、不经 deviceSupportsTool。
|
||||
//
|
||||
// 这些都不报错、不告警,GUI 照常运行,只是某些能力永远不会被触发。
|
||||
//
|
||||
// ## 与既有判据同路子
|
||||
//
|
||||
// endpoint-align / sse-backoff / retry-guard 都是:从**真实源码**提取,
|
||||
// 不抄一份逻辑重写(抄的那份会和真实代码漂移,而漂移正是要防的)。
|
||||
// 运行时探测在这里同样不可用:omniparse 是否可达取决于 agent 何时调用
|
||||
// 哪个工具(LLM 决策),不是能构造出来的固定条件。
|
||||
//
|
||||
// 运行:node device-align.test.mjs
|
||||
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(here, "..", "..");
|
||||
|
||||
const MAIN_JS = join(here, "main.js");
|
||||
const REGISTRY_GO = join(repoRoot, "internal", "plugins", "remotedevice", "registry.go");
|
||||
const PROTOCOL_GO = join(
|
||||
repoRoot,
|
||||
"internal",
|
||||
"devicebridge",
|
||||
"client",
|
||||
"protocol.go",
|
||||
);
|
||||
const DLL_JS = join(here, "devicebridge_dll.js");
|
||||
|
||||
let failures = 0;
|
||||
const check = (name, ok, detail) => {
|
||||
if (ok) {
|
||||
console.log(` ✓ ${name}`);
|
||||
} else {
|
||||
failures++;
|
||||
console.log(` ✗ ${name}${detail ? " — " + detail : ""}`);
|
||||
}
|
||||
};
|
||||
|
||||
// ── 1) 读源码 ───────────────────────────────────────────────────────
|
||||
|
||||
for (const [label, p] of [
|
||||
["GUI 主进程", MAIN_JS],
|
||||
["服务端能力矩阵", REGISTRY_GO],
|
||||
["客户端协议", PROTOCOL_GO],
|
||||
]) {
|
||||
if (!existsSync(p)) {
|
||||
console.error(`找不到${label}:${p}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const mainSrc = readFileSync(MAIN_JS, "utf8");
|
||||
const registrySrc = readFileSync(REGISTRY_GO, "utf8");
|
||||
const protocolSrc = readFileSync(PROTOCOL_GO, "utf8");
|
||||
|
||||
// ── 2) 抽能力集合 ───────────────────────────────────────────────────
|
||||
|
||||
// 服务端 capabilityTools 的键。形如:
|
||||
//
|
||||
// var capabilityTools = map[string][]string{
|
||||
// "screen": {"screensue", "screensee"},
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// 只在 map 字面量的键位置上匹配(行首 tab + 引号 + 冒号),
|
||||
// 不去匹配值位置的字符串,否则 tools 列表会被当成 cap。
|
||||
function serverCaps() {
|
||||
const start = registrySrc.indexOf("var capabilityTools");
|
||||
if (start < 0) return null;
|
||||
// 找 map 字面量结尾 "\n}",范围收窄以免吃到后面的 map
|
||||
const end = registrySrc.indexOf("\n}", start);
|
||||
if (end < 0) return null;
|
||||
const body = registrySrc.slice(start, end);
|
||||
const out = new Set();
|
||||
const re = /^\t"([a-z]+)":/gm;
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) out.add(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// compatFullCaps:声明了这些的设备视为全能力,不参与能力裁剪。
|
||||
function compatCaps() {
|
||||
const start = registrySrc.indexOf("var compatFullCaps");
|
||||
if (start < 0) return new Set();
|
||||
const end = registrySrc.indexOf("\n}", start);
|
||||
const body = registrySrc.slice(start, end < 0 ? undefined : end);
|
||||
const out = new Set();
|
||||
const re = /"([a-z]+)":\s*true/g;
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) out.add(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// metaCaps:服务端消费但不参与 deviceSupportsTool 裁剪的 cap。
|
||||
//
|
||||
// 这类 cap 不是设备工具,而是**元信息通道**,拿 capabilityTools 去比对必然误报:
|
||||
//
|
||||
// - status:registry.go 收设备的 {"op":"status"} 上报刷 LastSeen(心跳),
|
||||
// 不在工具面。
|
||||
// - deviceinfo:是 agent 工具名(device.go:168),但 deviceinfo 的实现
|
||||
// (device.go:338 info())直接读 meta.Caps 原始值回显,**不经过**
|
||||
// deviceSupportsTool —— 它要的就是「完整 caps」,正是能力裁剪的反面。
|
||||
//
|
||||
// 所以「服务端能不能识别一个 cap」有两个不同的问题:能被裁剪矩阵识别(capabilityTools),
|
||||
// 与被别处消费(metaCaps)。只查前者会把这两个误判成漂移。
|
||||
const metaCaps = new Set(["status", "deviceinfo"]);
|
||||
|
||||
// GUI 声明的 caps。main.js 里有**两处** hello 消息(首次连接 + set authorized
|
||||
// 后重发),两处必须一致 —— 不一致会让服务端在不同时机看到不同的能力集。
|
||||
function guiCaps() {
|
||||
const out = new Set();
|
||||
const re = /caps:\s*\[([\s\S]*?)\]/g;
|
||||
let m;
|
||||
while ((m = re.exec(mainSrc)) !== null) {
|
||||
const body = m[1];
|
||||
const item = /"([a-z]+)"/g;
|
||||
let c;
|
||||
while ((c = item.exec(body)) !== null) out.add(c[1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const caps = serverCaps();
|
||||
const compat = compatCaps();
|
||||
const gui = guiCaps();
|
||||
|
||||
check(
|
||||
"能抽到服务端能力矩阵",
|
||||
caps && caps.size >= 8,
|
||||
caps ? `只抽到 ${caps.size} 条 —— 正则可能已与 registry.go 漂移` : "找不到 capabilityTools",
|
||||
);
|
||||
check(
|
||||
"能抽到 GUI caps",
|
||||
gui.size >= 8,
|
||||
`只抽到 ${gui.size} 条 —— 正则可能已与 main.js 漂移`,
|
||||
);
|
||||
|
||||
// ── 3) GUI 声明的每个 cap 服务端都要认识 ────────────────────────────
|
||||
//
|
||||
// 这是**硬门禁**:GUI 声明一个服务端不认识的 cap,服务端不会报错(未知 cap
|
||||
// 只是匹配不到任何 tool),但依赖它的能力永远不会被下发 ⇒ 静默失效。
|
||||
//
|
||||
// 注意语义:命中 compatFullCaps 的 cap(cmd/cmdrun/cmdresult)在服务端
|
||||
// 眼里是「全能力」,本身不算漂移 —— 第 4) 条单独盯它。
|
||||
|
||||
const unknown = [...gui].filter(
|
||||
(c) => !caps.has(c) && !compat.has(c) && !metaCaps.has(c),
|
||||
);
|
||||
check(
|
||||
"GUI 声明的 cap 服务端都认识",
|
||||
unknown.length === 0,
|
||||
unknown.length
|
||||
? "服务端无此能力(对应实现永远不会被下发):\n - " + unknown.join("\n - ")
|
||||
: "",
|
||||
);
|
||||
|
||||
// ── 4) GUI 的能力声明必须真的起作用(不得整体退化为全能力)──────────
|
||||
//
|
||||
// ★ 早期版本这条判据说「GUI 不该声明 cmdrun/cmdresult」是**错的**,已撤回。
|
||||
// 错在哪:device_ctl_cmdrun(device.go:270)是所有设备能力的**统一下发通道**
|
||||
// ——agent 对 computeruse/screensue/clipboardsue 的每次调用,最终都是
|
||||
// cmdrun 下发一条 homeagent-* 命令。SupportsTool 查的 tool 名是
|
||||
// computeruse 这类具体能力,不是 cmdrun。所以 GUI 声明 cmdrun 是对的。
|
||||
//
|
||||
// 真正的问题是反向的失效:GUI 若**只**声明 compatFullCaps
|
||||
// (cmd/cmdrun/cmdresult)而漏掉具体能力,那 deviceSupportsTool 首循环
|
||||
// 就 return true,精确声明形同虚设;但只要 GUI 声明了具体能力,服务端
|
||||
// 就已经在按矩阵裁剪了,compat 项并不改变这一点(首个 return true 只在
|
||||
// 没有任何已知能力时才会「意外」发生,而 GUI 明确声明了 12 个能力,
|
||||
// 其中多数命中 capabilityTools)。
|
||||
//
|
||||
// 所以这条判据改为检查**真正会静默失效的形态**:声明了具体能力却同时
|
||||
// 带着 compat 标记,使矩阵裁剪与实际能力不一致 —— 即 GUI 少声明了某个
|
||||
// 它已实现的能力,但 compat 标记让服务端误以为它有。
|
||||
//
|
||||
// 若将来 GUI 要作为「只能跑 shell、无屏幕能力」的设备存在(即真的只有
|
||||
// compat 能力),请连同下面这条判据一起删,并在注释里写明理由。
|
||||
|
||||
const guiHasConcrete = [...gui].filter((c) => caps.has(c));
|
||||
const guiHasCompat = [...gui].filter((c) => compat.has(c));
|
||||
check(
|
||||
"GUI 能力声明不退化为 compat-only",
|
||||
!(guiHasCompat.length > 0 && guiHasConcrete.length === 0),
|
||||
guiHasCompat.length > 0 && guiHasConcrete.length === 0
|
||||
? `只声明了 ${guiHasCompat.join(", ")} ⇒ deviceSupportsTool 首循环 return true,` +
|
||||
"服务端无法按能力裁剪"
|
||||
: "",
|
||||
);
|
||||
|
||||
// ── 5) GUI 实现的每个命令都要对应一个服务端能力 ────────────────────
|
||||
//
|
||||
// executeHomeagentCmd 的 case 分支名就是设备桥命令名。服务端只会下发
|
||||
// capabilityTools 值里的工具名(经 device_ctl_* 工具面),所以 GUI
|
||||
// 实现了一个服务端没有对应工具的命令 ⇒ 永远收不到。
|
||||
|
||||
function guiCommandCases() {
|
||||
const start = mainSrc.indexOf("function executeHomeagentCmd");
|
||||
if (start < 0) return null;
|
||||
// 函数体到下一个顶层 function 声明为止
|
||||
const rest = mainSrc.slice(start);
|
||||
const end = rest.indexOf("\nfunction ");
|
||||
const body = end < 0 ? rest : rest.slice(0, end);
|
||||
const out = new Set();
|
||||
// 只取 switch 里的 case(缩进 4 空格),避免抓到嵌套 switch(computeruse
|
||||
// 内部还有一层 switch,case move/click/... 是动作名不是命令名)
|
||||
const re = /^ {4}case "([a-z]+)":/gm;
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) out.add(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 服务端可下发的工具名 = capabilityTools 所有 value 的并集。
|
||||
function serverTools() {
|
||||
const start = registrySrc.indexOf("var capabilityTools");
|
||||
if (start < 0) return new Set();
|
||||
const end = registrySrc.indexOf("\n}", start);
|
||||
const body = registrySrc.slice(start, end);
|
||||
const out = new Set();
|
||||
const re = /"([a-z]+)"/g;
|
||||
let m;
|
||||
while ((m = re.exec(body)) !== null) out.add(m[1]);
|
||||
return out;
|
||||
}
|
||||
|
||||
const cmdCases = guiCommandCases();
|
||||
const tools = serverTools();
|
||||
|
||||
check(
|
||||
"能抽到 GUI 命令 case",
|
||||
cmdCases && cmdCases.size >= 6,
|
||||
cmdCases ? `只抽到 ${cmdCases.size} 条 —— 正则可能已与 main.js 漂移` : "找不到 executeHomeagentCmd",
|
||||
);
|
||||
|
||||
const orphanCmds = cmdCases ? [...cmdCases].filter((c) => !tools.has(c)) : [];
|
||||
check(
|
||||
"GUI 实现的命令服务端都有对应工具",
|
||||
orphanCmds.length === 0,
|
||||
orphanCmds.length
|
||||
? "服务端无对应工具(下发不了):\n - " + orphanCmds.join("\n - ")
|
||||
: "",
|
||||
);
|
||||
|
||||
// ── 6) 声明了能力就必须实现它 ───────────────────────────────────────
|
||||
//
|
||||
// 反向:GUI 在 caps 里声明了 X,executeHomeagentCmd 却没有 case "X"
|
||||
// ⇒ 服务端会下发 X(因为 capabilityTools 里 X 是合法工具),GUI 落到
|
||||
// default 分支。这是**运行时空转**:req_id 收不到回执,网关侧超时。
|
||||
|
||||
const declaredButNotImplemented = [...gui]
|
||||
.filter((c) => tools.has(c) && !compat.has(c) && cmdCases && !cmdCases.has(c));
|
||||
check(
|
||||
"声明的能力都已实现",
|
||||
declaredButNotImplemented.length === 0,
|
||||
declaredButNotImplemented.length
|
||||
? "声明了但无 case 实现:\n - " + declaredButNotImplemented.join("\n - ")
|
||||
: "",
|
||||
);
|
||||
|
||||
// ── 7) devicebridge_dll.js 不得回来 ──────────────────────────────────
|
||||
//
|
||||
// ★ 这个文件已于 fix/devicebridge-capability-align 删除。删除理由(三条都查证过):
|
||||
//
|
||||
// 1. 无人引用:全仓(除本判据外)grep DeviceBridgeDLL 命中 0。
|
||||
// 2. 必然抛:start() 无条件 throw 'koffi not fully implemented'。
|
||||
// 3. 方向已废弃:GUI 走的是 main.js:999 手工构造的原生 WebSocket
|
||||
// (Sec-WebSocket-Key/Version 握手),不经 FFI。
|
||||
//
|
||||
// ⚠ 但 koffi **依赖本身要留着**:main.js:1873 用它 koffi.load("user32.dll")
|
||||
// 做 computeruse 的鼠标键盘注入。那是真实在用的,与本文件无关。
|
||||
// 删文件不等于删依赖。
|
||||
//
|
||||
// 判据保留为反向守卫:文件若被重新引入,必须是完整实现而非抛异常的桩。
|
||||
|
||||
if (!existsSync(DLL_JS)) {
|
||||
console.log(" ✓ devicebridge_dll.js 已删除(GUI 走原生 WebSocket,不经 FFI)");
|
||||
} else {
|
||||
const dllSrc = readFileSync(DLL_JS, "utf8");
|
||||
const stubs = [
|
||||
/throw new Error\(["']koffi not fully implemented["']\)/,
|
||||
/TODO:\s*实现 koffi 调用/,
|
||||
/TODO:.*FFI/,
|
||||
].filter((re) => re.test(dllSrc));
|
||||
check(
|
||||
"devicebridge_dll.js 不是未实现桩",
|
||||
stubs.length === 0,
|
||||
stubs.length
|
||||
? "存在未实现桩(require 即抛)。走 FFI 前补全,或删除该文件改用已实现的路径"
|
||||
: "",
|
||||
);
|
||||
}
|
||||
|
||||
// ── 8) protocol.go 的消息类型要在 GUI 侧有落点 ──────────────────────
|
||||
//
|
||||
// DataStart/DataEnd/SpeechStart/SpeechEnd/EventMsg/StatusMsg 是设备桥
|
||||
// 上行的消息契约。GUI 若不实现其中任何一个,对应能力(媒体回传、TTS
|
||||
// 播放、主动事件上报)就是哑的 —— 而 GUI 的 caps 里声明了 camerasue,
|
||||
// camerasue 正是靠 sendDeviceDataChunked 走 DataStart/DataEnd。
|
||||
//
|
||||
// 只判「caps 声明了该能力 ⇒ 相关消息类型必须有实现」。
|
||||
|
||||
const protocolTypes = new Set();
|
||||
{
|
||||
const re = /^type\s+([A-Z]\w+)\s+struct/mg;
|
||||
let m;
|
||||
while ((m = re.exec(protocolSrc)) !== null) protocolTypes.add(m[1]);
|
||||
}
|
||||
|
||||
check(
|
||||
"能抽到 protocol.go 消息类型",
|
||||
protocolTypes.size >= 6,
|
||||
`只抽到 ${protocolTypes.size} 条 —— 正则可能已与 protocol.go 漂移`,
|
||||
);
|
||||
|
||||
// 媒体回传能力 → 必须有 DataStart/DataEnd 实现
|
||||
if (gui.has("camerasue")) {
|
||||
const hasDataStart = /DataStart|data_start|dataStart/i.test(mainSrc);
|
||||
const hasDataEnd = /DataEnd|data_end|dataEnd/i.test(mainSrc);
|
||||
check(
|
||||
"声明 camerasue ⇒ 已实现媒体分块回传",
|
||||
hasDataStart && hasDataEnd,
|
||||
!hasDataStart || !hasDataEnd
|
||||
? "camerasue 依赖 cmd_data_start/cmd_data_end 分块回传,但 GUI 侧找不到实现"
|
||||
: "",
|
||||
);
|
||||
} else {
|
||||
console.log(" · GUI 未声明 camerasue,跳过媒体回传判据");
|
||||
}
|
||||
|
||||
// ── 9) 两处 hello 的 caps 必须一致 ──────────────────────────────────
|
||||
//
|
||||
// main.js 有两处 caps 数组(首次连接 / set authorized 后重发)。不一致
|
||||
// ⇒ 服务端在不同时机看到不同的能力集,表现为「改授权后能力变了」这类
|
||||
// 难复现的偶发问题。
|
||||
|
||||
const capsBlocks = [];
|
||||
{
|
||||
const re = /caps:\s*\[([\s\S]*?)\]/g;
|
||||
let m;
|
||||
while ((m = re.exec(mainSrc)) !== null) {
|
||||
const set = new Set();
|
||||
const item = /"([a-z]+)"/g;
|
||||
let c;
|
||||
while ((c = item.exec(m[1])) !== null) set.add(c[1]);
|
||||
capsBlocks.push(set);
|
||||
}
|
||||
}
|
||||
|
||||
if (capsBlocks.length < 2) {
|
||||
console.log(` · 只找到 ${capsBlocks.length} 处 caps 声明,跳过一致性判据`);
|
||||
} else {
|
||||
const [first, ...rest] = capsBlocks;
|
||||
const inconsistent = rest.filter(
|
||||
(b) => b.size !== first.size || [...b].some((v) => !first.has(v)),
|
||||
);
|
||||
check(
|
||||
"多处 hello 的 caps 声明一致",
|
||||
inconsistent.length === 0,
|
||||
inconsistent.length
|
||||
? `第 2 处起有 ${inconsistent.length} 处与第 1 处不一致`
|
||||
: "",
|
||||
);
|
||||
}
|
||||
|
||||
// ── 汇总 ────────────────────────────────────────────────────────────
|
||||
|
||||
console.log("");
|
||||
console.log(
|
||||
` 服务端能力 ${caps ? caps.size : 0} 个 / compat ${compat.size} 个 / ` +
|
||||
`可下发工具 ${tools.size} 个 / GUI caps ${gui.size} 个 / GUI 实现 ${cmdCases ? cmdCases.size : 0} 个`,
|
||||
);
|
||||
console.log("");
|
||||
|
||||
if (failures > 0) {
|
||||
console.log(`失败 ${failures} 条`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("全部通过");
|
||||
@ -1,126 +0,0 @@
|
||||
// DeviceBridge DLL 桥接模块
|
||||
// 提供设备桥共享库的 Node.js 封装,GUI 通过 FFI 调用 Go 编译的 DLL。
|
||||
// 优先尝试加载 DLL,失败则回退到纯 JS 实现(保留兼容)。
|
||||
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
let koffi = null;
|
||||
let bridgeLib = null;
|
||||
let _handle = null;
|
||||
|
||||
// DLL 路径
|
||||
function dllPath() {
|
||||
const dir = __dirname;
|
||||
const plat = os.platform();
|
||||
if (plat === 'win32') {
|
||||
return path.join(dir, 'devicebridge.dll');
|
||||
}
|
||||
// Linux/Mac 使用 .so/.dylib
|
||||
const ext = plat === 'darwin' ? 'dylib' : 'so';
|
||||
return path.join(dir, `devicebridge.${ext}`);
|
||||
}
|
||||
|
||||
// 尝试加载 FFI 库
|
||||
async function loadFFI() {
|
||||
try {
|
||||
koffi = require('koffi');
|
||||
return true;
|
||||
} catch (e) {
|
||||
try {
|
||||
const ffi = require('ffi-napi');
|
||||
const ref = require('ref-napi');
|
||||
// 使用 ffi-napi 作为备选
|
||||
return true;
|
||||
} catch (e2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载 DLL
|
||||
function loadDLL() {
|
||||
const dll = dllPath();
|
||||
try {
|
||||
if (koffi) {
|
||||
return koffi.load(dll);
|
||||
}
|
||||
const ffi = require('ffi-napi');
|
||||
const ref = require('ref-napi');
|
||||
return ffi.Library(dll, {
|
||||
'devicebridge_new': ['pointer', ['string', 'string', 'string', 'string', 'pointer', 'int']],
|
||||
'devicebridge_start': ['int', ['pointer']],
|
||||
'devicebridge_stop': ['void', ['pointer']],
|
||||
'devicebridge_free': ['void', ['pointer']],
|
||||
'devicebridge_connected': ['int', ['pointer']],
|
||||
'devicebridge_device_id': ['string', ['pointer']],
|
||||
'devicebridge_send_result': ['int', ['pointer', 'string', 'string', 'string', 'string']],
|
||||
'devicebridge_send_event': ['void', ['pointer', 'string', 'string']],
|
||||
'devicebridge_send_status': ['void', ['pointer', 'string']],
|
||||
'devicebridge_send_data_start': ['void', ['pointer', 'string', 'string', 'string', 'int']],
|
||||
'devicebridge_send_data_chunk': ['int', ['pointer', 'pointer', 'int']],
|
||||
'devicebridge_send_data_end': ['void', ['pointer', 'string', 'string', 'string']],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[devicebridge-dll] load failed:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 设备桥封装
|
||||
class DeviceBridgeDLL {
|
||||
constructor() {
|
||||
this.connected = false;
|
||||
this.deviceId = '';
|
||||
this._onCmd = null;
|
||||
this._onData = null;
|
||||
}
|
||||
|
||||
// 初始化并连接
|
||||
async start(gateway, token, deviceId, deviceName, caps, info) {
|
||||
bridgeLib = loadDLL();
|
||||
if (!bridgeLib) {
|
||||
throw new Error('DLL not loaded');
|
||||
}
|
||||
|
||||
// 构建 caps 数组
|
||||
const capsArr = caps.map(c => Buffer.from(c + '\0'));
|
||||
const capsPtr = Buffer.alloc(8 * capsArr.length);
|
||||
// 简化:实际 FFI 调用需要更复杂的参数处理
|
||||
// 这里使用 koffi 方式
|
||||
|
||||
if (koffi) {
|
||||
// 使用 koffi 调用
|
||||
try {
|
||||
// TODO: 实现 koffi 调用
|
||||
throw new Error('koffi not fully implemented');
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('FFI library not available. Install koffi or ffi-napi');
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (bridgeLib && _handle) {
|
||||
try {
|
||||
bridgeLib.devicebridge_stop(_handle);
|
||||
bridgeLib.devicebridge_free(_handle);
|
||||
} catch (e) {}
|
||||
_handle = null;
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
sendResult(reqId, status, output, error) {
|
||||
if (!bridgeLib || !_handle) return;
|
||||
try {
|
||||
bridgeLib.devicebridge_send_result(_handle, reqId, status, output || '', error || '');
|
||||
} catch (e) {
|
||||
console.error('[devicebridge-dll] sendResult error:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DeviceBridgeDLL, loadFFI };
|
||||
4
cmd/gui/package-lock.json
generated
4
cmd/gui/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "homeagent-gui",
|
||||
"version": "1.0.0",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "homeagent-gui",
|
||||
"version": "1.0.0",
|
||||
"version": "1.4.0",
|
||||
"dependencies": {
|
||||
"koffi": "^3.1.6"
|
||||
},
|
||||
|
||||
@ -104,6 +104,8 @@ var capabilityTools = map[string][]string{
|
||||
// 音频播放
|
||||
"speaker": {"speakeruse"},
|
||||
"speakeruse": {"speakeruse"},
|
||||
// omniparse:GUI 设备端已实现完整 case,补进能力矩阵使其可被下发
|
||||
"omniparse": {"omniparse"},
|
||||
}
|
||||
|
||||
// compatFullCaps 视为「全能力」的历史 caps 值:声明了这些的设备不参与能力裁剪。
|
||||
|
||||
Reference in New Issue
Block a user