DSH Desktop - HarmonyOS Edition

0 阅读10分钟

把一款 AI agent 应用搬上鸿蒙,通常只有两条老路:要么用 ArkUI 把界面重写一遍,从此背上一个永久 fork;要么把整个 Electron 塞进 HAP 里,为 JIT 去申请一个受限权限。

DSH Desktop 的 HarmonyOS Edition 选了第三条路:让系统 ArkWeb 来渲染官方界面,让进程内的 Node 来承载智能体宿主。 界面零重写、上游零补丁、安装不需要任何受限权限。


一、它是什么

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

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

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

三个 edition 的差别,只在由哪个运行时提供 Node 与渲染器。本篇讲的 dsh-desktop-ark,就是完全不含 Electron 的那一个:系统 ArkWeb 渲染 + 进程内 Node v24.2.0。

先交代现状:版本 0.2.1(versionCode 201),已在 HarmonyOS 真机上验证。零受限权限装机、载荷自解包并 ready、垫片全部安装、node:sqlite / FTS5 全文检索可用、文本与图片对话、三根目录工作区选择、git 克隆、插件市场安装/卸载,这些都在真机上跑过。

为什么执着于「零改动」?因为 dsh 还在 developer preview,上游动得很勤。一个外壳钉一个上游 tag,升级就是重跑产物管线,而不是重做补丁。 一旦把界面重写一遍,就等于把上游最活跃的部分永久 fork 进自己的仓库。这是很具体的工程判断,不是情怀。


二、鸿蒙上的两个版本

鸿蒙上有两个独立版本,壳架构、未改动的 Web UI、数据面完全相同,差别只在由谁提供 Node:

HarmonyOS Edition(本篇)Electron on HarmonyOS 版
渲染系统 ArkWeb,不随包带内核Electron 37 内置 Chromium
Node应用进程内 libnode.so(v24.2.0)Electron 运行时内置(Node 22.17)
上游 dsh 补丁0 条(profile + bundle + 已审计垫片)13 个源码补丁
安装要求无(默认无 JIT)需 JIT 受限权限(ACL)
包体约 88 MBElectron 体量(数百 MB)

这张表最扎眼的是后两行:补丁从 13 条降到 0 条,包体小了一个数量级,而且不需要任何受限权限。

两者不是替代关系,是两种成本结构。Electron 版用自带 Chromium 与 JIT,换来最接近桌面的保真度;本版用较小的包体与免权限安装,换来「平台差异由一层垫片兜住」的灵活度。


三、三条核心机制

整个方案能成立,靠的是三条机制:

1. 同源数据面。 Node 在进程内启动 dsh Host,把 webserver 绑到 127.0.0.1 的随机端口,拿到带一次性 token 的入口 URL,ArkWeb 直接 loadUrl() 同源加载。token 换签名 cookie,之后页面走 /api + WebSocket。零 CORS、零 Host 头改写、零自定义协议。

2. 适配层放在上游扩展点里。 新增一个 ohos profile + 自有 bundle,把 dsh 的能力 seam(命令执行、shell、沙箱、目录选择、文件系统等)换成鸿蒙实现。上游零源码补丁是设计目标,平台性缺陷走「带标记的垫片」。

3. 宿主能力走同进程桥。 ArkTS 与 Node 之间用文件信箱桥(真机实测往返约 2ms);不向网页注入任何 preload,桌面能力全部由 Host 侧插件经 RPC 提供。

这三条里,第一条是整个设计最值钱的事实:dsh 的 webserver 本来就同源服务页面和 API,所以数据面不需要 CORS、不需要鉴权、不需要自定义协议,也不需要新的 IPC 载体。这也是「上游代码保持原样」能成立的根本原因。


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

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

