把 PicGo 桌面端(Electron + Vue 3)移植到 HarmonyOS PC(2in1 形态)的完整记录:适配过程(含构建时间线)、适配方案、真机调试流程、5 张真机运行截图、8 个真实问题(报错与异常行为)的定位与解法、以及上线前必须知道的限制。

真机环境(下文截图与日志全部来自该设备)

项目值
设备HUAWEI MateBook Pro(型号 HAD-W32,软件版本串 HAD-W24 7.0.0.105(SP10C00E100R13P5))
形态2in1(PC)
CPU / APIarm64-v8a / const.ohos.apiversion = 26
工具链DevEco Studio 6.1.1,hdc 3.2.0d
运行时libelectron(华为 Electron for HarmonyOS 发行版)
代码基线commit 5fc17e8 feat: add HarmonyOS (OHOS) support for PicGo

开源鸿蒙 PC 社区与代码仓

PicGo 鸿蒙 PC 适配欢迎加入开源鸿蒙 PC 社区,交流适配经验、问题与项目实践。

本项目代码托管在 AtomGit 的 OpenHarmony PC Developer 组织仓库:OpenHarmonyPCDeveloper/ohos_picgo。后续代码、适配说明和问题跟踪以该 PC 仓为准。

欢迎在 OpenHarmony PC Developer 项目申请页 申请新建项目;按照社区流程,先提交 Issue,再联系阿太创建仓库。
问题和适配建议请提交到 ohos_picgo Issues。

当前迁移能力按 T0 使用:JS 构建、HAP 打包、安装启动和界面渲染已验证;T1 的真实图片上传及 T2 的发布、稳定性和生态兼容性尚未完成验证。

1. 背景:为什么不能直接复用 Electron 二进制

Electron 官方发行二进制没有 OHOS target,鸿蒙也不是 Linux 的一个发行版(musl + OHOS 内核 + ArkUI 图形栈)。华为给出的路线是 libelectron:

  • Chromium / Node / V8 按 OHOS 重新编译,产出 .so + 一套 resfile 资源(icudtl.dat、snapshot_blob.bin、resources.pak 等);
  • 提供一个 ArkTS 的 web_engine HAR,把 Chromium 的窗口、输入、剪贴板、文件、蓝牙等系统能力对接到 ArkUI;
  • 应用侧只需要一个很薄的 HAP 壳(Ability + 页面),把 Electron 的 JS 产物塞进 resfile/resources/app。

于是 PicGo 的适配工作被拆成两半:

  1. 不改动业务逻辑:主进程/preload/渲染进程照旧用 electron-vite 打包,产出的 dist_electron 直接进 HAP;
  2. 只修平台差异:托盘、迷你窗口、启动模式、静态资源路径、原生 addon —— 这些在鸿蒙上缺失或行为不同。

2. 适配过程

先说清楚证据基础:这次适配在仓库里是一个 commit(5fc17e8),中间步骤没留在 git 历史里。所以本节的过程不是从提交记录读出来的,而是从三类可核对的物证复原:

物证位置能证明什么
hvigor 构建报告libelectron/ohos_hap/.hvigor/report/report-*.json(10 份)每次构建的发起时间
hvigor 构建日志.hvigor/outputs/build-logs/build.log.1、build.log每次构建的成败、耗时、ArkTS 告警原文
设备侧日志files/picgo-gui-local.log、hilog、hidumper每次真机运行的实际行为

2.1 起点:华为的 libelectron demo 工程

不是从零搭 HAP。起点是 libelectron 自带的 demo HAP 工程,配 docs/ 下的华为资料与示例(见 §7)。三条证据:

  • AppWindowAdapter.ets:33 的字段默认值至今是 private bundleName: string = 'com.huawei.ohos_electron';——华为 demo 的 bundleName。它在构造函数里被 bundleManager.getBundleInfoForSelfSync(...).name 覆盖(同文件 58-64),所以改了包名也不会串到 demo 应用上去。
  • electron 模块里保留着 demo 的演示页:pages/Index(XComponent 版)、pages/WindowNode、pages/WebPage、pages/Login、pages/SubWindow、pages/EmbeddedWindow、pages/StatusBarPage,以及华为账号登录用的 QuickLoginButtonComponent.ets(24.6 KB);resources/base/profile/main_pages.json 把它们全部登记着。
  • web_engine/oh-package.json5 里仍挂着 demo 的 libadapter.so 本地依赖路径。

遗留项(未清尾):AppScope/resources/base/element/string.json 的 app_name 至今是 "Electron",而 app.json5 里 label: "$string:app_name"。真机 bm dump 查到的 labelResource 仍指向该字符串——桌面与任务栏上显示的应用名是 “Electron”,不是 PicGo。

