ZCode 3.16.2 鸿蒙移植修复记录

设备与环境

  • 设备: HUAWEI MatePad Pro Max (6XP0226603000087),HarmonyOS 7.0
  • ZCode 版本: 3.16.3 linux-arm64 AppImage(vendor/ZCode-3.16.3-linux-arm64/;升级记录见「升级 ZCode 版本的清单」末尾)。 历史:3.14.4/3.14.5/3.15.1/3.15.3/3.16.2。node-pty 的 linux prebuild 一律由构建插件忽略、改用自编的 openharmony-arm64;3.15.3+ 的 arm64 包内 ssh2 原生件本身即 aarch64 ✓ 3.16.3 升级(2026-10-24):零补丁改动——provider 配置与 3.16.2 字节相同;glob 导入又拆进 chunk(补丁本就遍历全部 chunk,命中);shiki engine:!1 命中新哈希 chunk。装机后启动/agent warmup/任务列表/历史会话渲染均正常
  • 架构: EntryAbility → XComponent → libadapter.so → libelectron.so → entry.mjs → host → agent

已修复的问题(7 层,按依赖顺序)

第 1 层:glob import 未被补丁覆盖 → 黑屏

根因: 3.14.4 的 esbuild 把 import{glob as ...}from"fs/promises" 拆进了 chunk-*.js 文件。 prepare-resfile.ts 只 patch index.js/host/index.js,漏掉了 chunk。 OHOS Node 20 无 fs/promises.glob,ESM 静态导入在链接期直接抛错 → 主 bundle 加载失败 → 黑屏。

修复: 补丁遍历 out/main/ 和 out/host/ 下全部 .js 文件(含 chunk-*.js), 将 glob 导入重定向到 glob-polyfill.mjs。

第 2 层:RUM 桩 API 漂移 → 主进程中断

根因: 3.14.x 通过 default.client.useReporter 等任意深链调用 @arms/rum-electron。 固定方法清单的桩随版本漂移崩溃(Cannot read properties of undefined (reading 'useReporter'))。

修复: 改为递归 thenable no-op Proxy——任意属性链/调用都返回可 thenable 桩, 兼容 BUSY 重试模式。不修改运行时构造语义。

第 3 层:process.title 守卫崩溃 → host 加载即死

根因: OHOS utility realm 的 process.title 是数据属性(value)而非访问器(get/set)。 wrapper 守卫的 _d.get.call(process) 对数据属性取 .get 得 undefined → SIGSEGV。

修复: 守卫兼容两种描述符:typeof _d.get === 'function' ? _d.get.call(process) : _d.value。

第 4 层:utility 事件循环排空 → host 静默退出

根因: OHOS utilityProcess 的 parentPort 不 ref 事件循环。 host 等待 MessagePort 期间 loop 排空 → 进程以 code 0 自然退出 → UI 显示「数据准备进程意外退出」。

修复: wrapper.mjs 加 setInterval(()=>{},60000) 保活。

第 5 层:「Agent 不支持独立存储准备」→ 存储准备失败

根因: wrapper 设置 ZCODE_AGENT_SERVER_COMMAND env 劫持了 resolveDefaultZCodeAgentCommand 路由进 env 覆盖分支(无 supportsStorageStartup 标记)。 且 ~/.zcode/server/agents/glm/ 缺少 sqlite-adapter 三件套。

修复: 移除 env + 在 wrapper 中成套摆放 zcode-cli.cjs/sqlite-adapter.cjs/ohos_sqlite_adapter.node 到 ~/.zcode/server/agents/glm/。

第 6 层:sqlite addon 跨 env 崩溃(SIGSEGV/SIGABRT)

根因(三重):

  1. 非 context-aware 注册(NAPI_MODULE)+ 进程级 g_stmt_class:worker env 使用主线程的类引用 → SIGSEGV
  2. process.title 守卫在 OHOS utility realm 对数据属性取 .get → 崩溃
  3. napi_add_env_cleanup_hook 在 OHOS utility 的 worker env 无 node::Environment → abort

修复:

  1. 按 env 惰性创建 Statement 类引用(mutex 保护的注册表)
  2. title 守卫兼容两种描述符
  3. 禁用 cleanup hook(少量 ref 泄漏可接受)
  4. 改为符号导出 napi_register_module_v1(OHOS loader 不支持构造器注册)
  5. 构建变体必须为 libshim(HIDE_NAPI_AND_UV)

补充(2026-10-07):存储准备 worker 的入口名是 ~/.zcode/server/agents/glm/zcode.cjs (应用自己落盘的那份)。删除 ~/.zcode 后应用无法重建它(其 agent 部署走远端 release, 本环境返回 HTTP 400)→ 报 unsupported_runtime。wrapper.mjs 现在会把 zcode-cli.cjs 额外复制为 zcode.cjs(仅在缺失时,不覆盖应用版本)。

第 7 层:agent 无法 spawn → main 进程中继

根因: utility 进程被 seccomp 禁止 execve(accessSync(X_OK) 通过的文件 spawn 仍 EACCES——拦截在 syscall 层)。

修复: spawn-wrapper.mjs 改为 relaySpawn——模拟 ChildProcess 接口, 通过 parentPort.postMessage 向 main 发送 spawn 请求,main fork agent 并中继 stdio/exit。 包含大帧分片传输(96KB/chunk)和 op 前缀正确传播。

附加修复:模型配置加载失败

