fix(harmony): guide official HDC setup and recheck tool availability

This commit is contained in:
2026-09-10 14:33:53 +08:00
parent 170ea4d112
commit 5666bb8f40
13 changed files with 372 additions and 70 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
"id": "dev.trove.harmony-connector", "id": "dev.trove.harmony-connector",
"name": "Harmony Connector", "name": "Harmony Connector",
"description": "连接鸿蒙设备,收发完整文件夹、管理应用,查看手机画面并通过键鼠操作。", "description": "连接鸿蒙设备,收发完整文件夹、管理应用,查看手机画面并通过键鼠操作。",
"version": "0.3.0", "version": "0.3.1",
"engine": { "trove": ">=0.1.0 <1.0.0" }, "engine": { "trove": ">=0.1.0 <1.0.0" },
"icon": "assets/icon.svg", "icon": "assets/icon.svg",
"ui": { "ui": {
@@ -15,7 +15,7 @@
"lifecycle": "on-demand", "transport": "unix", "startWithUi": true, "lifecycle": "on-demand", "transport": "unix", "startWithUi": true,
"startupTimeoutMs": 5000, "shutdownGraceMs": 5000, "idleTimeoutMs": 0 "startupTimeoutMs": 5000, "shutdownGraceMs": 5000, "idleTimeoutMs": 0
}, },
"services": { "requires": [{ "id": "host.dialog", "version": "^1.0.0" }] }, "services": { "requires": [{ "id": "host.dialog", "version": "^1.0.0" }, { "id": "host.runtime", "version": "^1.0.0" }] },
"build": { "command": ["node", "build.mjs"] }, "build": { "command": ["node", "build.mjs"] },
"package": { "files": ["dist/ui", "backend", "assets", "legal"] } "package": { "files": ["dist/ui", "backend", "assets", "legal"] }
} }
+1 -1
View File
@@ -204,7 +204,7 @@ dependencies = [
[[package]] [[package]]
name = "harmony-connector" name = "harmony-connector"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "harmony-connector" name = "harmony-connector"
version = "0.3.0" version = "0.3.1"
edition = "2021" edition = "2021"
publish = false publish = false
+51 -29
View File
@@ -102,6 +102,33 @@ fn executable(path: &Path) -> bool {
.metadata() .metadata()
.is_ok_and(|m| m.permissions().mode() & 0o111 != 0) .is_ok_and(|m| m.permissions().mode() & 0o111 != 0)
} }
fn selected_tool(path: &Path) -> Option<PathBuf> {
std::iter::once(path.to_path_buf())
.chain(
[
"hdc",
"toolchains/hdc",
"openharmony/toolchains/hdc",
"sdk/default/openharmony/toolchains/hdc",
"command-line-tools/sdk/default/openharmony/toolchains/hdc",
"Contents/sdk/default/openharmony/toolchains/hdc",
]
.into_iter()
.map(|suffix| path.join(suffix)),
)
.find(|p| executable(p))
.and_then(|p| p.canonicalize().ok())
}
fn tool_version(path: &Path, cancel: &AtomicBool) -> Result<String> {
let version = command::run(path, &["-v".into()], Duration::from_secs(5), cancel, 8192)?;
if !version.contains("Ver:") && !version.to_lowercase().contains("hdc") {
return Err(Fault::new(
"hdc.invalid_tool",
"所选工具没有返回 HDC 版本信息。",
));
}
Ok(version.trim().to_owned())
}
fn discover() -> Option<PathBuf> { fn discover() -> Option<PathBuf> {
let mut candidates = Vec::new(); let mut candidates = Vec::new();
if let Some(path) = env::var_os("PATH") { if let Some(path) = env::var_os("PATH") {
@@ -290,42 +317,37 @@ impl Manager {
"screenFrame" => self.screen.frame(parse(value)?), "screenFrame" => self.screen.frame(parse(value)?),
"screenInput" => self.screen.input(parse(value)?), "screenInput" => self.screen.input(parse(value)?),
"tool" => { "tool" => {
let path = self.path.lock().unwrap().clone(); let path = {
let mut path = self.path.lock().unwrap();
if path.is_none() {
*path = discover();
}
path.clone()
};
let Some(path) = path else { let Some(path) = path else {
return Ok(json!({"available":false})); return Ok(json!({"available":false}));
}; };
let version = command::run( if !executable(&path) {
&path, return Ok(
&["-v".into()], json!({"available":false,"path":path,"reason":"此前选择的 HDC 已移动、删除或不可执行,请重新选择。"}),
Duration::from_secs(5), );
&self.cancel, }
8192, match tool_version(&path, &self.cancel) {
)?; Ok(version) => Ok(json!({"available":true,"path":path,"version":version})),
Ok(json!({"available":true,"path":path,"version":version.trim()})) Err(error) => Ok(
json!({"available":false,"path":path,"reason":format!("HDC 无法运行:{} 请检查下载版本与同目录依赖文件,或重新选择工具。", error.message)}),
),
}
} }
"configure" => { "configure" => {
let selected: LocalPath = parse(value)?; let selected: LocalPath = parse(value)?;
let path = PathBuf::from(selected.path); let path = selected_tool(Path::new(&selected.path)).ok_or_else(|| {
if !executable(&path) { Fault::new(
return Err(Fault::new(
"hdc.invalid_tool", "hdc.invalid_tool",
"请选择可执行的官方 hdc 文件。", "未找到可执行的 HDC。请先完整解压官方 Command Line Tools,选择其目录或 toolchains 中的 hdc 文件;不能选择 ZIP 压缩包。",
)); )
} })?;
let path = path.canonicalize()?; let version = tool_version(&path, &self.cancel)?;
let version = command::run(
&path,
&["-v".into()],
Duration::from_secs(5),
&self.cancel,
8192,
)?;
if !version.contains("Ver:") && !version.to_lowercase().contains("hdc") {
return Err(Fault::new(
"hdc.invalid_tool",
"所选文件没有返回 HDC 版本信息。",
));
}
if let Some(settings) = &self.settings { if let Some(settings) = &self.settings {
fs::create_dir_all(settings.parent().unwrap())?; fs::create_dir_all(settings.parent().unwrap())?;
let temporary = settings.with_extension("tmp"); let temporary = settings.with_extension("tmp");
@@ -4,6 +4,76 @@ use std::{io::Write, time::Instant};
#[path = "screen_tests.rs"] #[path = "screen_tests.rs"]
mod screen_tests; mod screen_tests;
fn version_tool(path: &Path) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, "#!/bin/sh\nprintf 'Ver: fixture\\n'\n").unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).unwrap();
}
#[test]
fn configures_extracted_official_toolkit_and_preserves_selection_on_error() {
let (directory, mut manager) = fixture(false);
let settings = directory.path().join("data/harmony-tools.json");
Arc::get_mut(&mut manager).unwrap().settings = Some(settings.clone());
let root = directory.path().join("download");
let hdc = root.join("command-line-tools/sdk/default/openharmony/toolchains/hdc");
version_tool(&hdc);
let result = manager.call("configure", json!({"path": root})).unwrap();
assert_eq!(result["available"], true);
assert_eq!(result["path"], json!(hdc.canonicalize().unwrap()));
let saved: Value = serde_json::from_slice(&fs::read(&settings).unwrap()).unwrap();
assert_eq!(saved["hdcPath"], result["path"]);
assert!(manager
.call(
"configure",
json!({"path": directory.path().join("missing")})
)
.is_err());
assert_eq!(
manager.call("tool", json!({})).unwrap()["path"],
result["path"]
);
fs::remove_file(&hdc).unwrap();
assert_eq!(manager.call("tool", json!({})).unwrap()["available"], false);
}
#[test]
fn rechecks_hdc_installed_after_manager_start() {
const CHILD: &str = "HARMONY_TOOL_DISCOVERY_TEST";
if let Some(root) = env::var_os(CHILD) {
let root = PathBuf::from(root);
let manager = Manager::new();
// Reproduce the running plugin's unconfigured state without depending
// on whether the test machine also has a system-wide DevEco install.
*manager.path.lock().unwrap() = None;
let hdc = root.join("bin/hdc");
version_tool(&hdc);
let result = manager.call("tool", json!({})).unwrap();
assert_eq!(result["available"], true, "{result}");
assert_eq!(result["path"], json!(hdc.canonicalize().unwrap()));
return;
}
let root = tempfile::tempdir().unwrap();
let output = std::process::Command::new(env::current_exe().unwrap())
.args([
"--exact",
"hdc::tests::rechecks_hdc_installed_after_manager_start",
"--nocapture",
])
.env(CHILD, root.path())
.env("HOME", root.path())
.env("PATH", root.path().join("bin"))
.env("TROVE_DATA_DIR", root.path().join("data"))
.output()
.unwrap();
assert!(
output.status.success(),
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn fixture(debug: bool) -> (tempfile::TempDir, Arc<Manager>) { fn fixture(debug: bool) -> (tempfile::TempDir, Arc<Manager>) {
let directory = tempfile::tempdir().unwrap(); let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("hdc"); let path = directory.path().join("hdc");
+6 -1
View File
@@ -520,7 +520,12 @@ $("folder-form").addEventListener("submit", (event) => {
await load(); await load();
}); });
}); });
const openManagement = management({ native, host, notice }); const openManagement = management({
native,
host,
notice,
openUrl: (url) => trove.call("host.runtime", "openUrl", { url }),
});
for (const view of ["files", "management"]) for (const view of ["files", "management"])
$("tab-" + view).onclick = () => { $("tab-" + view).onclick = () => {
state.view = view; state.view = view;
+78 -23
View File
@@ -135,36 +135,91 @@
<div class="management-card"> <div class="management-card">
<div class="management-heading"> <div class="management-heading">
<h2>设备管理</h2> <h2>设备管理</h2>
<button id="hdc-configure" class="secondary"> <div class="management-actions">
选择 HDC 工具 <button id="hdc-configure" class="secondary">
</button> 选择 HDC 文件
</button>
<button id="hdc-refresh" class="secondary">刷新连接</button>
</div>
</div> </div>
<p id="hdc-tool-status" class="muted">正在检查官方连接工具…</p> <p id="hdc-tool-status" class="muted" role="status">
<p class="muted"> 正在检查官方连接工具…
在手机开发者选项中启用 USB 调试并信任这台 Mac。请保留 HDC
同目录中的依赖文件。文件传输无需启用调试。
</p> </p>
<div class="device-picker-row"> <section
<label for="hdc-target" class="sr-only">管理设备</label id="hdc-setup"
><select id="hdc-target"></select class="tool-setup"
><button id="hdc-refresh" class="secondary">刷新连接</button> aria-label="安装官方连接工具"
hidden
>
<h3>准备好工具,让手机连接桌面</h3>
<p>
应用管理与投屏需要华为官方 HDC 连接工具。完成以下三步即可开始。
</p>
<ol class="tool-steps">
<li>
<strong>下载官方工具</strong>
<p>
前往华为开发者官网下载 Command Line Tools。Apple 芯片选择
Mac (ARM64),Intel 芯片选择 Mac
(x86);如页面提示,请登录华为账号。
</p>
<button id="hdc-download" class="primary">
前往华为官网下载 ↗
</button>
<p id="hdc-download-status" class="muted" role="status"></p>
</li>
<li>
<strong>解压,选择工具目录</strong>
<p>
将下载的 ZIP 完整解压并保留整个目录,然后选择解压后的
Command Line Tools 文件夹。已有 DevEco
Studio?也可用上方「选择 HDC 文件」选择其 SDK 中的 hdc。
</p>
<button id="hdc-directory" class="secondary">
选择已解压的工具目录
</button>
</li>
<li>
<strong>连接并授权手机</strong>
<p>
用 USB 连接手机,在手机开发者选项中启用 USB
调试,解锁并允许这台 Mac
的调试请求。配置工具后会自动检查连接,也可点击「刷新连接」。
</p>
</li>
</ol>
<p class="muted">
只收发文件?可直接使用「文件传输」,无需安装 HDC。
</p>
</section>
<div id="hdc-connection" hidden>
<p class="muted">
工具已就绪。在手机开发者选项中启用 USB 调试,解锁手机并允许这台
Mac 的调试请求。
</p>
<div class="device-picker-row">
<label for="hdc-target" class="sr-only">管理设备</label
><select id="hdc-target"></select>
</div>
<p id="hdc-connection-status" role="status">等待管理连接</p>
<div id="hdc-capabilities" class="capabilities"></div>
<div class="management-actions">
<button id="device-screenshot" class="secondary">
保存截图</button
><button id="device-logs" class="secondary">导出日志</button
><button id="device-report" class="secondary">诊断报告</button>
</div>
<details>
<summary>设备信息</summary>
<pre id="device-info"></pre>
</details>
</div> </div>
<p id="hdc-connection-status" role="status">等待管理连接</p>
<div id="hdc-capabilities" class="capabilities"></div>
<div class="management-actions">
<button id="device-screenshot" class="secondary">保存截图</button
><button id="device-logs" class="secondary">导出日志</button
><button id="device-report" class="secondary">诊断报告</button>
</div>
<details>
<summary>设备信息</summary>
<pre id="device-info"></pre>
</details>
</div> </div>
<section <section
id="screen-panel" id="screen-panel"
class="management-card screen-card" class="management-card screen-card"
aria-label="投屏与控制" aria-label="投屏与控制"
hidden
> >
<div class="management-heading"> <div class="management-heading">
<div> <div>
@@ -256,7 +311,7 @@
画面连续采集,刷新速度以设备响应为准。切换工作区、隐藏窗口或断开连接会停止投屏。 画面连续采集,刷新速度以设备响应为准。切换工作区、隐藏窗口或断开连接会停止投屏。
</p> </p>
</section> </section>
<div class="management-card"> <div id="apps-panel" class="management-card" hidden>
<div class="management-heading"> <div class="management-heading">
<h2>应用</h2> <h2>应用</h2>
<div class="management-actions"> <div class="management-actions">
+38 -7
View File
@@ -1,7 +1,7 @@
import { screen } from "./screen.js"; import { screen } from "./screen.js";
import { bytes } from "./format.js"; import { bytes } from "./format.js";
export function management({ native, host, notice }) { export function management({ native, host, notice, openUrl }) {
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const call = (method, params = {}) => native(`hdc.${method}`, params); const call = (method, params = {}) => native(`hdc.${method}`, params);
let screenPanel; let screenPanel;
@@ -31,7 +31,8 @@ export function management({ native, host, notice }) {
const locked = state.working || state.busy || state.screenBusy; const locked = state.working || state.busy || state.screenBusy;
for (const button of $("management").querySelectorAll("button")) for (const button of $("management").querySelectorAll("button"))
if (!button.closest("#screen-panel")) button.disabled = locked; if (!button.closest("#screen-panel")) button.disabled = locked;
$("hdc-target").disabled = locked; $("hdc-target").disabled =
locked || !state.available || !state.devices.length;
const connected = state.devices.some( const connected = state.devices.some(
(d) => d.id === state.target && d.state === "Connected", (d) => d.id === state.target && d.state === "Connected",
); );
@@ -91,9 +92,23 @@ export function management({ native, host, notice }) {
} }
function showTool(tool) { function showTool(tool) {
state.available = !!tool.available; state.available = !!tool.available;
$("hdc-setup").hidden = state.available;
for (const id of ["hdc-connection", "screen-panel", "apps-panel"])
$(id).hidden = !state.available;
$("hdc-tool-status").textContent = tool.available $("hdc-tool-status").textContent = tool.available
? `${tool.version} · ${tool.path}` ? `${tool.version} · ${tool.path}`
: "未发现 HDC。选择 DevEco Studio 或官方 Command Line Tools 中的 hdc 文件。"; : tool.reason || "未发现 HDC 连接工具,请先完成以下准备。";
if (!state.available) {
clearDevice();
state.devices = [];
state.target = "";
$("hdc-target").replaceChildren();
$("hdc-connection-status").textContent = "配置官方工具后检查手机连接。";
}
}
async function refreshTool() {
showTool(await call("tool"));
if (state.available) await refresh();
} }
function renderApps() { function renderApps() {
const filter = $("apps-search").value.trim().toLowerCase(); const filter = $("apps-search").value.trim().toLowerCase();
@@ -283,6 +298,24 @@ export function management({ native, host, notice }) {
} }
const confirm = async (title, message) => const confirm = async (title, message) =>
(await host("confirm", { title, message })).confirmed; (await host("confirm", { title, message })).confirmed;
$("hdc-download").onclick = () =>
action(async () => {
await openUrl(
"https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos",
);
$("hdc-download-status").textContent =
"已在浏览器打开华为官方下载页。下载并解压后,返回此处选择工具目录。";
});
$("hdc-directory").onclick = () =>
action(async () => {
const { path } = await host("openDirectory", {
title: "选择已解压的 Command Line Tools 目录",
});
if (path) {
showTool(await call("configure", { path }));
await refresh();
}
});
$("hdc-configure").onclick = () => $("hdc-configure").onclick = () =>
action(async () => { action(async () => {
const { path } = await host("openFile", { const { path } = await host("openFile", {
@@ -293,7 +326,7 @@ export function management({ native, host, notice }) {
await refresh(); await refresh();
} }
}); });
$("hdc-refresh").onclick = () => action(() => refresh()); $("hdc-refresh").onclick = () => action(refreshTool);
$("hdc-target").onchange = () => $("hdc-target").onchange = () =>
action(async () => { action(async () => {
state.target = $("hdc-target").value; state.target = $("hdc-target").value;
@@ -412,10 +445,8 @@ export function management({ native, host, notice }) {
schedule(); schedule();
return; return;
} }
const tool = await call("tool"); await refreshTool();
showTool(tool);
state.ready = true; state.ready = true;
if (tool.available) await refresh();
}).finally(scheduleMonitor); }).finally(scheduleMonitor);
}; };
} }
+30
View File
@@ -875,6 +875,36 @@ dialog li {
font-size: 13px; font-size: 13px;
color: var(--muted); color: var(--muted);
} }
.tool-setup {
border-top: 1px solid var(--line);
margin-top: 18px;
padding-top: 6px;
}
.tool-setup h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.tool-steps {
padding-left: 23px;
display: grid;
gap: 20px;
margin: 24px 0;
}
.tool-steps li {
padding-left: 6px;
}
.tool-steps li::marker {
color: var(--blue);
font-weight: 600;
}
.tool-steps p {
color: var(--muted);
margin: 7px 0 12px;
}
#hdc-download-status:empty {
display: none;
}
.application-layout { .application-layout {
display: grid; display: grid;
grid-template-columns: minmax(180px, 1fr) minmax(200px, 1fr); grid-template-columns: minmax(180px, 1fr) minmax(200px, 1fr);
+12 -4
View File
@@ -1,6 +1,6 @@
# Harmony Connector # Harmony Connector
插件 ID:`dev.trove.harmony-connector`。当前版本:`0.3.0`。 插件 ID:`dev.trove.harmony-connector`。当前版本:`0.3.1`。
macOS 上的鸿蒙设备连接工具。通过 USB 收发文件与完整文件夹;使用官方 HDC 管理应用、保存截图、导出日志和调试应用数据。功能按连接工具、设备实际能力和授权开放,不按 HarmonyOS 大版本或机型名称限制。 macOS 上的鸿蒙设备连接工具。通过 USB 收发文件与完整文件夹;使用官方 HDC 管理应用、保存截图、导出日志和调试应用数据。功能按连接工具、设备实际能力和授权开放,不按 HarmonyOS 大版本或机型名称限制。
@@ -33,13 +33,19 @@ macOS 上的鸿蒙设备连接工具。通过 USB 收发文件与完整文件夹
## 设备与应用 ## 设备与应用
在「设备与应用」选择官方 SDK 的 `hdc` 可执行文件。插件会尝试发现 PATH 或 `/Applications`、`~/Applications` 中 DevEco Studio 的内置工具。典型路径: 首次进入「设备与应用」,如果没有可用 HDC,页面会显示安装引导:
1. 点击「前往华为官网下载」,在浏览器打开 [Command Line Tools 官方下载页](https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos)。Apple Silicon 选择 Mac (ARM64),Intel 选择 Mac (x86),按官网提示登录华为账号并下载。
2. 完整解压 ZIP,点击「选择已解压的工具目录」。可选择包含 `command-line-tools` 的解压目录、`command-line-tools` 本身,或其 `toolchains` 目录;插件定位 HDC、运行版本检查,成功后保存路径。错误选择和取消不会覆盖已有配置。
3. 手机连接 USB,启用开发者模式与 USB 调试,解锁并允许这台 Mac 的调试请求。配置成功后自动检查设备;「刷新连接」也会重新检查工具,支持启动后才安装到 PATH 的 HDC。
已有 DevEco Studio 或 SDK 的用户也可点击「选择 HDC 文件」。插件启动时尝试发现 PATH 或 `/Applications`、`~/Applications` 中 DevEco Studio 的内置工具。典型路径:
```text ```text
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc /Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
``` ```
也可使用华为官方 Command Line Tools。保留 HDC 同目录的依赖库,插件不重打包官方 SDK。选择路径保存在插件自己的数据目录中。手机需启用开发者模式与 USB 调试,并信任这台 Mac。管理页明确选择 HDC 设备,不根据 MTP USB 地址猜测二者对应关系。 请保留完整的工具目录及同目录依赖库,插件不重打包官方 SDK。选择路径保存在插件自己的数据目录中;工具被移动、删除或无法运行时重新显示安装引导和原因。工具准备完成前隐藏空设备列表及不可用的管理面板,文件传输仍可直接使用。管理页明确选择 HDC 设备,不根据 MTP USB 地址猜测二者对应关系。
| 操作 | 行为与边界 | | 操作 | 行为与边界 |
| --- | --- | | --- | --- |
@@ -101,7 +107,9 @@ curios/harmony-connector/backend/harmony-connector --check-screen /absolute/path
该命令走 UI 相同的后端接口,采集并解码 3 帧,输出耗时,最后关闭会话;不启用控制、不发送输入。首次 HDC 连接显示 Unauthorized 时,在手机上允许 USB 调试。`hdc -v` 能运行但管理失败时,先用 `hdc list targets -v` 检查授权状态。 该命令走 UI 相同的后端接口,采集并解码 3 帧,输出耗时,最后关闭会话;不启用控制、不发送输入。首次 HDC 连接显示 Unauthorized 时,在手机上允许 USB 调试。`hdc -v` 能运行但管理失败时,先用 `hdc list targets -v` 检查授权状态。
本次自动化验证通过 39 项原生测试、27 项 UI/格式/坐标测试、3 项 USB C 桥接检查及 3 项底座协议检查。浏览器另外验证了持续显示模拟画面、启用控制、中文输入、放大和停止清空;模拟数据不会包含在发布包内。官方 Command Line Tools 26.0.0.821(Mac ARM)的 HDC 返回 `Ver: 3.2.0f`,已识别真机,但当次状态为 Unauthorized,故投屏和输入的真机效果尚未验收,不能将上述替身测试当作真机通过。 0.3.0 自动化验证通过 39 项原生测试、27 项 UI/格式/坐标测试、3 项 USB C 桥接检查及 3 项底座协议检查。浏览器另外验证了持续显示模拟画面、启用控制、中文输入、放大和停止清空;模拟数据不会包含在发布包内。官方 Command Line Tools 26.0.0.821(Mac ARM)的 HDC 返回 `Ver: 3.2.0f`,已识别真机,但当次状态为 Unauthorized,故投屏和输入的真机效果尚未验收,不能将上述替身测试当作真机通过。
0.3.1 的安装引导验证通过 41 项原生测试、30 项 UI/格式/坐标测试和 6 项 USB/协议检查(共 77 项)。新增回归覆盖官网下载入口、选择已解压目录、取消/错误选择保留配置、启动后安装工具、工具丢失后返回引导。浏览器以明确标记的模拟工具检查了引导和配置后的界面切换。使用已下载的官方工具目录调用实际发布后端,成功定位 HDC 并返回 `Ver: 3.2.0f`;设备当时仍为 Unauthorized,后续投屏验收因无已授权设备而停止。
## 构建与发布 ## 构建与发布
+16 -1
View File
@@ -2,7 +2,7 @@
"schemaVersion": 1, "schemaVersion": 1,
"id": "trove-official", "id": "trove-official",
"name": "Trove 官方软件源", "name": "Trove 官方软件源",
"generatedAt": "2026-09-10T06:14:13.482Z", "generatedAt": "2026-09-10T06:33:16.439Z",
"curios": [ "curios": [
{ {
"id": "dev.trove.qa.host", "id": "dev.trove.qa.host",
@@ -200,6 +200,21 @@
"name": "Harmony Connector", "name": "Harmony Connector",
"description": "连接鸿蒙设备,收发完整文件夹、管理应用,查看手机画面并通过键鼠操作。", "description": "连接鸿蒙设备,收发完整文件夹、管理应用,查看手机画面并通过键鼠操作。",
"versions": [ "versions": [
{
"version": "0.3.1",
"engine": {
"trove": ">=0.1.0 <1.0.0"
},
"releasedAt": "2026-09-10T06:33:16.439Z",
"artifacts": [
{
"target": "aarch64-apple-darwin",
"url": "packages/dev.trove.harmony-connector/0.3.1/aarch64-apple-darwin.curio",
"sha256": "1509fe112cbbeae11c800ac4f637932813468f9adffc7ef1222e5233053a8d3c",
"size": 3032158
}
]
},
{ {
"version": "0.3.0", "version": "0.3.0",
"engine": { "engine": {
+67 -1
View File
@@ -59,6 +59,9 @@ async function app(t) {
screenControl: false, screenControl: false,
screenFrameWait: null, screenFrameWait: null,
screenFrameError: null, screenFrameError: null,
openedUrls: [],
selectedDirectory: "/fixture/folder",
configureError: null,
}; };
const { createMockBridge } = await import( const { createMockBridge } = await import(
pathToFileURL(path.join(directory, "sdk/index.js")) pathToFileURL(path.join(directory, "sdk/index.js"))
@@ -120,6 +123,18 @@ async function app(t) {
version: "HDC test", version: "HDC test",
path: "/fixture/hdc", path: "/fixture/hdc",
})), })),
handler("hdc.configure", () => {
if (model.configureError) throw model.configureError;
model.hdcAvailable = true;
return { available: true, version: "HDC test", path: "/fixture/hdc" };
}),
[
"host.runtime.openUrl",
({ url }) => {
model.openedUrls.push(url);
return { ok: true };
},
],
handler("hdc.devices", () => ({ handler("hdc.devices", () => ({
devices: model.hdcConnected devices: model.hdcConnected
? [{ id: "phone", transport: "USB", state: "Connected" }] ? [{ id: "phone", transport: "USB", state: "Connected" }]
@@ -152,7 +167,7 @@ async function app(t) {
})), })),
handler("hdc.start", () => ({ id: 9 })), handler("hdc.start", () => ({ id: 9 })),
["host.dialog.confirm", () => ({ confirmed: model.confirmed })], ["host.dialog.confirm", () => ({ confirmed: model.confirmed })],
["host.dialog.openDirectory", () => ({ path: "/fixture/folder" })], ["host.dialog.openDirectory", () => ({ path: model.selectedDirectory })],
handler("info", () => ({ connection: model.connection, busy: false })), handler("info", () => ({ connection: model.connection, busy: false })),
handler("devices", () => ({ devices: model.attached ? [device] : [] })), handler("devices", () => ({ devices: model.attached ? [device] : [] })),
handler("connect", () => (model.connection = { device, storages })), handler("connect", () => (model.connection = { device, storages })),
@@ -374,6 +389,57 @@ test("management is optional and missing HDC does not disable file transfers", a
await connect(); await connect();
assert.equal($("browser").hidden, false); assert.equal($("browser").hidden, false);
}); });
test("missing HDC guides official download and configuring an extracted toolkit", async (t) => {
const { $, click, model } = await app(t);
await click("tab-management");
assert.equal($("hdc-setup")?.hidden, false);
assert.match($("hdc-setup").textContent, /Mac.*ARM64/);
assert.match($("hdc-setup").textContent, /解压/);
assert.match($("hdc-setup").textContent.replace(/\s+/g, " "), /USB 调试/);
await click("hdc-download");
assert.deepEqual(model.openedUrls, [
"https://developer.huawei.com/consumer/cn/download/command-line-tools-for-hmos",
]);
assert.equal($("hdc-setup").hidden, false);
assert.ok(!model.calls.includes("hdc.devices"));
await click("hdc-directory");
assert.deepEqual(
model.parameters.find(([name]) => name === "hdc.configure")[1],
{
path: "/fixture/folder",
},
);
assert.equal($("hdc-setup").hidden, true);
assert.equal($("hdc-target").options.length, 1);
});
test("refresh rechecks tool availability and clears stale devices if the tool disappears", async (t) => {
const { $, click, model } = await app(t);
await click("tab-management");
await click("hdc-refresh");
assert.ok(!model.calls.includes("hdc.devices"));
model.hdcAvailable = true;
await click("hdc-refresh");
assert.equal($("hdc-setup").hidden, true);
assert.equal($("screen-start").disabled, false);
model.hdcAvailable = false;
await click("hdc-refresh");
assert.equal($("hdc-setup").hidden, false);
assert.equal($("hdc-target").options.length, 0);
assert.equal($("screen-start").disabled, true);
});
test("cancelled or invalid toolkit selection preserves installation guidance", async (t) => {
const { $, click, model } = await app(t);
await click("tab-management");
model.selectedDirectory = null;
await click("hdc-directory");
assert.ok(!model.calls.includes("hdc.configure"));
model.selectedDirectory = "/fixture/wrong";
model.configureError = new Error("未找到 HDC,请选择解压后的工具目录。");
await click("hdc-directory");
assert.equal($("hdc-setup").hidden, false);
assert.ok(!model.calls.includes("hdc.devices"));
assert.match(document.body.textContent, /未找到 HDC/);
});
test("application installation requires confirmation and pins the reviewed package hash", async (t) => { test("application installation requires confirmation and pins the reviewed package hash", async (t) => {
const { $, click, model } = await app(t); const { $, click, model } = await app(t);
model.hdcAvailable = true; model.hdcAvailable = true;