能力状态
dsh Web UI 在窗口内运行(100% 复用 dsh 前端)✅
会话持久化 + 全文检索(node:sqlite + FTS5)✅
文件读写 / 编辑、图片附件、文本与图片对话✅
三根目录工作区选择(沙箱 / 文档 / 下载)+ 授权门✅
Git(21 个工具,纯 JS,设备无需 git 二进制)✅
插件市场(内置 dshmarket,安装 / 卸载闭环)✅
零受限权限装机(默认无 JIT 形态)✅

再诚实地交代当前边界:

能力状态
命令执行(bash 工具)⚠️ 执行器已交付,路径解析修复待落地
内容搜索(grep / glob)⚠️ 需要纯 JS 后端(arm64 上没有 ripgrep 二进制)
删除 / 移动 / 复制 / chmod⚠️ 上游 ctx.fs 尚无这些原语
托盘图标、窗口状态记忆— 本版不提供
PTY 真终端、进程沙箱❌ 平台无 /dev/ptmx、无原生 launcher

「不能用的」不是没做,而是做不了或按产品算账决定不做。比如 PTY:鸿蒙没有 /dev/ptmx,所以没有交互式终端面板,但命令执行本身仍可通过常驻 sh + 哨兵行协议来做。这些边界必须说清,否则容易变成错误的期待。


五、零 ACL:免受限权限的关键

这是本版和 Electron 版最本质的分野。

Electron 要跑,就得有 JIT;要有 JIT,就得申请 ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY 这个受限权限——它只对平板 / 2in1 开放,而且审核、上架都卡这一道门。

本版的答案是:默认跑无 JIT 的 compat 形态,用 --jitless 关闭 V8 的可执行内存分配,从而声明面收敛为零 ACL。 代价是 V8 退化为纯解释器执行,而且 WebAssembly 直接不存在(不是变慢,是 typeof WebAssembly === 'undefined')。

这带来一个致命问题:Node 内置的 fetch 依赖 WASM 实现的 HTTP 解析器,无 JIT 下「UI 能打开,一说话就失败」——因为模型调用走的就是全局 fetch。

于是本工程补了一个纯 JS 的 fetch 兼容层:基于原生 node:http/https + node:stream/web 的 ReadableStream,覆盖 SSE / FormData / 重定向 / AbortSignal,零第三方依赖。同时封印 node:http 的惰性 getter,阻断 undici 的初始化。这个兼容层只在 WebAssembly 不存在时才安装,WASM 可用时一个动作都不做。

所以本版是双形态:

形态JITACL说明
compat(默认)无(--jitless)零 ACL解释器执行,纯 JS fetch 兼容层
jit有需 1 项 ACL完整 V8 + WASM,性能最好

两种形态由编译期常量切换,运行期不做探测。这是踩坑得来的结论:权限位不是 JIT 可用性的预检——一旦误判,进程内 Node 会 SIGTRAP 崩溃,而它每进程只能启动一次,无法重启。


六、插件市场与进程内 pnpm

鸿蒙沙箱有个很「不讲情面」的约束:应用域 fork 出来的子进程会被 seccomp 直接杀掉,沙箱又禁止 symlink / hardlink。 这让「装插件」这件在桌面上稀松平常的事,在鸿蒙上变得很麻烦。

本版的解法是:构建期把纯 JS 的 pnpm 引擎物化进载荷;运行期在同一个进程的 worker 线程里跑 pnpm。 不 spawn 子进程、不建符号链接、不改市场与上游源码。

配置上刻意选择了 hoisted + copy 布局来绕开链接限制,ignoreScripts 关闭插件作者代码的执行(既是平台约束,也是安全属性)。于是设备上没有 pnpm 二进制,也能在插件市场里安装 / 卸载 npm 插件——这条链路已在真机闭环。

一个诚实的代价:因为 node::Start 每进程只能一次、ArkWeb 的客户端模块图固定,插件装卸是「重启生效」。装卸即持久化、重启即生效,这是真机上唯一稳定的路径。


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

真机验证不是「看一眼能不能打开」。跑过的是完整链路:装机、载荷自解包、Host 就绪、文本与图片对话、三根目录工作区授权、git 克隆、插件市场安装/卸载。

