fix(harmony): guide official HDC setup and recheck tool availability
This commit is contained in:
@@ -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
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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": {
|
||||||
|
|||||||
Binary file not shown.
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user