根因: 3.14.x 按 resourcesPath/config/provider/zcode-builtin.json 查找 Bundled 模型配置, HAP 布局无此目录 → AggregateError → UI 报「模型配置加载失败」。

修复: AppImage 的 config/provider/zcode-builtin.json 放入 resfile, entry.mjs 设 ZCODE_BUILTIN_PROVIDER_CONFIG_FILE 指向随包路径。

第 8 层:agent 中继缺 spawn 事件 → 任务列表恒空

根因: spawn-wrapper.mjs 的 relaySpawn 模拟 ChildProcess 时只 emit exit/error, 从不 emit spawn。host 的 ZCodeAgentProcessManager 只在子进程 'spawn' 事件里 runtimeLifecycleEmitter.fire({state:'available'});zcode-task-index-syncer 用该事件 设置 runtimeGeneration,缺失时 ensureWorkspaceSubscription 只建状态、永不注册 emitter 监听、永不订阅 sessions-index → 帧无人消费 → tasks 表恒 0 行 → 侧边栏任务列表空。 (这解释了此前「键对齐 / re-fire」补丁全部无效:根本没有监听者。)

修复: relaySpawn 在收到 main 的 op:'pid'(fork 成功、真实 pid 已知)时 emit('spawn'),与真实 ChildProcess 语义一致。

第 9 层:sqlite addon 缺命名参数绑定 → 所有写入失败

根因: ZCode 用 node:sqlite 的命名参数形式 stmt.run({ '@name': value }); addon 只实现位置绑定,对象被当作单个位置参数 → 命名参数全部落成 NULL → sqlite3_step failed (19): NOT NULL constraint(tasks 表种子写入 13/13 失败)。 agent 侧同源故障:会话/消息不落盘、v4 gateway 报 Session not found。

修复: ohos_sqlite_adapter.c 实现命名参数绑定(sqlite3_bind_parameter_name 取参数名, 先裸名后带前缀名回退),并补 bigint→int64、Buffer/TypedArray→BLOB(原先静默落 NULL)。 compile.sh 补 -Os + strip(与旧产物一致)并修正 NAPI 头路径(temp/ → docs/)。 后续补齐 DatabaseSync.isTransaction(用 sqlite3_get_autocommit 判断,无需自跟踪状态), 并加 probeTransaction() 自检(每次启动跑一遍 open→BEGIN→COMMIT 的 isTransaction 序列)。

第 10 层:对话代码块渲染为空(shiki 高亮引擎走 WASM)

症状:对话里的代码块只有头部(语言标签 + 复制按钮),正文一片空白(外层容器还留着 200px 占位高度);同一段代码用普通文本输出就正常。

根因:代码块由 @pierre/diffs 渲染(DIFFS-CONTAINER 的 shadow DOM),高亮走 shiki 的 Oniguruma WASM 引擎(应用把 preferredHighlighter 定成 "shiki-wasm", 3.14.4 里是 var Jk='shiki-wasm')。本引擎编译期裁掉了 WebAssembly,所以

  • worker:Worker error: ReferenceError: WebAssembly is not defined(diffs.worker-*.js)
  • renderer:WebAssembly is not defined at Module.t (wasm-*.js) at async …(styles-*.js)

两处都拿不到 token → <pre> 为空。取证:CDP 实测 typeof WebAssembly === "undefined"; shadow DOM 里就是 <pre></pre>;重载页面捕获到上面两条错误。

修复:包内自带 @shikijs/engine-javascript(oniguruma-to-es 纯 RegExp 转译, 实测 new Function/d 标志/lookbehind 在该引擎均可用)。构建期把两处引擎选择短路到 JS 分支:压缩形态 engine:e===shiki-wasm?<wasm>:<js> → engine:!1?…(锚点 (?<![\w$.])([A-Za-z_$][\w$]*)===shiki-wasm\?,见 prepare-resfile.ts §5c; @pierre/diffs/dist 下的未压缩副本走字面量补丁 preferredHighlighter === "shiki-wasm" ?)。 代价:JS 引擎比 WASM 慢(首次高亮语言有转译开销),换来完整语法着色。

验证:装机后 shadow DOM <pre> 内 89 个 token span、71 个带主题色 (--diffs-token-dark:#F97583 等),控制台不再有 WebAssembly/worker 报错。

第 11 层:3.15.x 启动失败 —— preload 用了引擎没有的 Electron API

症状:升级到 3.15.1 后应用"打不开",屏幕是 ZCode 自己的启动失败页 「无法完成启动准备 / 未能收到启动状态 / 诊断 ID: startup-channel-unavailable」。

根因:渲染进程控制台报 Unable to load preload script: .../out/preload/index.cjs + TypeError: pt.contextBridge.executeInMainWorld is not a function —— 3.15.x 的 preload 用了 Electron 35+ 的 contextBridge.executeInMainWorld (用法是 let t = contextBridge.executeInMainWorld({func:Ux}) === true,拿布尔值开关一个特性), 本引擎(Chromium 132 时代的 Electron)没有该 API → preload 整体加载失败 → 渲染进程没有 IPC 桥 → 启动握手拿不到状态。注意这不是补丁漏命中:既有补丁在 3.15.1 产物里全部命中(含 renderer 的 shiki-wasm → JS 引擎)。

修复:构建期给 out/preload/*.cjs 前置垫片(prepare-resfile.ts §7b,标记 /*zcode-eimw-shim*/):若 contextBridge.executeInMainWorld 不存在,就用 webFrame.executeJavaScript 把 func 源码注入主世界执行并同步返回 true。 验证:启动失败页消失、界面正常(3.15.1 新功能可见)、控制台无该 TypeError。

