鸿蒙上没有官方 Electron。

所以「把一款 Electron 应用搬到鸿蒙」,标准答案基本只有一个:用 ArkUI 把界面重写一遍。壳是新写的,逻辑重新接一遍,界面照着截图复刻——用户看着差不多,代价是维护者从此背上一个永久的 fork。

我选了另一条路:把 Electron 37 整个塞进 HAP 里。

于是跑在鸿蒙窗口里的,是官方 DeepSeek Harness 的 Web UI,逐字节复用、界面零重写。

这篇是 DSH Desktop 鸿蒙版的完整介绍:它是什么、这条路的取舍在哪、现在能干什么和干不了什么、架构上到底只改了三处、以及你怎么把它装到自己的机器上。

先把最容易含糊的一点说清:这是一个真正的 Electron 应用。 不是浏览器套壳,也不是 ArkUI 把界面重画一遍再假装是同一个东西。

一、它是什么

背景一句话:DeepSeek Harness(dsh)是开源的 AI agent 运行框架,采用「一切皆插件」的架构——模型适配器、工具注册表、会话日志、甚至 agent 主循环本身,全是可替换的插件。它自带一个相当完整的 Web UI。

DSH Desktop 要做的,是把这个 Web UI 变成一等公民的应用形态。它有三个 edition,共用同一套外壳架构、同一条数据面:

Edition运行时平台
dsh-desktopElectron 官方运行时Windows / Linux / macOS
dsh-desktop-hos(本篇)Electron 37 on HarmonyOS鸿蒙 2in1 / 平板
dsh-desktop-ark系统 ArkWeb + 进程内 Node鸿蒙 2in1 / 平板(可延伸到手机)

三个 edition 共用同一个 Host 和同一条数据面,差别只在由哪个运行时提供 Node 与渲染器——runProfile('desktop') 还是那一行调用。

现状先交代清楚(这条别跳过):版本 0.2.1(versionCode 1009),已构建并签名。核心链路的真机验证是在 0.2.0 上完成的——对话、Agent 循环、工具调用、会话持久化、插件市场、窗口状态,都在鸿蒙 6.1.0.135(API 24)的 2in1 真机上跑过。0.2.1 目前只验证了构建与签名,真机复验还没做。

为什么非要「零改动」?因为 dsh 还在 developer preview,上游动得很勤。一个外壳钉一个上游 tag,patches/<dsh-tag>/ 跟着迁移——如果我把界面重写一遍,等于把上游最活跃的那部分永久 fork 进自己仓库,从此每次升级都是重做。这是一个很具体的工程判断,不是情怀。

图 1|鸿蒙 2in1 真机实拍:Agent 对话界面(浅色 / 深色双主题)

二、这条路贵在哪

把 DSH 搬上鸿蒙不止一条路,取舍本身就是这个工程的全部内容:

Electron 路线(本工程)原生宿主路线
界面官方 Web UI 逐字节复用,零重写同一份 Web 内容外套原生宿主,平台外壳需重写
与桌面端一致性同一渲染器、同一套 Node API、同一条数据面观感好,但宿主层是重实现,必须持续跟上游
运行时权限需要 ACL 受限权限(JIT 编译)无 JIT,不需要该权限
设备覆盖鸿蒙 2in1 / 平板可延伸到手机

这张表里最扎眼的是第三行:Electron 要跑,就得有 JIT;要有 JIT,就得申请 ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY 这个 ACL 受限权限——system_basic 级、system_grant,官方明确只对平板 / 2in1 开放,只给「启用了自带引擎 JIT 编译」的应用,并且不允许用于热更新。

这也是它跑不了手机的直接原因。

两条路线不存在谁更好,只是优化目标不同:一条买「与桌面端语义完全一致」,另一条买「设备覆盖更广」。本工程存在的意义,是把 Electron 路线端到端跑通——包括它撞到的每一处沙箱冲突。

图 2|两条路线对比:官方 UI 复用 vs 原生宿主重写

三、现在能用的,和明确不能用的

先给能用的(都是真机验证过的):