与 demo 最关键的差别:demo 的 JS 侧只有一个 index.js + index.html(见 docs/electron-example/),而 PicGo 要跑的是 electron-vite 的 main/preload/renderer 三件套,外加 node_modules 与 public/ 静态资源。

2.2 第一步:把 JS 产物塞进 HAP

libelectron 的资源根就是 process.resourcesPath,在 HAP 内落于 .../resources/resfile/。scripts/build-ohos.js 按 electron-builder 的布局把三样东西搬进去(七步流程见 §3.2),真机上由此得到可用的入口路径:

file:///data/storage/el1/bundle/electron/resources/resfile/resources/app/dist_electron/renderer/index.html#/main/...

这一步踩了两个坑:

  1. externalizeDepsPlugin() 会让 bundle 里保留 require(),所以必须在 resfile/resources/app/ 下再装一次生产依赖,否则运行时 MODULE_NOT_FOUND;
  2. src/main/utils/env.ts 原本按 isDev 判断静态资源目录(cwd()/public vs resourcesPath/public),而 libelectron debug 构建下 app.isPackaged 恒为 false——不加例外就会指向根本不存在的 cwd()/public。修法是引入 isOhosRuntime:鸿蒙一律走 resourcesPath/public。

2.3 第二步:白屏,换掉渲染链路

Ability 起来了、窗口也画出来了,但内容区永远空白。定位方法是直接看适配层日志:

hdc shell hilog -x | grep -iE "NodeHandle|NodeContent|bind node handle"
  • 只看到 bind node handle with node content failed, not find node content for id... → 走的是 demo 默认的 XComponent 链路(pages/WindowNode);
  • 看到 NodeHandleImpl Initialize success, support node handle feature → NodeContent 链路已生效。

改法是让 BrowserAbility 用回 WebBaseAbility 默认的 getContentPath()(pages/NodeHandleWindow),并把 AppWindowAdapter.bind() 从"一次性取 NodeContent"改成"轮询取"(100 次 × 100ms,最长 10s)。两处细节问题见 §5.1、§5.2。

2.4 第三步:把鸿蒙没有的能力摘掉

逐个决定"关掉"还是"降级"。判断顺序是:先让它跑起来 → 看哪里报错、哪里功能空转 → 再加 isOhos 分支兜住。完整改动清单见 §3.3,其中四处是"看起来正常、其实点不动"的典型:

看起来实际处理
有托盘图标鸿蒙没有托盘 API,创建即失败system/index.ts:271 直接不创建
标题栏有迷你窗口按钮鸿蒙不创建该窗口,点了抛异常见 §5.6(按钮本身也应隐藏)
启动时弹 DevToolsdebug 构建 isDev 恒真windowList.ts:120 加 && !isOhos(§5.3)
设置页能选"启动模式"鸿蒙端硬编码直开主窗口设置项应置灰,见 §6.2

另外有一处真机结论留在代码注释里:AppWindowAdapter.ets:107-108 记载,StartOptions 里若带 processMode / startupVisibility / startWindowIcon,从 JS/Node 上下文发起窗口创建会报 401 invalid parameter,因此窗口只传 bounds。

2.5 第四步:处理原生模块

clip-filepaths 只提供 macOS/Windows/Linux 的 .node,鸿蒙没有对应构建 → 在组装阶段给它打桩(见 §5.5)。由此得出一条通用规则:凡是带 .node 的依赖,要么有鸿蒙构建,要么打桩,否则启动即崩。

2.6 第五步:真机联调闭环

整个过程就是这一个循环,工具用法见 §4.4:

改代码 → pnpm run build:ohos → ./hvigorw assembleHap … → hdc app install
      → aa start → snapshot_display 截图 / hilog / picgo-gui-local.log / hidumper
      → 定位 → 回到第一步

交互路径用 hdc shell uitest uiInput click x y 注入,比手点更可重复——§5.6 那个崩溃就是这样定点复现出来的。

2.7 构建时间线(有据可查)

hvigor 日志里的完整构建记录(时间为构建机本地时间,UTC−07:00):

#时间结果耗时备注
12026-07-13 19:27:27失败1.5 sUnknown mode 'debug' is specified in the command line(Error Code 00308018)
219:28:02成功21.9 s改用 --mode module 后首次成功
319:31:56成功16.2 s
419:36:17成功34.8 s
519:40:40成功19.1 s
619:57:12成功31.4 s首份 report 落盘于 19:56:41
720:02:15成功15.1 s
820:08:13成功59.1 s当日最慢
920:12:53成功16.0 s
1020:35:54成功15.8 s
1120:45:01成功30.1 s
1221:08:09成功16.9 s
1321:16:34成功14.7 s最后一次;21:39:44 提交 5fc17e8
142026-08-10 20:46:43成功17.2 s距上次构建近一个月
1520:48:01成功29.5 s产出 electron-default-signed.hap(415 MB,20:48 落盘)