行为定制:标题栏关闭按钮 → 隐藏窗口(hideAbility)

原行为: 窗口 frame:false,标题栏自绘;X → executeDesktopCommand(CloseWindow) → win.close() → handleDesktopWindowCloseRequest(linux 分支)→ 确认框 → app.quit()。

改为: main bundle 补丁把该命令的 win.close() 换成 win.hide()(锚点 case $.CloseWindow:t?.close();return;)。引擎经 AppWindow.HideWindow 调用 ArkTS 的 hideAbility(),窗口隐藏、进程保留;EntryAbility 已注册状态栏图标 (statusBarIconClick → context.showAbility()),点击图标即可恢复窗口。 系统侧关闭(任务栏/最近任务)仍走原确认退出逻辑。

终端(node-pty):无 pty 环境下的管道退化方案

现象: node-pty is unavailable in this runtime: Cannot find module './prebuilds/openharmony-arm64/pty.node',终端完全不可用。

三层根因:

  1. node-pty 按 prebuilds/<platform>-<arch>/pty.node 找产物;OHOS 上 process.platform === "openharmony",官方包只有 linux-arm64 → 需自编 (native/node-pty-ohos/,compile.sh;必须静态链接 libc++,动态依赖 libc++_shared.so 会 ERR_DLOPEN_FAILED;注册要导出 napi_register_module_v1)。
  2. OHOS Electron 对 .node 请求有重写:取「node_modules 之后那段」到 NODE_PATH (默认 <bundle>/libs/arm64/node_modules.asar.unpacked/:<bundle>/libs/arm64/) 再找一次 → 产物要放到 libs/arm64/<node_modules 之后的路径>(仓库对应 electron/libs/arm64-v8a/node-pty/prebuilds/openharmony-arm64/pty.node)。
  3. 设备沙箱不允许打开 /dev/ptmx(addon 探针:main/host 均 openpty=fail(errno=13), 连 shell 用户 exec 3<>/dev/ptmx 也是 Permission denied;SDK 无 pty API) → 真 pty 不可得。

修复: pty-shim.mjs 顶替 host 的 import("node-pty")(构建期重定向): 优先真 pty(将来设备放开 pty 会自动启用),不可用则退化为「管道 + 子进程 + 本地行 编辑」(host 可 fork+execve /bin/sh,已实测 exec=ok read=ok)。 输出统一规范成 CRLF(管道无 ONLCR,裸 LF 在 xterm.js 会"阶梯"错位),并过滤 -i + 无 tty 产生的两行 shell 警告。

能力边界: 可执行命令/看输出/cd/管道重定向;不可跑全屏交互程序(vim/top)、 无真作业控制、isatty() 相关特性缺失(部分程序因此关闭彩色输出)。 行编辑(退格)与 Ctrl+C/Ctrl+D 由 shim 模拟(无历史记录/方向键)。

诊断探针: addon 导出 probePty() / probeExec(),wrapper.mjs 启动时打印 [wrapper] pty-probe(host): ... / [wrapper] exec-probe(host): ..., 用于快速判断"终端为何不可用"(pty 权限 / exec 能力)。

诊断手段(无需重建,可复用)

  • 构建期补丁报告:prepare-resfile 每次构建末尾打印 ── patch report ──, 逐条列出补丁命中数/文件数;[OK] 必需补丁已命中、[OK*] 可选补丁已命中、 [SKIP] 可选补丁未命中(正常)、[MISS] 必需补丁未命中。 未命中的必需补丁会在末尾集中告警并给出「未命中后果」——换 ZCode 版本后先看这一段, 就知道哪个补丁因 bundle 形状变化失效了。 可选诊断探针:构建时设 ZCODE_DIAG_EMITTER_KEY=1 会给 emitter 取键函数加日志 (排查 sessions-index 帧派发的 key 不匹配)。
  • renderer 读私有文件:CDP 连 ws://127.0.0.1:9229(hdc fport tcp:9229 tcp:9229), 在页面上下文执行 fetch('file:///storage/Users/currentUser/...') 可直读应用私有目录: v2/tasks-index.sqlite(含 -wal)、cli/db/db.sqlite、cli/log/zcode-<date>.jsonl、 部署包 /data/storage/el1/bundle/electron/resources/resfile/...。
  • renderer 直连 host RPC:扫描 React fiber 树取得 window.__probeServices 服务容器, 直接调用 zcodeTaskService.listTasks(...)、 zcodeAgentService.onDynamicSessionsIndexFrame({workspacePath})、 subscribeSessionsIndexV4(...),观测 host 内部状态。
  • 日志计数判定"代码路径是否执行":同一模块的日志有的出现、有的 0 次, 是"该分支未运行"的强证据(例:ZCode agent process started 0 次 vs ZCode agent spawn preflight 26 次 → spawn 处理器从未执行)。
  • 部署产物核对:out/host/index.js 与本地 resfile 逐字节比对,确认补丁真的进包。

