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)
根因(三重):
- 非 context-aware 注册(
NAPI_MODULE)+ 进程级g_stmt_class:worker env 使用主线程的类引用 → SIGSEGV process.title守卫在 OHOS utility realm 对数据属性取.get→ 崩溃napi_add_env_cleanup_hook在 OHOS utility 的 worker env 无node::Environment→ abort
修复:
- 按 env 惰性创建 Statement 类引用(mutex 保护的注册表)
- title 守卫兼容两种描述符
- 禁用 cleanup hook(少量 ref 泄漏可接受)
- 改为符号导出
napi_register_module_v1(OHOS loader 不支持构造器注册) - 构建变体必须为 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',终端完全不可用。
三层根因:
- 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)。 - 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)。 - 设备沙箱不允许打开 /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 started0 次 vsZCode agent spawn preflight26 次 → spawn 处理器从未执行)。 - 部署产物核对:
out/host/index.js与本地 resfile 逐字节比对,确认补丁真的进包。
升级 ZCode 版本的清单
- 解包新 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后重建。 - 手工同步
web_engine/.../resfile/resources/app/config/provider/zcode-builtin.json(取自新 AppImage 的config/provider/;该文件不在自动复制范围内,不更新会导致模型列表过时)。 - 构建环境:
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]的必需补丁要按「未命中后果」逐条修锚点(换版本后锚点漂移是最常见的回归)。 - 装机后按上面的诊断手段核对:部署包内补丁字符串、
tasks-index.sqlite行数、 hilog 中ZCode agent process started/AppWindow --> hideAbility success、 以及基线补齐 task index 失败之类的新报错。 - 已知每次升级都要留意的耦合点:锚点补丁(glob/
node:sqlite/spawn/close→hide/re-fire/shiki-wasm→JS 引擎)、ZCODE_BUILTIN_PROVIDER_CONFIG_FILE等 env 名、agents/glmstaging 目录与文件名、 agent 参数面(app-server --stdio)、addon 的node:sqliteAPI 覆盖面、 引擎侧 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),
恢复完整着色。
残留:
- 任何只能用 wasm 的第三方模块在本移植版都无法工作(长期约束):docx/xlsx/pdf 预览 (duke_sheets_wasm / docx_wasm / pdfjs 的 openjpeg、qcms)、rive 动画等仍属不可用。 能换纯 JS 实现的(undici→undici-shim、shiki→JS 引擎)已逐个绕开。
- 仍会有 2 条/启动的
WebAssembly is not defined(unhandledRejection):诊断定位为 Electron 自身的net.fetch内部读全局Request(undici 懒加载),触发点是 app 的 遥测上报(sendReport→sendReportAttempt→session.fetch)。属无害(仅遥测发送失败); 要消除需给 Electron 内部伪造Request,风险大于收益,故保留。 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 -xok、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 卡片)。
改动:
electron/src/main/module.json5:EntryAbility.launchType从specified改singleton(specified下 key 不同即新建实例;实测构建产物已确认写入 singleton)。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.log3.8MB 持续增长。 utility 文件通道(第 13 层)仍是设备策略收紧时的回退。 - 部署后实测:界面正常、会话订阅/帧正常(
v4/conversation/subscribeack +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。