From 90880b135e6bc823510f826b0ac8cb8e3c446338 Mon Sep 17 00:00:00 2001 From: JianFeeeee <109188060+JianFeeeee@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:27:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(remotedevice):=20=E8=A1=A5=20omniparse=20?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=20+=20=E8=AE=BE=E5=A4=87=E6=A1=A5=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=E9=97=A8=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 要判的是什么 GUI(cmd/gui/main.js)作为**设备端**被 agent 驱动,走的是 WS 上行/下行的 op 消息,与服务端 remotedevice 插件共享同一套能力契约。这条通道此前 **没有任何强制手段** —— endpoint-align.test.mjs 只守 REST 面 (api("...") 对齐 webui 的 mux.HandleFunc),明确不碰设备桥。 两套实现(Go client / GUI JS / 鸿蒙 ETS)全靠注释人工对齐 (main.js:1367「与鸿蒙端 DeviceBridge.ets:209 对齐」)。 ## 真实漂移(2026-10-05 核实) GUI caps 含 "omniparse",executeHomeagentCmd 有完整 case(调 PowerShell + ha_omniparse_*.ps1)。但 capabilityTools 里没有这个键,整个 remotedevice 包 grep omniparse 命中 0 ⇒ deviceSupportsTool 恒 false ⇒ agent 永远不会下发 ⇒ 那段实现是死代码。 修复:capabilityTools 补键,使其可被下发。 ## 删 devicebridge_dll.js 三条都查证过: 1. 无人引用 —— 全仓 grep DeviceBridgeDLL 命中 0; 2. 必然抛 —— start() 无条件 throw 'koffi not fully implemented', 且 loadFFI() 的 ffi-napi 备选分支只 return true 不返回 lib; 3. 方向已废弃 —— GUI 走 main.js:999 手工构造的原生 WebSocket (Sec-WebSocket-Key/Version 握手),不经 FFI。 ⚠ koffi **依赖保留**:main.js:1873 用 koffi.load("user32.dll") 做 computeruse 的鼠标键盘注入,那是真实在用的,与本文件无关。 ## 撤回两个误判 写门禁初版时判错两处,均已在判据注释里记录理由: - 「GUI 声明 cmdrun/cmdresult 会绕过能力矩阵」是错的。device_ctl_cmdrun (device.go:270)是所有能力的**统一下发通道**:agent 调 computeruse/screensue 最终都由它下发 homeagent-*,SupportsTool 查的是 具体能力名而非 cmdrun。声明 cmdrun 是对的。 - 「status/deviceinfo 服务端不认」也是错的。二者是元信息通道而非设备 工具:status 是心跳上报(registry.go:831),deviceinfo 的实现 (device.go:338)直接回显 meta.Caps 原始值、不经 deviceSupportsTool。 判据改为区分 capabilityTools(可裁剪能力)与 metaCaps(元信息通道)。 ## 门禁(cmd/gui/device-align.test.mjs) 从 registry.go / protocol.go / main.js 三方**源码文本**提取,不抄逻辑 重写 —— 同 endpoint-align 路子。9 条判据覆盖:cap 双向识别、capability- > 实现的一致性、protocol.go 消息类型落点、多处 hello 的 caps 一致性、 DLL 桩守卫。 变异测试验证非假红:撤掉服务端 omniparse ⇒ 第 3、5 条立刻转红; 只改第二处 caps ⇒ 第 9 条报「第 2 处起有 1 处不一致」。 ## 验证 - node device-align.test.mjs ⇒ 11 条全绿 - gofmt -l registry.go ⇒ 干净 - go vet ./internal/plugins/remotedevice/ ⇒ 本包无告警 ⚠ go test ./internal/plugins/remotedevice/ 未能跑通:internal/agent/api 的 cgo 构建失败(codec_cgo.go 静态断言 + C 符号解析)。已确认该失败在**未 改动的 main 上同样存在**,需先构建 csrc 静态库,是既有环境前置条件, 与本次改动无关。 顺带同步 cmd/gui/package-lock.json 版本号 1.0.0 → 1.4.0,与 cmd/gui/package.json 对齐。 --- cmd/gui/device-align.test.mjs | 413 ++++++++++++++++++++++ cmd/gui/devicebridge_dll.js | 126 ------- cmd/gui/package-lock.json | 4 +- internal/plugins/remotedevice/registry.go | 2 + 4 files changed, 417 insertions(+), 128 deletions(-) create mode 100644 cmd/gui/device-align.test.mjs delete mode 100644 cmd/gui/devicebridge_dll.js diff --git a/cmd/gui/device-align.test.mjs b/cmd/gui/device-align.test.mjs new file mode 100644 index 00000000..38173a1c --- /dev/null +++ b/cmd/gui/device-align.test.mjs @@ -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("全部通过"); \ No newline at end of file diff --git a/cmd/gui/devicebridge_dll.js b/cmd/gui/devicebridge_dll.js deleted file mode 100644 index a4620524..00000000 --- a/cmd/gui/devicebridge_dll.js +++ /dev/null @@ -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 }; \ No newline at end of file diff --git a/cmd/gui/package-lock.json b/cmd/gui/package-lock.json index df5ae176..1b21c8dd 100644 --- a/cmd/gui/package-lock.json +++ b/cmd/gui/package-lock.json @@ -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" }, diff --git a/internal/plugins/remotedevice/registry.go b/internal/plugins/remotedevice/registry.go index a3520ddf..cd218dbc 100644 --- a/internal/plugins/remotedevice/registry.go +++ b/internal/plugins/remotedevice/registry.go @@ -104,6 +104,8 @@ var capabilityTools = map[string][]string{ // 音频播放 "speaker": {"speakeruse"}, "speakeruse": {"speakeruse"}, + // omniparse:GUI 设备端已实现完整 case,补进能力矩阵使其可被下发 + "omniparse": {"omniparse"}, } // compatFullCaps 视为「全能力」的历史 caps 值:声明了这些的设备不参与能力裁剪。