升级 ZCode 版本的清单

  1. 解包新 AppImage 到 vendor/ZCode-<版本>-linux-<arch>/(目录名要能被 ZCode-x.y.z 正则解析;AppImage 内层就是 resources/,与插件期望的目录结构一致)。 Windows 上直接用 7-Zip 25.x(原生支持 SquashFS): 7z x ZCode-<版本>-linux-x64.AppImage -o vendor\ZCode-<版本>-linux-x64 "resources\*" -y ——同时必须删除 vendor/zcode/app.asar + vendor/zcode/app.asar.unpacked + vendor/zcode-cli.cjs,插件只在它们缺失时才自动探测版本目录(否则继续用旧资源)。 归档里已有的旧 renderer chunk(styles-*/diffs.worker-*/wasm-* 等带哈希名)不会被清理, 属惰性文件(只增加 HAP 体积),需要时手动清 resfile/resources/app/out 后重建。
  2. 手工同步 web_engine/.../resfile/resources/app/config/provider/zcode-builtin.json (取自新 AppImage 的 config/provider/;该文件不在自动复制范围内,不更新会导致模型列表过时)。
  3. 构建环境:hvigorw 在 C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat, 且必须先设 DEVECO_SDK_HOME=C:\Program Files\Huawei\DevEco Studio\sdk (未设置会以 00303217 Configuration Error 直接失败)。命令: hvigorw assembleApp -p buildMode=debug --no-daemon。构建完先看 patch report: [MISS] 的必需补丁要按「未命中后果」逐条修锚点(换版本后锚点漂移是最常见的回归)。
  4. 装机后按上面的诊断手段核对:部署包内补丁字符串、tasks-index.sqlite 行数、 hilog 中 ZCode agent process started / AppWindow --> hideAbility success、 以及 基线补齐 task index 失败 之类的新报错。
  5. 已知每次升级都要留意的耦合点:锚点补丁(glob/node:sqlite/spawn/close→hide/re-fire/ shiki-wasm→JS 引擎)、 ZCODE_BUILTIN_PROVIDER_CONFIG_FILE 等 env 名、agents/glm staging 目录与文件名、 agent 参数面(app-server --stdio)、addon 的 node:sqlite API 覆盖面、 引擎侧 Electron API(Chromium 132 固定)、原生模块(node-pty/ssh2/sharp 降级)。

当前状态

功能 状态
应用启动 ✅
登录 (BigModel) ✅ tyxbuefo
对话(agent 回复) ✅ DeepSeek/GLM
模型切换 ✅
项目列表 ✅ Documents / SweetMusic
工具执行 ✅(agent 分析项目结构)
设置持久化 ✅
任务列表 ✅ tasks 表 21 行,侧边栏项目/任务均正常显示
终端 ✅ 可用(管道退化模式,见下节;设备沙箱禁 pty)

遗留问题

1. 引擎无 WebAssembly(已绕行,残留长期约束)

事实:这套 OHOS Electron 引擎编译期裁掉了 WebAssembly(renderer/main/host 均 typeof WebAssembly === "undefined"),因此 undici 的 llhttp(wasm) 解析器不可用, main 侧联网("保存远程文件"下载、远端组件下载)原本直接抛 ReferenceError: WebAssembly is not defined。

绕行:undici-shim.mjs(Node http/https 实现 fetch/Agent/ProxyAgent 兼容面)

  • 构建期把 from"undici" 重定向到 shim + entry.mjs/wrapper.mjs 覆盖 globalThis.fetch(Node 内置 undici 是另一份副本,同样需要 wasm)。 dispatcher.connect.lookup 透传给 http.request,保住 app 的 DNS pinning/私网防护。 ProxyAgent 不受支持(告警后直连),host 侧配置代理时属降级。

同类影响(已修):对话代码块的语法高亮同样依赖 wasm(shiki 的 Oniguruma 引擎), 原本渲染成空 <pre>——见「第 10 层」,现已切到包内自带的纯 JS 引擎(oniguruma-to-es), 恢复完整着色。

残留:

  1. 任何只能用 wasm 的第三方模块在本移植版都无法工作(长期约束):docx/xlsx/pdf 预览 (duke_sheets_wasm / docx_wasm / pdfjs 的 openjpeg、qcms)、rive 动画等仍属不可用。 能换纯 JS 实现的(undici→undici-shim、shiki→JS 引擎)已逐个绕开。
  2. 仍会有 2 条/启动的 WebAssembly is not defined(unhandledRejection):诊断定位为 Electron 自身的 net.fetch 内部读全局 Request(undici 懒加载),触发点是 app 的 遥测上报(sendReport → sendReportAttempt → session.fetch)。属无害(仅遥测发送失败); 要消除需给 Electron 内部伪造 Request,风险大于收益,故保留。
  3. provider-settings.refresh FAIL: ZCode Built-in client-config: HTTP 400 是远端接口 返回的 400(非 wasm 问题,有内置回退),仍在。

2. 终端为退化模式(无 pty)

设备沙箱禁 /dev/ptmx(见"终端"节),当前用管道退化方案:可执行命令/看输出/cd/ 管道重定向;不能跑全屏交互程序(vim/top)、无真作业控制、无历史记录/方向键、 isatty() 相关特性缺失。若系统放开 pty,shim 会自动切回真 pty。

3. git 与工具链改为 harmonybrew 提供(2026-10-28 起)