设备是华为 MateBook Pro 2in1(HarmonyOS 6.x / 7.x,API 24–26)。几个值得记一笔的事实:

  • 净启耗时:装机后首启 60–75 秒(解包 1.5 万个文件约 13 秒);命中幂等标记后冷启约 3 秒到 ready。
  • 包体:HAP 约 88 MB(含约 42 MB 载荷归档),离 AppGallery 的上限很远。
  • node:sqlite + FTS5 在真机直接可用,全文搜索不需要另编原生插件。

调试过程中还踩掉了一批「鸿蒙特有」的坑,最典型的一个:ArkWeb 输入即黑屏——默认的异步渲染模式下,系统输入法窗口出现/关闭后,Web 的图形 surface 会脱离合成树,截图整片黑、DOM 却正常。改用同步渲染模式后键入恢复。这类坑没有捷径,只能真机一条条撞出来。


八、架构:一张图

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

┌─ 鸿蒙 HAP ────────────────────────────────────────────┐
│  ArkTS 外壳(窗口 + ArkWeb + 加载/错误/诊断视图)        │
│      │ loadUrl(同源 http://127.0.0.1:<随机端口>)        │
│      │ 宿主桥(文件信箱,往返约 2ms)                    │
│  ┌───▼───────────────────────────────────────────────┐ │
│  │ Node(进程内,libnode.so / node::Start)           │ │
│  │   ├─ 垫片安装(带标记 + 自检)                     │ │
│  │   ├─ runProfile('ohos')                          │ │
│  │   ├─ Host:webserver + /api + connection         │ │
│  │   └─ 鸿蒙 seam 插件(shell/subprocess/picker…)    │ │
│  └───────────────────────────────────────────────────┘ │
│  资源:resfile 载荷 tar.gz + libs/libnode.so            │
└─────────────────────────────────────────────────────────┘

平台差异被收敛成三层:YAML patch 层(关 HMR、换 seam 行)、seam 插件包(鸿蒙实现)、带标记垫片(fs.link/fs.symlink 回退、flock 替身、sharp 替身等)。每一条垫片都有唯一标记、自检断言、登记理由与撤销条件——这是「零补丁」能长期成立、且经得起审计的原因。


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

三步:

1. 准备环境。 DevEco Command Line Tools(SDK API 24)、同级子模块 deepseek-harness、Node 18+、pnpm、HDC。Node 运行时来自 OpenHarmony 三方库中心仓的 @fellow99/node-bridge 与 @fellow99/node-runtime,ohpm install 自动拉取。

2. 跑构建。

node scripts/assemble-payload.mjs --deploy --apply   # 物化 dsh 运行树
bash scripts/build-hap.sh            # ohpm install → 载荷归档 + HAP 构建/签名

3. 安装并启动。

hdc app install -r entry/build/default/outputs/default/entry-default-signed.hap

默认构建是零 ACL 的 compat 形态,侧载不需要任何受限权限;如需 JIT 形态,用 scripts/set-node-mode.mjs jit 切换并重新构建(需已获可写代码内存权限)。


写在最后

DSH Desktop HarmonyOS Edition 的核心,其实只有一句话:抓住「dsh 的 webserver 本来就同源服务页面和 API」这个事实,然后老老实实把平台差异收敛成 profile + bundle + 一层可审计的垫片。

它换来的东西很具体:官方 Web UI 一个字节没改、上游零源码补丁、包体约 88 MB、安装零受限权限、升上游 tag 只需重跑管线。代价也很清楚:无 JIT 形态下解释器性能会打折,命令执行、内容搜索、文件变更原语等能力还需继续补齐。

如果你想自己动手,代码和文档都在这里:

如果你也在折腾「鸿蒙上跑 AI agent 应用」,希望这篇能帮你少走一点弯路。后面我会继续把真机调试、ACL 申请、沙箱适配这些脏活写细。

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