能力状态
dsh Web UI 在窗口内运行(100% 复用 dsh 前端)✅
会话持久化 + 全文检索(SQLite,aarch64 ABI 138)✅
插件市场(内置 dshmarket)✅
窗口状态记忆(最大化 / 位置尺寸)+ F11 全屏✅
系统托盘:状态栏图标 + 右键菜单✅
命令执行:常驻 /system/bin/sh,哨兵行协议驱动✅ 但围栏是 best-effort
图片附件:容器头解析 + ArkTS 桥交给平台图像框架⚠️ 有边界
随包自带 Node / pnpm / Python 运行时⛔ 不可实现
进程沙箱(landlock 等)❌ 不在范围内

「不可实现」不是没做,是做不了。 这几条得说清楚,否则容易变成错误的期待:

命令执行。 上游的 tool-bash 行保持 disabled——node-pty 没有 aarch64 产物,PTY 也被 SELinux 拒绝。替代方案是常驻一个 /system/bin/sh,用哨兵行协议读输出,路径围栏加在工具层。注意:这个围栏设计上就是可被绕过的,它不是沙箱。

随包自带运行时:不可实现。 分发 ELF 需要二进制证书,而华为不授予个人开发者。好消息是应用能在系统 PATH 上看到结构校验通过的 node,只缺 pnpm——用户在系统终端里装好 pnpm 之后,插件市场就能装插件了。但这条属于「可选依赖」,绝不作产品能力宣传:默认状态就是没有。

图片。 sharp 换成纯 JS 容器头解析(PNG / JPEG / GIF / WebP),做不了的转换(解码、EXIF 方向、缩放、重编码)交给 ArkTS 桥走平台图像框架。干净的 8-bit sRGB 文件按字节原样通过、零转换成本;取不到桥时每次转换都如实拒绝,不假装成功。没有缩略图。

另外两项是二期评估后主动放弃的:无边框窗口(把窗口控制搬进渲染层,代价与收益不成比例)、开机自启(系统只提供只读查询)。不是漏做,是算完账不做。

四、架构:只改了三处

整个工程的骨架,一张图能画完:

┌─ 鸿蒙 HAP ──────────────────────────────────────────┐
│  electron 模块(entry):EntryAbility 启动运行时      │
│  web_engine 模块(HAR):ArkTS 桥接层 + 承载 dsh 产物 │
│       ┌─ Electron 主进程(Node.js,承载 dsh Host)──┐│
│       │  解压 dsh-dist.tar.gz → runProfile('desktop')││
│       │    ├─ webserver ← 0.0.0.0:<空闲端口>        ││
│       │    ├─ apiProxy  ← RPC 网关                   ││
│       │    └─ connection ← /api + WebSocket 注册     ││
│       │  就绪后 loadURL(http://<局域网 IP>:<port>/) ││
│       └──────────────────▲──────────────────────────┘│
│                          │ 同源 + Host 头改写         │
│       ┌──────────────────┴──────────────────────────┐│
│       │ 渲染进程:标准 dsh Web UI(fetch + WS 事件) ││
│       └─────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────┘

让这张图成立的是三处鸿蒙适配,每一处都对应一个写清了根因的 patch:

1. loopback 网络隔离。 鸿蒙下渲染进程访问不了 127.0.0.1,所以 webserver 绑 0.0.0.0、渲染进程走局域网 IP 建连;同时在内嵌渲染进程请求出栈前,把 Host / Origin 改写回 127.0.0.1:<端口>,让 dsh 的 loopback-only 特权方法围栏照旧通过。

这里有一条必须写明的安全边界:监听绑定的是 wildcard 地址,而 /api 会把 bind 字面量当作合法 Host。所以局域网内的非浏览器客户端,只要发 Host: 0.0.0.0:<端口> 且不带 Origin,就不会被 Host 围栏拦下。结论:请把它当单用户本地应用对待,不要放在不可信网络里。

2. 禁 symlink、禁 hardlink。 沙箱两者都拒(EACCES / EPERM),所以 profile 发布回退 cpSync,原子写改成同目录 rename 发布(目标已存在就先做守卫校验)。这一处看着小,实际波及会话日志首次落盘、代际发布、插件目录镜像——凡是「用链接省事」的地方,全部得重写。

3. os.homedir() 指向沙箱外。 启动前把 HOME 指向沙箱内可写的 userData 目录。

剩下的都不需要改:dsh 的 webserver 本来就在同一端口同时服务 SPA 页面和 /api,所以数据面不需要 CORS、不需要鉴权、不需要自定义协议、不需要新的 IPC 载体。这是整个设计最值钱的一条事实,也是「上游代码保持原样」能成立的原因。

五、构建:四阶段流水线 + 十三个补丁

dsh 依赖的一些 Node 能力(原生目录对话框、flock、硬链接原子写)和鸿蒙沙箱直接冲突,所以构建前要先打一批 patch。当前是十三个,全部幂等(--reverse --check 检测到已应用就跳过),按 dsh tag 分目录存放。

流水线共四阶段,之后才是本机构建 + 签名:

# ⓪ 同步运行时:copy 上游 Electron 运行时(模块 + 3 个 SO),
#    prune 掉不需要的上游文件,再 overlay 回盖本工程定制
node scripts/collect-runtime.mjs

# ① 构建 dsh:清理残留 → apply patch → build host/client/web → 构建插件市场
node scripts/build-dsh.mjs

# ② 收集产物:依赖物化 → 补包 → sharp stub → SQLite 注入 → web dist + profile + 插件
node scripts/collect-dsh.mjs

# ③ 打一个 ustar 格式的 dsh-dist.tar.gz(约 143MB,运行时流式解压)
tar -czf web_engine/src/main/resources/resfile/resources/app/dsh-dist.tar.gz --format=ustar -C . dsh-dist

# ④ 构建 + 签名
powershell -ExecutionPolicy Bypass -File scripts\build-debug.ps1

十三个补丁里,最能说明鸿蒙沙箱「不讲情面」的是这几个:

Patch它解决了什么
allow-all-interfaces移除 webserver 拒绝 0.0.0.0 的检查(loopback 隔离要绑全网卡 + 局域网 IP)
hardlink-to-rename沙箱禁硬链接 → 改为同目录 rename 独占发布,其他平台仍保持 link 优先
fs-chmod-primitive给文件系统 seam 补 chmod 原语,且在存储层「接受调用但不落实模式」时报错——鸿蒙的用户目录挂载正是如此
fs-remove-primitive补上 remove 原语,让删除走和读写在同一道沙箱围栏上
extra-writable-roots部署期一次性授予 Desktop / Documents / Download 可写根,免得用户每次写入都过一次提权审批
attachment-durable-walk-sandbox附件保存会逐级 fsync 到文件系统根,而沙箱拒绝打开 /、/data——改成在平台第一次拒绝的那一层就结束参与

第 ⓪ 阶段还有一个容易把自己埋掉的设计:它整目录 cpSync 覆盖上游运行时,所以任何没被后续阶段重新施加的定制都会被静默回退。工程用两个机制保护自己——prune(删掉本应用不用的上游文件,缺一个就硬失败,绝不静默跳过)和 overlay(把定制文件按相同相对路径回盖回来),再加构建期守卫逐条断言。

这条纪律值得抄走:凡是「整目录同步」的构建步骤,都要配一个能硬失败的守卫。

体积上不必担心:dsh-dist.tar.gz 约 143MB,构建出的 HAP 约 360MB、App Pack 约 245MB,离 AppGallery 4GB 的上限很远。

图 3|四阶段构建流水线:collect-runtime → build-dsh → collect-dsh → tar

六、签名:密钥永不入库,但分发现实很硬

签名材料全部外置:两个 gitignored 的本地配置(debug / release 各一份,只把 .sample 入库),build-profile.json5 因此保持无密钥、可安全入库,签名配置只在内存里注入、永不写盘。密码字段要求是 hvigor 的 AES-GCM 密文,而不是明文。

模式由环境变量 SIGN_MODE 选择,未设置默认 debug,其他任何取值都硬失败——绝不静默回退。而且构建成功后会做一次断言:用 SDK 工具解出产物内嵌 profile 的 type,和请求的签名模式比对,不一致就醒目报错并非零退出。

为什么要做这个断言?因为这个工程真的出现过「release 构建被静默用 debug 材料签名」。守卫不是为了好看,是为了记住教训。

然后是分发现实,这块不好看,但不能糊:

  • debug 签名的 HAP 绑定签名账号和设备。 别人的 debug 包装不到你的机器上。
  • release 签名的包只能经 AppGallery 分发。 直接 hdc app install 会失败(code:9568322 ... not trusted app source)。要做真机回归,请用 -BuildMode release -SignMode debug(release 编译 + debug 签名)。
  • AppGallery 这条路卡在两道门:ACL 受限权限审批(约 3 个工作日)与 AI 合规三件套(算法备案 / 大模型登记 / 安全评估)。而这两道门后面还有一道我已经交代过结论的墙——个人开发者身份。我在《个人开发者上架鸿蒙 AI 应用,最后倒在安全评估实名制这一格》里写完了那段:实名制要求手机号验证,而所有能合规拿到手机号的通道都要求企业资质。所以上架这件事目前搁置。

也就是说,今天的获取方式只有两种:自己构建,或者用自己的华为账号重签一个 debug 包。

七、真机验证:怎么证明它真的跑起来了

真机验证不是「看一眼能不能打开」。0.2.0 上跑过的是完整链路:对话、Agent 循环、工具调用、会话持久化、插件市场、窗口状态。

调试手段里最强的一个,是主进程默认带 --inspect:

export MSYS_NO_PATHCONV=1        # 否则 Git Bash 会把 /data/... 改写成 Windows 路径
hdc fport tcp:19229 tcp:9229     # 本地端口 → 设备 9229
curl -s http://127.0.0.1:19229/json/list   # 拿到 webSocketDebuggerUrl

那个上下文里 require 可用、能拿到 electron,所以既能查 dsh 宿主状态,也能驱动渲染进程——用 Node 自带的 WebSocket 走 CDP Runtime.evaluate,几行脚本就能问出「当前有哪些窗口、各自在什么 URL」。

一个具体的小坑:输入框是 contenteditable 富文本编辑器,且受 React 受控。直接赋 value / innerText 完全不会被识别,必须走 CDP 的 Input.insertText。

还有一个更常见的误解:外部浏览器(Playwright 之类)驱动不了它。 宿主的每个 RPC 方法都需要一个浏览器会话——GET / 只接受 ?token=<launchToken> 形式的启动令牌,校验后下发与 authority 绑定的签名 cookie;没有 cookie,请求会在 RPC 分发之前就返回 401。而启动令牌从不落日志(web-runtime 以 printUrl: false 运行),交换后又会重定向到干净的 /。所以哪怕把端口映射出来,也完不成鉴权。这不是缺陷,是鉴权围栏按设计工作。

系统的 uitest 能 dump 布局、能点击 Web 内容,但 uiInput text / keyEvent 触达不到——编辑器不是原生控件。文本输入还是得回到 Input.insertText。

⚠️ 最后一条安全提示得留着:--inspect 意味着任何拿到 hdc 的人都能完全控制宿主进程(那个上下文里 require 可用)。这来自上游运行时的默认参数,不是本工程引入的。本地调试可接受,但哪天要上架,应该评估在 release 构建里移除它。

图 4|真机调试通路:主进程 inspector + 端口转发 + CDP 求值

八、怎么装到自己的机器上

三步,没有第四步:

1. 准备同级目录的几个工程。 本工程消费三个同级工程(非 submodule):dsh、插件市场、Electron-on-HarmonyOS 运行时。前两个是公开仓库,第三个需要解压 Electron 37 的编译产物补齐三个 SO——缺任何一个,⓪ 阶段直接报错。

2. 跑构建。 按 §五 的四阶段走完,再执行 build-debug.ps1。环境要求:DevEco Studio 4.0+、HarmonyOS SDK API 17+(targetSdk 6.1.1(24))、Node 18+、pnpm@11、HDC。

3. 用你自己的账号重签。 默认的调试 profile 不授予那个 ACL 受限权限,直接装会报 grant request permissions failed。在 DevEco Studio 里 File → Project Structure → Signing Configs、勾上自动生成签名、用华为开发者账号登录,让重新生成的 .p7b 在 acls.allowed-acls 里带上它,然后把本地签名配置指向新生成的签名材料,重构建即可。

装的时候记得先卸载旧的——设备上残留的 dsh-dist 会让新包跑起老代码:

hdc uninstall <你的 bundleName>
hdc app install -r <签名后的 HAP 路径>
hdc shell aa start -a EntryAbility -b <你的 bundleName>

本项目刻意不使用 HNP:声明 hnpPackages 会要求每个 HAP 和 App Pack 都携带配套的 .hnp 原生包,而 hvigor 产不出它,两个产物就都得走「事后注入 + 重签」。现在的构建不需要任何注入步骤——hvigor 自己签出的产物就是交付物。

九、另一个版本:dsh-desktop-ark

如果你更在意「能不能跑到手机上」,这个工程不是终点。

dsh-desktop-ark(HarmonyOS Edition) 是完全不含 Electron 的另一个 edition:系统 ArkWeb 渲染 + 进程内 Node v24.2.0。不打包任何浏览器内核,也不把界面用 ArkUI 重写——同一个 dsh Host、同一套外壳架构、同一条数据面。

但两者的差异不只是「换个运行时」,而是整条代价链的重新分配:

Electron on HarmonyOS(本篇)HarmonyOS Edition(ark)
渲染器Chromium,随 Electron 37 一起打包系统 ArkWeb,不打包内核
NodeElectron 自带(22.17)进程内 libnode.so(24.2.0)
上游 dsh 补丁13 条0 条(改用 profile + 垫片层)
webserver 绑定0.0.0.0 + 局域网 IP + Host/Origin 改写127.0.0.1 同源回环
ACL 权限需要(JIT)不需要
载荷HAP 约 360MB未签名 HAP 86MB(载荷 40.4MB)
成熟度0.2.1,真机验证过0.1.0 scaffold,编排与载荷启动已验证,真机未验证

这份对照表诱人得有点离谱:没有 JIT 需求,就不受「仅 2in1 / 平板」的钳制,设备覆盖才有机会延伸到手机;补丁从 13 条变成 0 条;包体小一个数量级。

代价也很清楚:平台差异从「打补丁改上游」搬到了「自己维护一层经过审计的垫片」——自由度更高,责任也全部转回自己身上。而它现在还是 scaffold:已验证的是载荷物化(628.6 MiB 运行树 → 40.4MB 载荷)、Host 在进程内启动、token URL 上返回零改动 Web UI(HTTP 200);鸿蒙真机上还没跑过。

所以这两版不是替代关系,是两种成本结构。下一篇就讲 ark 这一版:去掉 Electron 之后,同一个 dsh 是怎么在 ArkWeb + 进程内 Node 上活下来的。如果你正纠结选哪条,那篇会比这篇更有用。

写在最后

DSH Desktop 鸿蒙版的核心,其实只有一句话:抓住「dsh 的 webserver 本来就同源服务页面和 API」这个事实,然后老老实实撞完鸿蒙沙箱的每一处冲突。

它换来的东西也很具体:上游 Web UI 一个字节都没改,三个 edition 共用一套外壳架构和一条数据面;代价是十三个补丁、一个 ACL 受限权限、三项做不到的事(随包运行时、PTY、进程沙箱),以及一个不好看但真实的分发现实。

如果你想自己动手,或者只是想看看 Electron 到底是怎么被塞进 HAP 的,代码和文档都在这里:

  • 鸿蒙工程(本篇):https://github.com/fellow99/dsh-desktop-hos
  • 鸿蒙 Edition(ArkWeb,预告):https://github.com/fellow99/dsh-desktop-ark
  • 桌面工程(Windows / Linux / macOS):https://github.com/fellow99/dsh-desktop
  • 工程总览:https://github.com/fellow99/deepseek-harness-workspace
  • 上游 DeepSeek Harness:https://github.com/deepseek-ai/deepseek-harness
  • 插件市场:https://github.com/dsh-market/dsh-market
  • 鸿蒙 Electron 运行时:https://atomgit.com/jianguoxu/harmonypc-electron

如果这篇让你对「鸿蒙上跑 Electron」少了一点怀疑,点个「在看」或转给同样在折腾鸿蒙应用化的朋友,就是对我最大的支持。后面我会继续把真机调试、ACL 申请、沙箱适配这些脏活写细。

下一篇预告:dsh-desktop-ark —— 去掉 Electron 之后,同一套 dsh 在 ArkWeb + 进程内 Node 上跑起来的样子。

感谢各位关注,欢迎访问我的GitHub主页:https://fellow99.github.io/

Logo

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

更多推荐