设备上的 ~/.harmonybrew(61 个包、约 1.3 GB)实测可直接执行:git 2.56.0、 node v26.10.0、python3 3.14.7 均正常,agent 终端可直接使用。此前"不可用"的结论 来自探针盲区(statSync 失败即返回,从未真正试过 execve)+ 缺权限声明,现已纠正:

  • ohos.permission.CUSTOM_SANDBOX 是 exec 外部已签名二进制的开关(对照实验: 有它 test -x ok、node/git 可跑;移除后立刻 EACCES)。文件持久授权只覆盖读写。
  • 个人文件夹持久授权必须保留并自愈:授权会在重装/升级后丢失(读仍可用、exec 失效)。 应用启动 3s 后自动检测 → 失败弹 dialog(标题「目录授权」)→ 确认后拉起文件夹选择器 → persistPermission → activatePermission → URI 存 preferences。实测可选中个人 文件夹根(file://docs/storage/Users/currentUser)。
  • SHELL 必须钉成 /bin/sh(entry.mjs / wrapper.mjs 两域):CLI 执行命令前会 自动探测 shell(先 $SHELL,否则按 PATH 找 bash/zsh)。brew bin 进 PATH 后系统无 bash、brew 有 → 选中 brew 的 bash → execve 卡死(fork 成功但从不返回,表现为 Bash 工具永远"正在执行"、停止按钮也失效)。钉死 SHELL 后不再复现。
  • 自打包 git(HNP + el2 脚本农场 + 145 个 helper 包装脚本)已全部移除; ~/.harmonybrew/bin 与 /system/bin:/bin 在模块期无条件追加进 PATH (existsSync 依赖 /storage 挂载可见性,早期会假阴性)。

4. agent 侧 session.persisted_missing(评估后保留)

agent 创建 subagent 会话时查父会话的时序告警(父行尚未提交),采样约 12/2370 行; 子代理功能实测正常、用户不可见、hilog 无对应错误。消除需改 14MB 压缩 agent bundle (zcode-cli.cjs),风险大于收益;且真实持久化故障时该告警是有用信号。

5. 升级风险(换 ZCode 版本时)

见"升级 ZCode 版本的清单"一节:锚点补丁(有 patch report 告警兜底)、env 名/路径/ CLI 参数面等 workaround、addon 的 node:sqlite API 覆盖面、引擎固定 Chromium 132、 原生模块降级、zcode-builtin.json 需手工同步。

关键文件

文件 变更
hvigor/plugin/prepare-resfile.ts 补丁扩展到全部 .js + RUM 递归桩 + spawn-wrapper 模板拷贝
hvigor/plugin/templates/wrapper.mjs title 守卫 + 保活 + agent bundle staging + 启动探针(pty/exec/txn)
hvigor/plugin/templates/spawn-wrapper.mjs main 中继 + 大帧分片 + HNP 路径候选 + emit('spawn')
web_engine/.../entry.mjs handleAgentRelay + IPC 观测 + ZCODE_BUILTIN env
native/ohos-sqlite-adapter/ohos_sqlite_adapter.c context-aware 注册 + per-env 注册表 + busy_timeout + errcode + 命名参数绑定
native/ohos-sqlite-adapter/compile.sh NAPI 头路径探测(docs/ 优先)+ -Os + strip
native/node-pty-ohos/ node-pty 1.1.0 unix 源码 + node-addon-api 头 + compile.sh(libshim、静态 libc++、符号注册)
hvigor/plugin/templates/pty-shim.mjs 终端后端 shim:优先真 pty,否则管道退化(CRLF 规范化 + 行编辑)
hvigor/plugin/templates/undici-shim.mjs WASM-free undici 替身(http/https 实现 fetch/Agent,lookup 透传保住 DNS pinning)
native/ohos-sqlite-adapter/ohos_sqlite_adapter.c 命名参数绑定 + isTransaction + probePty()/probeExec()/probeTransaction() 诊断导出
web_engine/.../resfile/.../ohos_sqlite_adapter.node libshim 构建产物(同步替换两处)
web_engine/.../resfile/.../config/provider/zcode-builtin.json 3.14.4 随包模型配置
electron/libs/arm64-v8a/ohos_sqlite_adapter.node 与 resfile 同步(OHOS loader 重定向)

构建与部署

cd C:\Users\salmo\DevEcoStudioProjects\zcode
export DEVECO_SDK_HOME="C:\Program Files\Huawei\DevEco Studio\sdk"

# 推荐:一条命令完成 构建 + 安装 + 启动(USB 离线时用 TCP 目标)
devecocli device list
devecocli run --device 192.168.0.10:12345

# 仅改 native addon 时:先重编(libshim 变体)再走上面的 run
cd native/ohos-sqlite-adapter
OHOS_SDK_DIR="C:/Program Files/Huawei/DevEco Studio/sdk/default/openharmony/native" bash compile.sh
cp ohos_sqlite_adapter.node ../../electron/libs/arm64-v8a/ohos_sqlite_adapter.node
cp ohos_sqlite_adapter.node ../../web_engine/src/main/resources/resfile/resources/app/ohos_sqlite_adapter.node

第 12 层:平板模式 agent 无法启动(CLI 找不到内置 provider 配置)

背景:这台设备的"平板模式"下应用不能 exec 应用自有文件/HNP 文件(PC 模式可以), 所以 fork 路径不可用,agent 只能由 agent-host.mjs 在 Electron utility 进程里承载。 承载后 agent 能加载,但 200ms 内静默 exit(1):无异常、无 stdout/stderr、 无 worker/child_process 创建、它自己的 logger 也没写。

根因:读 CLI 源码 + 探针逐步收敛到 main → prepareCliProviderRuntimeEnv → gfs():

无法定位 CLI ZCode Built-in Provider Config:
  <dirname(argv[1])>/provider/zcode-builtin.json,
  <dirname(argv[1])>/../../../../../config/provider/zcode-builtin.json

它抛错 → 被 main 的 catch 吞掉(写进 u = Object.create(process.stderr) 的 passthrough,普通 stderr 钩子抓不到)→ process.exitCode = 1 → complete(1) → exit(1)。 PC 模式之所以正常:fork 路径继承了 main 设的 ZCODE_BUILTIN_PROVIDER_CONFIG_FILE 环境变量,utility 路径没生效。

修复:把 resfile 的 config/provider/zcode-builtin.json 摆到 CLI 同级 (el2/provider/zcode-builtin.json,以及代理目录 ~/.zcode/server/agents/glm/provider/), 不依赖环境变量。同时补 el2/zcode.cjs(存储准备入口;wrapper 原先只在缺失时复制, 导致它长期停留在旧版、且 el2 里根本没有)。

验证:el2 日志出现持续协议帧(process/resourceSample,每 60s 一次,连续 6 分钟); agent 自身日志 ~/.zcode/cli/log/zcode-<date>.jsonl 恢复写入;界面项目/模型选择器/ 变更前确认模式/任务列表全部正常。

⚠️ 事后更正:上述证据不足以证明修复成功——那些协议帧是 agent 写自己的文件(tee 落到 el2 日志), 并不经过 app 的协议通道,属于假阳性。真正的判据是"界面发消息→收到回复",见第 13 层。

配套修复(utility 承载 agent 必需的几处,缺一不可): ① process.title 守卫(否则 uv_set_process_title 的 memset 下溢 SIGSEGV); ② 每进程 enableJIT()(utility 是系统新建进程,不继承 fork+execve 的 prctl(SET_JITFORT)); ③ 入口必须 staged 到 el2(utility 读不到 el1 入口); ④ FIFO 当 stdin(utility 只接受 stdin:'ignore',fd0 是 /dev/null); ⑤ 失败节流(回退每次尝试要拉起 utility + 加载 15MB agent,而 agent 管理器每 ~4s 重试, 曾把设备拖垮:load 36.78 / 可用内存 299MB;节流后 load ~14 / 4.4GB)。

防复发加固(2026-10-13 收尾,commit b5e86ba):

  • 诊断日志统一 4MB 上限:排查期加的探针会持续追加日志,其中一份曾递归涨到 5.5GB 把整机拖垮。现在 agent-host.log、agent-diag.log、relay-stdout.log、relay-stdin.log 全部走 capAppend()(entry.mjs / agent-host.mjs):超过 4MB 先清空再写。 实测生效:agent-diag.log 7.8MB → 10KB。
  • require("node:sea") 桩:OHOS 的 Node 20 没有 node:sea(Node 21+ API), 而 CLI 里有懒加载模块在顶层无 try/catch 地 require 它 → 一旦初始化即 MODULE_NOT_FOUND → 被 main 的 catch 吞掉 → 静默 exit(1)。构建时替换为 {isSea:()=>false}(非 SEA 环境下语义等价)。
  • electron 启动器以 .so 后缀入 libs:HAP 打包器会静默丢弃 libs/ 里无扩展名的文件, 必须叫 libelectron-launcher.so 才能随包分发(该副本由 prepare-resfile 生成,已 gitignore)。

第 13 层:协议帧到不了 app —— utility 的 stdout 管道不可达(改文件通道)

症状:界面点发送 → "发送失败,请稍后重试。" → 展开详情 SQLite startup failed: transport_closed。 界面上项目/任务列表/模型选择器都正常(它们来自本地索引与内置配置,不代表链路健康)。

根因:平板模式(agent 由 utility 承载)下 agent 的 stdout 管道到不了 main: 两个实例运行 30+ 分钟,main 侧 relay-stdout.log / relay-stdin.log 从未生成(0 字节), 而 agent 进程自身一直在输出(它的 tee 把帧写进 el2 日志,所以"看起来活着")。 host 因此永远收不到 agent 的启动帧 → 握手/存储准备失败 → 上层只看到 transport_closed。 PC 模式用 child_process.fork 的真管道,不受影响。

修复(文件通道,双向对称):

  • agent → app:agent-host.mjs 把 agent 的 stdout 分片按 base64 逐行写入 agent-frames-<mainPid>-<relayId>.jsonl(hostWrite 标记排除宿主自己的诊断行—— 帧必须逐字节忠实,混入非 JSONL 行会让 app 的解析器报错);entry.mjs 每 40ms 轮询增量, 按与 fork 路径完全相同的 __agent_relay_stdout 协议转发(>96KB 自动分片, wrapper 按 seq 重组)。utility 分支不再 wireStdout(proc.stdout)(既不可达,也防重复)。
  • app → agent:main 把 stdin 帧追加到 agent-in-<mainPid>-<relayId>.jsonl; agent-host 轮询增量并写进它用 O_RDWR 占住的 fd0 FIFO 管道(agent 直接读原始 fd0)。 读写端都在 agent-host 进程内部 → 两个 app 实例并存时不再互相 unlink 劫持对方的 FIFO (旧方案把写端放在 main,B 实例启动即把 A 的 FIFO 换掉)。
  • 路径经 argv 传:实测 utilityProcess.fork 的 env 选项在 OHOS 上不可靠 (ZCODE_AGENT_CWD/FRAMES/IN 都可能收不到,帧文件曾写到 pid 默认名导致 main 轮询空文件); argv 已被反复证明可达(agentScript 就是这么进来的)。
  • 失败节流改冷却制:旧"每 main 进程只尝试一次"使 transport 一旦断开就永久无法恢复 (用户的"重新连接"也无效,只能重启 app)。现为 60s 冷却:防重试风暴拖垮设备,又能自愈。

排查这类问题的可用事实:①relay-*.log 是否生成(main 是否真的见过帧); ②agent-diag.log 的 relay-utility attempt / frames tail start / agent-host ready; ③两个实例并存会同时污染 FIFO 与日志——先确认设备上只有一个 app 实例。

端到端验证(唯一可信的判据):界面发消息 → agent 回帧。实测命令序列 createSession → conversation/subscribe → session/read / workflowRuns / plans, agent-frames-*.jsonl 1.8MB、agent-in-*.jsonl 12.8KB,界面无报错,用户实测可用。

行为定制:单实例(框架 launchType + Electron 侧锁,双保险)

背景:这台设备上曾实测出现两个完整 ZCode 实例并存(各自带 Renderer/NodeService 与自己的 agent 进程),共享 ~/.zcode 存储与 el2 传输文件,互相干扰(旧传输方案下第二个实例会 unlink 掉 第一个的 FIFO;用户也在任务视图里看到两张 ZCode 卡片)。

改动:

  1. electron/src/main/module.json5:EntryAbility.launchType 从 specified 改 singleton (specified 下 key 不同即新建实例;实测构建产物已确认写入 singleton)。
  2. entry.mjs 入口加单实例硬锁:el2/app-instance.lock 存 main pid,启动时若锁文件里的 pid 仍存活(process.kill(pid,0))→ 本实例写 app-instance.wake 标记后 app.quit(); 存活实例轮询 wake 标记,把已有窗口 show/focus 到前台(尽量贴近"复用"体验)。 锁文件在进程退出时自行清理(仅当仍属自己)。

实测:force-stop 后 aa start ×3 → 始终只有 1 个主进程(同一 pid,无新建); 单实例下传输正常(agent-frames/agent-in 均持续增长),用户消息能生成任务。

6. 手机(Phone 设备类型)无法运行引擎——设备策略禁多进程(诊断结论,非本仓可修)

现象:HAP 能装进手机(deviceType 加 phone 后),打开黑屏。

证据链:

  • 现场:app 主进程活着但无任何子进程(对照平板:Renderer/NetworkService/NodeService 齐全);手机 hilog: Adapter: Got invalid device type: phone;StartNativeChildProcessWithName failed … errcode: 16010004; StartGpuProcess error: 16010004。
  • SDK 头文件 AbilityKit/native_child_process.h:NCP_ERR_MULTI_PROCESS_DISABLED = 16010004 —— "The multi-process mode is disabled on the device"。
  • SDK JS API 文档(childProcessManager):「该接口在 Tablet、PC/2in1 中可正常调用,在其他设备类型中返回错误码」。
  • module.json5 配置文档:process / isolationMode / isolationProcess 均标注「仅 2in1 和 tablet 设备支持」。

结论:OHOS Electron 引擎是 Chromium 多进程架构(GPU/Network/Renderer 均为原生子进程), 而手机设备类型禁用多进程 → 没有 GPU 进程 → 没有合成 → 黑屏。不是移植层(JS/配置)能绕过的; 要支持手机需要引擎方提供单进程/phone 适配的构建,或设备侧策略放开。附注:该手机上连 HNP electron 也不在 (探测 ENOENT),所以即便只跑 CLI agent 也缺 Node 运行时。

升级到 3.15.3 的实测记录(2026-10-14)

  • 补丁零回归:补丁报告 26 项全部命中(所有必需补丁均已命中),锚点无漂移—— 含 close-button→hide(main)、shiki-engine-js(renderer/diffs dist)、preload-eimw-shim、 spawn-wrapper(host)、CLI 的 sqlite-adapter 与 node-sea-stub。
  • CLI(glm/zcode.cjs)3.15.1→3.15.3 仅 +219 字节;node:sqlite/require("node:sea")/ app-server/zcode-builtin 锚点数量与旧版一致。
  • config/provider/zcode-builtin.json 与 3.15.1 字节相同(无需同步,仍照抄了一份到 resfile)。
  • 3.15.3 是 linux-arm64 包:app.asar.unpacked 里 ssh2 原生件已是 aarch64(此前 x64 包内 恰好也是 aarch64),node-pty 仍是 linux-arm64 prebuild(照旧忽略)。
  • 版本指纹(设备侧可核):el2 缓存的 zcode-cli.cjs = 15127055 字节 (= 3.15.3 原始 15127039 + 补丁增量);构建缓存 .hvigor/cache/zcode-extracted/package.json "version": "3.15.3"。
  • 本次平板走的是 fork 真管道路径(不是 utility 文件通道):HNP electron /data/service/hnp/electron.org/... 现在可 exec(NET 探针返回 NET_EXIT code=0 out=NET_OK status=401), pickAgentRuntime() 探测通过 → relay pid= + relay-stdout.log 3.8MB 持续增长。 utility 文件通道(第 13 层)仍是设备策略收紧时的回退。
  • 部署后实测:界面正常、会话订阅/帧正常(v4/conversation/subscribe ack + v4/conversation/frame), 单实例锁正常,用户确认可用。

升级到 3.16.2 的实测记录(2026-10-20)

这次升级踩了三个新坑,全部已修,补丁报告 26 项全绿。

① bundle 导入形状:node: 前缀(导致 3 个补丁 MISS) 3.16.2 起所有 Node 内置模块导入统一带 node: 前缀,且 glob 导入变成双绑定形态: import{glob as ky,readFile as Sy}from"node:fs/promises"、import{spawn}from"node:child_process"。 → 修正 glob-polyfill(main) / spawn-wrapper(host) 正则接受 (?:node:)?; host 侧已不再导入 glob(已核查 out/host 全部 chunk 无 glob 绑定、无 glob( 调用)→ glob-polyfill(host) 标记为可选(未命中显示 [SKIP] 而非 FAIL)。

② sqlite-adapter 的具名导出形态(ESM 导入会解析失败) Node 的 cjs-module-lexer 只认得出对象字面量的第一项:module.exports = { DatabaseSync: x, version: y } 里 version 解析不到 → import{version}from"...cjs" 直接 SyntaxError。3.16.2 起主/host bundle 都有 import{DatabaseSync}from"../../sqlite-adapter.cjs"(ESM 形态,3.15.3 只有 createRequire 形态)。 → adapter 改为 exports.X = ... 逐项导出(lexer 100% 可识别),并补 3.16.2 新用到的 backup (VACUUM INTO 实现,返回 Promise)。

③ 原生 addon 缺 Node 22 的 StatementSync API(真正的启动阻塞点) 现象:启动页「无法完成启动准备」,data-error-code="sql_failed", failedPhase="preparing_host_storage"(0 秒即失败)。真因:3.16.2 的 启动迁移 snapshotTable 用 stmt.setReadBigInts(true) + stmt.iterate(),而我们的 addon 的 Statement 只有 get/all/run → TypeError → 迁移失败 → sql_failed。 → native/ohos-sqlite-adapter/ohos_sqlite_adapter.c 新增 iterate(复用 all 的行数组包数组迭代器) 与 setReadBigInts(接受参数、no-op;一次运行内快照哈希自洽)。重编 libshim 变体后 同步到 electron/libs/arm64-v8a/ 与 resfile(799160 字节)。 注意:升级 3.16.2 必须带上这个新 addon,否则启动必然 sql_failed。

新增启动诊断(entry.mjs,长期保留)

  • ZCODE_MAIN_OK pid=N / ZCODE_MAIN_FAIL <stack> 写入 agent-diag.log(判断主模块是否加载成功);
  • APPLOG[6000|16000] 把 ~/.zcode/v2/logs/ 最新两个文件尾部落盘到 agent-diag.log —— shell 读不了用户存储,而 app 自己的启动错误就在这些日志里(这次正是靠它 + React 状态定位到 sql_failed)。

其它事实:CLI(glm/zcode.cjs)15127039→15243891(+114KB,新增 2 处 require("node:sqlite"), 补丁 4 处全命中);provider 配置变化(184963→180598,已同步);ESM import{DatabaseSync} 的 node:sqlite 站点 ×3(main)/×3(host)。验证:启动成功(无 sql_failed)、agent 正常 fork (relay pid=+relay-stdout 增长)、模型列表来自 agent(glm-5.3-flash)、单实例锁正常、 当前运行日志零 error/warn。

用户目录(home)访问范围(实测 2026-10-20)

应用进程(app uid)已能访问整个用户目录: /storage/Users/currentUser 根目录可列出(Desktop/Documents/Download/Images/Music/Videos/"Huawei Share"/.zcode/.npm/.hvigor/... 全部可见), Documents/Desktop/Download/.zcode 均可读;写入方面 agent 一直在写 ~/.zcode/cli/log 与 ~/.zcode/v2(同为 app uid)。 module.json5 已声明 READ_WRITE_USER_FILE + READ_WRITE_DOCUMENTS_DIRECTORY/DESKTOP/DOWNLOAD + FILE_ACCESS_PERSIST, ArkTS 侧有运行时申请通道(PermissionManagerAdapter.requestPermissionsFromUser)。 无需额外申请。注意:hdc shell 看不到该挂载(不是应用权限问题),需要读 ~/.zcode 内部文件时由 app 落盘到 el2 (APPLOG 诊断 / HOME_ACCESS 探针就是这么做的)。 补充(持久化):ohos.permission.READ_WRITE_USER_FILE 在 SDK 的 PermissionDefinitions.json 里是 grantMode=system_grant(安装时自动授予、不弹运行时授权框),因此重启/升级都不会丢,无需额外"持久化申请"; 但 availableLevel=system_basic + provisionEnable=true —— 正式签名时需要在 AGC profile 的 ACL 里勾选该权限, 当前调试 profile 已带上(否则 HOME_ACCESS 探针不会成功)。FILE_ACCESS_PERSIST 是另一条路:只用于 file picker 返回 URI 的持久授权(fileShare.persistPermission),不是整目录授权。

为什么 agent 无法执行 harmonybrew 的二进制(实测 2026-10-21)

不是 exec 策略,也不是缺失解释器——是文件权限: BREW_EXEC 探针(应用身份、uid 20020278)对 ~/.harmonybrew/bin/* 的 stat 和 copyFile 全部 EACCES, 也就是说应用连读都读不到,更谈不上 exec。目录可 readdir,但文件本身属主是 20001006、模式 -rwxrwx--- (other 无任何权限),用户存储是 FUSE(default_permissions)→ 属 other 的 uid 在 lookup/stat 阶段就被拒。 这也解释了 agent 看到的"文件列表有、权限位显示 ?"。 修复:以属主身份把这些文件放开(如 chmod -R a+rX ~/.harmonybrew),应用即可读/exec; 若放开后 exec 仍被拒,再检查用户目录所在挂载是否带 noexec。