读这张表能看出三件事:

  1. 主适配集中在 2026-07-13 的 1 小时 50 分钟:13 次构建,1 次失败、12 次成功,成功构建平均约 24 秒。构建之所以快,是因为 HAP 里只有 ArkTS 壳,JS 产物是"拷进去"的——真正耗时的是 build:ohos 里的 electron-vite build 与 npm install,那部分不在这份日志里。
  2. 首次失败是 hvigor 命令行用法问题:--mode 只接受 module,debug/release 由 buildMode 决定(日志里可见 buildMode: 'Debug')。正确命令就是 README 里那条 --mode module -p product=default。
  3. 构建成功之后还有一轮真机往返:8 月 10 日又补了 2 次构建(HAP 落盘 20:47/20:48),设备侧日志里对应 08-10、08-11、08-14、09-13 四次运行。构建通过 ≠ 真机可用——渲染链路是否打通,只有 hilog 和窗口截图能回答,这正是 §2.3 那次返工的原因。

2.8 ArkTS 编译告警(每轮都刷,但不是错误)

单次构建会打 337 条 WARN: ArkTS(去重后 308 处)。它们全部来自 libelectron 的 HAR 与 demo 遗留页面,没有一条出自 PicGo 自己改过的代码——所以不影响构建成功,但必须会分辨。按条数分布:

告警单次条数出现位置是否要处理
Function may throw exceptions. Special handling is required.117adapter/*.ets 等否,提示自行 try/catch
To use this API, you need to apply for the permissions: …70adapter/*.ets、QuickLoginButtonComponent.ets视情况:真正用到置顶/隐私窗/悬浮窗/蓝牙时才报错,权限须在 module.json5 申请(出现最多的是 ACCESS_BLUETOOTH,38 次)
Classes cannot be used as objects (arkts-no-classes-as-obj)69各 jsbindings/*Bind.ets否,框架写法
… has been deprecated(vp2px / getShared / getContext / showToast / show / pushUrl / back / getParams)45pages/*.ets、components/*.ets否;但自研 ArkTS 代码应避开
The system capacity of this api '…' is not supported on all devices30statusBarManager、selectMode、DocumentSelectMode 等要:这些能力在 PC 上不保证存在,用到就得做降级
The '…' API is supported since SDK version 23. However, the current compatible SDK version is 5.0.5(17).6打印、后台相关 API要:compatibleSdkVersion 是 5.0.5(17),用了更高版本 API 需自行评估(工作区已在 build-profile.json5 里补 targetSdkVersion: 6.1.1(24))

还有一条有意思的编辑痕迹:同一批告警的行号会随构建漂移——AppWindowAdapter.ets 里的告警在第 3 次构建位于 153/157/159 行,第 10 次变成 152/156/158,第 13 次又变成 141/145/147。行号整体上移,说明这个文件在那两小时里被反复改短过,正好对应 §2.4 里"StartOptions 只传 bounds 以避开 401"那处改动。

3. 适配方案

3.1 三层结构

resfile (Electron 资源)

web_engine HAR (libelectron)

HAP 应用层 (electron 模块 / ArkTS)

NodeContent

ContentSlot

require()

EntryAbility / BrowserAbility

pages/NodeHandleWindow
(NodeContent 宿主)

WebAbility / WebBaseAbility

AppWindowAdapter
NodeHandleManager

CommandLineAdapter
CmdArgsHelper

resources/app/*
dist_electron + node_modules

主进程 / preload / renderer

resources/public/*
静态资源 (logo、i18n)

Chromium 渲染管线

窗口渲染链路(实测):

ArkUI NodeHandleWindow
  └─ WebNodeHandleWindow (web_engine 组件)
       └─ NodeContent  ──绑定──▶  AppWindowAdapter.bind(id, cb)
                                     └─▶ Chromium ContentSlot 绘制窗口表面

WebBaseAbility.getContentPath() 决定用哪条链路(web_engine/src/main/ets/ability/WebBaseAbility.ets:85-91):

protected getContentPath(): string {
   if (this.nativeContext.IsSupportNodeHandleFeature()) {
    return 'pages/NodeHandleWindow';   // NodeContent:Chromium 直接绘制到窗口表面
  } else {
    return 'pages/Index';              // 旧链路:XComponent 中转
  }
}

真机日志确认走的是前者(hdc shell hilog -x,2026-09-13 11:38:39.463):

09-13 11:38:39.463  A00001/com.jiujiang.picgo/Adapter:
  NodeHandleImpl Initialize success, support node handle feature

3.2 JS 产物如何进 HAP

scripts/build-ohos.js 做了 7 件事,目录映射刻意对齐 electron-builder 的产物布局:

步骤行为目标路径
1调 electron-vite build 编译 main/preload/rendererdist_electron/
2清空目标目录—
3拷贝编译产物resfile/resources/app/dist_electron
4拷贝静态资源(对齐 process.resourcesPath + '/public')resfile/resources/public
5写最小 package.json(main: dist_electron/main/index.js)resfile/resources/app/package.json
6在 app/ 内 npm install --omit=dev(externalizeDepsPlugin 保留了 require())resfile/resources/app/node_modules
7给不支持鸿蒙的原生模块打桩见 5.5

渲染进程在真机上的实际 URL(hilog 抓取,能直接证明第 3~5 步生效):

file:///data/storage/el1/bundle/electron/resources/resfile/resources/app/dist_electron/renderer/index.html#/main/plugins

3.3 应用侧代码适配清单

全部改动都遵循一个原则:新增 isOhos 分支,不改动其它平台行为。

位置改动为什么
src/universal/utils/common.ts:147-148新增 isOhos;isLinux 顺带并入 isOhosprocess.platform 在 libelectron 下为 ohos/openharmony
src/main/utils/env.ts:7-13isDev 仍为 !app.isPackaged,但静态资源目录改为「非 OHOS 才用 cwd()/public,否则用 process.resourcesPath/public」libelectron debug 构建下 app.isPackaged 恒为 false,按 isDev 走会取错 public 目录
src/main/apis/app/window/windowList.ts:43启动模式兜底值:OHOS → SHOW_MAIN_WINDOW鸿蒙没有托盘/悬浮窗,只能直开主窗口
src/main/apis/app/window/windowList.ts:114SETTING_WINDOW 的 options.show = trueOHOS 上从 JS 调 showAbility() 会失败(16000067),必须创建即可见
src/main/apis/app/window/windowList.ts:120if (isDev && !isOhos) openDevTools()OHOS debug 构建 isDev 恒真,会每次启动都弹 DevTools
src/main/apis/app/window/windowList.ts:161MINI_WINDOW.isValid = process.platform !== 'darwin' && !isOhos没有悬浮窗能力,不创建该窗口
src/main/lifeCycle/index.ts:89-101OHOS 跳过 settingWindow.show()/focus() 与迷你窗创建同上,窗口已 show: true 直出
src/main/apis/app/system/index.ts:271if (!isOhos && !tray) createTray()鸿蒙无系统托盘 API
libelectron/ohos_hap/electron/src/main/ets/entryability/BrowserAbility.ets:45-49使用 WebBaseAbility 默认的 getContentPath(),不再显式指向 pages/WindowNodeWindowNode 走 XComponent,永远不会注册 NodeContent,导致 bind() 超时、Chromium 不绘制(见 5.1)
web_engine/.../adapter/AppWindowAdapter.ets:794-819bind() 加 100 × 100ms 轮询重试Ability 启动与 NodeContent 注册存在时序竞争(见 5.2)

3.4 HAP 工程配置

// AppScope/app.json5
{ "app": { "bundleName": "com.jiujiang.picgo", "versionName": "1.0.0", ... } }

// build-profile.json5(app 级)
"compatibleSdkVersion": "5.0.5(17)",
"targetSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"

// electron/src/main/module.json5
"deviceTypes": ["2in1", "tablet"],
"abilities": [
  { "name": "EntryAbility",   "srcEntry": "./ets/entryability/EntryAbility.ets" },
  { "name": "BrowserAbility", "srcEntry": "./ets/entryability/BrowserAbility.ets",
    "process": ':browser' }   // Chromium 跑在独立进程
]

真机校验:

$ hdc shell bm dump -n com.jiujiang.picgo | grep -E 'deviceTypes|versionName|compileSdkVersion'
"deviceTypes": [ "2in1", "tablet" ]
"compileSdkVersion": "6.1.1.125"
"versionCode": 1000000
"versionName": "1.0.0"

3.5 README 规范:软件迁移能力分级

面向 OpenHarmony PC 的迁移项目,README 必须同时说明原软件、运行时、构建入口、已验证能力、已知限制和证据环境,不能只写“支持鸿蒙”或只提供一个 HAP 下载地址。迁移能力按项目当前已经验证的最低完整阶段标注:

阶段能力定义README 必须给出的证据
T0:可构建运行能完成依赖安装、JS 产物构建、HAP 打包、安装并在 OpenHarmony PC 上启动工具链版本、完整构建命令、产物路径、设备启动命令
T1:核心功能适配主界面、核心业务流程、PC 输入与窗口交互可用;平台不支持的能力已明确禁用或降级功能验证清单、设备与系统版本、截图或日志、未验证功能
T2:可交付与生态适配具备稳定的 PC 体验和发布条件,完成签名、性能、异常恢复、升级以及插件/原生模块兼容性验证发布包信息、稳定性与性能结果、签名说明、兼容性矩阵、问题反馈入口

README 应在醒目位置写明当前阶段,并分别列出“已验证”“不支持/已降级”“未验证”。阶段结论以实际设备验证为准:能打包不等于核心功能可用,核心功能可用也不等于达到发布条件。本文件按上述规范记录 PicGo 的 OpenHarmony PC 迁移能力,分级示例参考 ohos_markdownify。


4. 真机调试全流程

以下命令里的 hdc 来自 DevEco Studio:
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc

4.1 构建 + 安装

# 1) JS 产物 + 资源组装进 HAP 工程
pnpm run build:ohos

# 2) 构建 HAP(在 libelectron/ohos_hap 下)
./hvigorw assembleHap --mode module -p product=default

# 3) 安装(先确认设备在线)
hdc list targets -v                 # 3QC0****1875  USB  Connected
hdc app install electron/build/default/outputs/default/electron-default-signed.hap

产物体积:electron-default-signed.hap ≈ 415 MB,electron-default-unsigned.hap ≈ 414 MB(本地实测)。绝大部分是 Chromium 运行时 + node_modules,属于预期,不是打包错误。

4.2 启动与观察

hdc shell aa start -a EntryAbility -b com.jiujiang.picgo
hdc shell ps -ef | grep picgo          # 主进程 + :Renderer + :GPU + :NetworkService

真机进程形态(多进程架构可作启动成功的第一个信号):

com.jiujiang.picgo                 # 主进程(Node)
com.jiujiang.picgo:NetworkService
com.jiujiang.picgo:Renderer
com.jiujiang.picgo:GPU

4.3 运行截图(真机实测)

图 1 · 主界面(上传页):拖拽区、链接格式、快捷上传全部正常渲染。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 2 · 图床设置:图床列表、PicGo Cloud 配置面板正常。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 3 · PicGo Cloud:登录面板与云端功能入口正常。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4 · 设置 · 通用:语言、启动模式、开机自启等表单项渲染正常(启动模式这一项的行为要注意,见 6.2)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5 · 插件页:插件搜索与「插件设置」双栏布局正常。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.4 调试四件套

① 截图(不经外部相机,直接设备侧落盘再拉回):

hdc shell snapshot_display -f /data/local/tmp/shot.jpeg
# 注意:本机目标目录必须已存在,否则报 "Error opening file: no such file or directory"
mkdir -p /tmp/shots && hdc file recv /data/local/tmp/shot.jpeg /tmp/shots/shot.jpeg

真机分辨率 3120 × 2080,单张约 340 KB。

② 日志(系统级 + 应用级,两层都要看):

# 系统侧:Chromium/ArkTS 适配层日志
hdc shell hilog -x | grep -E "picgo|Adapter|chromium|NodeHandle" | grep -v GetWindowFromWidget
hdc shell hilog -r                      # 清缓冲,抓冷启动全过程

# 应用侧:PicGo 自己的错误日志(主进程捕获到的异常都会写到这里)
hdc shell "tail -n 40 /data/app/el2/100/base/com.jiujiang.picgo/files/picgo-gui-local.log"

应用数据目录(/data/app/el2/100/base/com.jiujiang.picgo/files/):

文件内容
data.jsonPicGo 配置(图床、设置项)
picgo-gui-local.log主进程错误日志,排查首选
window-state.json主窗口尺寸状态,如 {"mainWindow":{"width":946,"height":668,"isMaximized":false}}
picgo-remote-notice.json远端通知已读状态

③ 窗口层级(“窗口看不见” 类问题必用):

hdc shell hidumper -s WindowManagerService -a '-a'
WindowName  DisplayId Pid     WinId Type Mode Flag ZOrd ...  [ x    y    w    h    ]
settings0   0         21528   89    1    102  16   110  ...  [ 762  293  1596 1467 ]
picgo0      0         24328   90    1    102  0    111  ...  [ 661  354  1797 1269 ]

ZOrd 越大越靠前(实测 111 在前、110 在后,正是"窗口存在但被压在后面")。Flag 当时在前台窗口为 16、后台为 0,[推断] 与聚焦态相关,可作辅助判断。

④ 交互注入(复现"点一下按钮就报错"这类路径,比手点更可重复):

hdc shell uitest uiInput click 2220 395     # 坐标即截图里的物理像素

本文 5.6 的错误就是这样一次点击复现出来的。


5. 报错与解决

5.1 窗口一片空白:Chromium 不绘制

现象:Ability 正常起来,窗口框架可见,但内容区永远不渲染。

定位:Ability 本身起来了(AbilityStage 无异常),但 AppWindowAdapter.bind() 的日志只会等到超时:

bind node handle with node content failed, not find node content for idbrowser1

根因:页面选用了 pages/WindowNode(内部是 WebWindowNode,基于 XComponent)。这条链路不会注册 NodeContent,而 AppWindowAdapter.bind() 取的正是 nodeHandleManager.getNodeContent(id) —— 拿不到就回调 undefined,Chromium 拿不到 ContentSlot,自然没有绘制目标。

解决:改用 pages/NodeHandleWindow(WebNodeHandleWindow + NodeContent),即 2.3 表格里 BrowserAbility.ets 那条改动:直接复用 WebBaseAbility.getContentPath() 的默认分支,不再覆盖。

验证:启动日志出现 NodeHandleImpl Initialize success, support node handle feature,随后窗口正常出图。

5.2 bind() 时序竞争:NodeContent 还没注册

现象:5.1 的页面换掉后,bind() 仍有可能拿不到 NodeContent——首次调用时 ArkUI 页面尚未构建完成,getNodeContent(id) 返回空,回调 undefined,Chromium 依然没有绘制目标。

依据:AppWindowAdapter.bind() 的自带注释 “Ability may still be starting up — poll up to 10s for NodeContent registration”,以及 commit 5fc17e8 中 “Fix AppWindowAdapter.bind() with retry logic (100x100ms) for NodeContent registration timing”。

解决:AppWindowAdapter.ets:794-819 加轮询重试(100 次 × 100ms = 最长 10s):

bind(id: string, callback: (nodeContent: NodeContent | undefined) => void) {
  let nodeContent = this.nodeHandleManager.getNodeContent(id);
  if (nodeContent) { callback(nodeContent); return; }
  let retries = 0;
  const maxRetries = 100;
  const tryBind = () => {
    nodeContent = this.nodeHandleManager.getNodeContent(id);
    if (nodeContent) {
      LogUtil.info(TAG, `bind node handle success after ${retries} retries for id${id}`);
      callback(nodeContent); return;
    }
    if (retries++ < maxRetries) { setTimeout(tryBind, 100); }
    else { callback(undefined); }
  };
  setTimeout(tryBind, 100);
}

注意:DEBUG 包首帧会因此最多晚 ~100ms,属可接受代价;若日志里频繁出现 success after N retries 且 N 偏大,说明启动链路还有别的阻塞。

5.3 每次启动都弹 DevTools

现象:鸿蒙上启动即弹开发者工具窗口。

根因:src/main/utils/env.ts 的 isDev = !app.isPackaged,而 libelectron debug 构建下 app.isPackaged 恒为 false,所以 windowList.ts 里的 if (isDev) openDevTools() 在真机上永远成立。

解决:windowList.ts:120 改为 if (isDev && !isOhos)。需要 DevTools 时走 6.3 的远程调试方式按需打开。

5.4 showAbility() 失败(16000067)

现象:想按 Windows 的习惯"先隐藏创建、启动后再 show()",结果窗口永不出现。

根因:OHOS 上从 JS 上下文调 showAbility() 会返回 16000067(调用方不在前台等状态不满足)。lifeCycle/index.ts:90 的注释记录了这一点。

解决:双管齐下——

  • SETTING_WINDOW.options.show = true(创建即可见);
  • lifeCycle 中 if (!isOhos && isWindowShouldShowOnStartup(...)) 才执行 show()/focus()。

5.5 原生模块 clip-filepaths 加载失败

现象:主进程启动抛 Unsupported OS: ohos, architecture: arm64(clip-filepaths 不认识 process.platform === 'ohos')。

根因:该模块的 .node 只有 macOS/Windows/Linux 版本,没有鸿蒙构建。

解决:scripts/build-ohos.js:81-91 在组装阶段给它的 index.js 打桩,把鸿蒙落到空实现(剪贴板文件路径读取在鸿蒙上本就走别的路径):

case 'openharmony':
case 'ohos':
  nativeBinding = { helloWorld: () => '', writeClipboardFilePaths: () => {},
                    readClipboardFilePaths: () => ({ filePaths: [] }) }
  break

5.6 TypeError: Cannot read properties of null (reading 'show')

现象:点窗口标题栏上的迷你窗口按钮,应用报错(界面不崩,但功能没反应)。

真机日志(picgo-gui-local.log,本文实测复现于 2026-09-13 11:40:27;历史记录在 08-14 一天内重复 4 次):

2026-09-13 11:40:27 [PicGo ERROR]
------Error Stack Begin------
TypeError: Cannot read properties of null (reading 'show')
    at IpcMainImpl.<anonymous> (.../resources/app/dist_electron/main/index.js:17733:18)
    at IpcMainImpl.emit (node:events:518:28)
    at WebContents.<anonymous> (node:electron/js2c/browser_init:2:86701)
    at WebContents.emit (node:events:518:28)
-------Error Stack End-------

定位:

  • 报错行 index.js:17733 对应源码 src/main/events/ipcList.ts:143 的 miniWindow.show();
  • 触发方是渲染进程的标题栏按钮:src/renderer/components/common/title-bar.tsx:104-113(aria-label="Open mini window" → windowControlsAdapter.openMiniWindow());
  • 根因:鸿蒙上 MINI_WINDOW 不创建(windowList.ts:161 的 isValid=false),windowManager.get(IWindowList.MINI_WINDOW) 返回 null,而 handler 直接 !.show()。

解决(二选一,建议都做):

  1. ipcList.ts:135-146 加防御:if (!windowManager.has(IWindowList.MINI_WINDOW)) return;
  2. 标题栏在 isOhos 下不渲染迷你窗口按钮(鸿蒙没有悬浮窗能力,按钮本身就是死功能)。

复现命令:

hdc shell uitest uiInput click 2220 395     # 标题栏第 2 个按钮(最小化右侧)
hdc shell "tail -n 14 /data/app/el2/100/base/com.jiujiang.picgo/files/picgo-gui-local.log"

5.7 ReferenceError: WebAssembly is not defined

现象:该日志里 2026-08-10、08-11、08-14、09-13 的每一次启动都记了一条同样的错误。

2026-09-13 11:38:40 [PicGo ERROR]
------Error Stack Begin------
ReferenceError: WebAssembly is not defined
    at lazyllhttp (node:internal/deps/undici/undici:5771:9)
    at lib/dispatcher/client-h1.js (node:internal/deps/undici/undici:5817:25)
    at __require (node:internal/deps/undici/undici:6:50)
    ...
    at lib/global.js (node:internal/deps/undici/undici:8151:17)
-------Error Stack End-------

根因:Node 内置的 undici 用 llhttp 的 WASM 版本做 HTTP/1.1 解析;libelectron 的 V8 未开启 WebAssembly,于是 require('undici') 在模块初始化(lib/global.js 创建全局 dispatcher)时直接抛错。

影响范围:PicGo 主进程捕获了该异常,所以不崩。实测:

  • 界面渲染与页面切换全部正常:上传页 / 图床设置 / PicGo Cloud / 设置·通用 / 插件页(见图 1–5);
  • 网络可用:启动即弹出「发现新版本 3.0.2」对话框,而版本查询走的是 axios(src/main/utils/getLatestVersion.ts,经 http/https 适配器),说明 axios 这条链路正常。

结论与未验证项:这不是"自动回退到非 WASM 路径",而是 Node 全局 fetch()(undici 承载)在鸿蒙上不可用——后续若有插件或代码直接 fetch(),在鸿蒙上会失败,应改用 axios/http。本文也未做真实图片上传验证,故不对"上传是否可用"下结论。

处置建议:日志降噪(把该 ReferenceError 归为已知平台差异),不要试图在应用层 polyfill WebAssembly。

5.8 启动后窗口被其它应用挡住

现象:hdc shell aa start 返回成功,进程也在,但屏幕上找不到 PicGo。

真机实测:force-stop 后重新 aa start,hidumper 显示 PicGo 窗口已创建,只是 ZOrd 比当时聚焦的系统「设置」窗口低一档(110 vs 111);再执行一次 aa start 后才被置前。该时段多张截图中 PicGo 窗口区域均未绘制出应用界面。

解决:应用侧无法规避"启动时不抢焦点"这一系统行为(这本身也是体验上的正确选择),建议:

  • 用户侧:任务栏点一次图标即可置前(FAQ H2 已给出三种操作路径);
  • 开发侧:排查"窗口不见了"时,先用 hidumper 的 ZOrd/Flag 确认是"没创建"还是"没置前",两者解法完全不同。

6. 注意事项

6.1 鸿蒙上的能力差异(不要做无用功)

能力状态依据
系统托盘 / 菜单栏图标无system/index.ts:271 直接跳过 tray 创建
迷你悬浮窗无windowList.ts:161 isValid=false(并引出 5.6 的崩溃)
全局快捷键(如 Ctrl+Shift+U)无依赖 OS 级热键注册 API,libelectron 未提供
右键菜单上传无依赖 Shell 扩展钩子
关闭主窗口 = 退出应用生效windowList.ts:146-150 的 if (isLinux) app.quit(),而 isLinux 在鸿蒙下为 true(common.ts:148)——与 Windows/Linux 一致,不是 macOS 的"关窗驻留"心智

6.2 「启动模式」设置项在鸿蒙是空转的

真机 data.json 里没有 settings.startupMode 这个键:

{ "settings": { "server": {...}, "shortKey": {...}, "showUpdateTip": true,
                "language": "zh-CN", "autoStart": true, "pasteStyle": "Custom" } }

而渲染进程的默认设置里 startupMode 是 HIDE(src/renderer/components/main/settings/utils.ts:342),所以设置页会显示「静默启动」——但实际行为是"每次都直开主窗口",因为:

  • windowList.ts:43 只在配置缺失时兜底为 SHOW_MAIN_WINDOW;
  • windowList.ts:114 把 OHOS 的 show 硬编码为 true;
  • lifeCycle/index.ts:89 在 OHOS 上完全跳过 show()/focus() 分支。

结论:设置页展示值与鸿蒙实际行为不一致,建议在 UI 上对 isOhos 置灰该选项并提示"鸿蒙端固定显示主窗口"。

6.3 命令行参数与调试通道

  • 默认参数由 web_engine/src/main/ets/common/CommandLineAdapter.ets:146-162 的 buildDefaultArgs() 给出:--ozone-platform=ohos、--user-data-dir=/data/storage/el2/base/files/、--no-zygote 等;版本检查日志里能看到逐条 Appended argument:。
  • UNSUPPORTED_ARGS 名单(同文件 10-19)会打 warning:--disable-renderer-backgrounding、--host-rules、--force_high_performance_gpu、--experimental-network-inspection、--diagnostic-dir 等。
  • 尤其注意 --disable-renderer-backgrounding 无效:华为文档明确说明,鸿蒙对后台进程有更严格的资源限制,“Electron 在后台时自动进入能效模式,无论是否启用该参数”。[推断] 相应地,窗口 webPreferences.backgroundThrottling: false 在鸿蒙上也不能指望绕过节流——有定时刷新/长连接逻辑时,按"后台可能被节流"设计。
  • 远程调试(官方路径):端口参数 + hdc fport tcp:N tcp:N,再在 Chrome 的 chrome://inspect/#devices 里加端口。

6.4 插件与原生 addon

  • 纯 JS 插件可用;含 .node 的插件(含所有依赖原生模块的插件)默认不可用。
  • 要支持,得按华为 electron-napi-library 的方式重新编译:用 include/electron_node/src/node_api.h + lib/libshim.a,编译时定义 HIDE_NAPI_AND_UV,并以 NODE_MODULE_VERSION=116 对齐 ABI;编译目标是 aarch64-linux-ohos。
  • 现阶段对用户的建议:装插件失败时先看它有没有 .node。

6.5 签名与仓库卫生

build-profile.json5 里的 signingConfigs.material 会包含明文 keyPassword / storePassword 和本机绝对路径(/Users/xxx/.ohos/config/*.cer|.p7b|.p12)。这类内容不要提交:改为本地文件覆盖、或纳入忽略规则;调试证书也不要复用他人机器上生成的 profile。本文写作时,仓库工作区的 build-profile.json5 正处于未提交修改状态(新增了 signingConfigs 与 targetSdkVersion),提交前请先按此处理。

6.6 排查顺序(省时间的经验)

  1. 进程在不在:ps -ef | grep picgo(主进程 + Renderer + GPU 四件套);
  2. 窗口在不在、在不在前面:hidumper -s WindowManagerService -a '-a' 看 ZOrd/Flag;
  3. 系统日志:hilog -x 过滤 Adapter|chromium|NodeHandle(可先 hilog -r 清缓冲抓冷启动);
  4. 应用日志:files/picgo-gui-local.log(主进程捕获到的异常及其完整栈);
  5. 都没线索再上截图与坐标点击注入复现交互路径。

7. 参考

  • 仓库内华为侧资料:libelectron/ohos_hap/docs/
    • electron调用ets指导文档0411.pdf(adapter / addon / HAR / HAP 全链路)
    • Electron框架命令行参数支持说明.zip(各启动参数在鸿蒙的支持情况,含 --remote-debugging-port、hdc fport 调试说明)
    • 鸿蒙平台Electron加载addon(基于node-sqlite3-5.1.7).pdf、electron-napi-library.zip(原生模块编译)
    • callArkTSFunction接口文档与示例.zip(ETS ↔ JS 互调)
  • 仓库文档:README_zh-CN.md(鸿蒙安装/构建)、FAQ.md(HarmonyOS FAQ H1–H8)
  • 代码基线:commit 5fc17e8

附:本文证据来源

结论来源
图 1–5、进程形态、hilog 片段、hidumper 窗口表真机实测(2026-09-13)
picgo-gui-local.log 中的 WebAssembly / miniWindow null 报错真机日志,含 08-10 ~ 09-13 多次记录
5.6 的完整复现链路真机点击注入 + 源码对照
各代码分支与行号仓库源码
6.3 参数支持矩阵华为《命令行参数支持说明》
2.7 构建时间线、2.8 告警统计与行号漂移.hvigor/report/*.json(10 份)与 .hvigor/outputs/build-logs/build.log*(按 BUILD SUCCESSFUL/FAILED 分段统计,去 ANSI 转义后正则计数)
2.1 demo 遗留项(bundleName、app_name、演示页)web_engine/electron 模块源码 + hdc shell bm dump -n com.jiujiang.picgo
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