鸿蒙HarmonyOS HAR 包打包与分发实战 —— 从 assembleHar 到独立消费者验证
一、前言:从"仓库内能跑"到"外部能消费"
很多鸿蒙 SDK 开发者最初的打包流程是这样的:在 DevEco Studio 里点一下 Build,看到 BUILD SUCCESSFUL,就把 build/.../outputs/default/xxx.har 拷给别人。直到真正发布那天,问题才集中爆发:
-
HAR 拷给别人,import 报错:外部工程
import { AgentRuntime } from '@arkagent/core'报"找不到模块"。 -
依赖解析失败:
ohpm install报@arkagent/core@1.0.0找不到——因为你的包根本没发布到 OHPM 仓库。 -
嵌套依赖断裂:消费者引入了
@arkagent/eval,但 eval 内部声明"@arkagent/core": "1.0.0",ohpm 解析不了。 -
daemon 模式 SDK 报错:命令行构建偶发
00303217 Configuration Error,提示DEVECO_SDK_HOME无效,但明明环境变量设了。 -
本仓库编译通过,独立工程编译不过:因为仓库里有
file:../agent_core这种 monorepo 相对路径,外部根本没有这个目录。
根因只有一句话:"在本仓库里编译通过"和"作为独立产物被外部消费"是两件完全不同的事。前者依赖的是 monorepo 的源码相对路径,后者依赖的是 HAR 包内的二进制产物和包元数据。
ArkAgent 在阶段 9 发布前专门设计了 verify-har-consumer.sh,用 mktemp 建一个完全独立的临时工程,真实地编译一遍 HAR,就是为了关掉"假成功"这道门。本文把这套发布门禁的每个环节拆开讲清楚。
二、HAR 与 HAP:两种产物的本质区别
在动手打包之前,先把基础概念理清楚。HarmonyOS 工程有两类核心产物,它们的定位、内容、依赖关系完全不同。
2.1 一张表看清差异
|
维度 |
HAR(HarmonyOS Archive) |
HAP(HarmonyOS Ability Package) |
|---|---|---|
|
定位 |
SDK 二进制产物,供其他模块依赖 |
完整应用安装包,装到设备上 |
|
构建命令 |
|
|
|
模块类型 |
|
|
|
能否独立运行 |
❌ 不能,必须被 HAP 引入 |
✅ 能,安装即可启动 |
|
内容 |
ArkTS 字节码 |
应用代码 + 资源 + Ability 配置 + 签名 |
|
被谁依赖 |
被其他 HAR 或 HAP 通过 ohpm 引入 |
不被依赖,是终端产物 |
|
是否签名 |
否 |
是(debug/release 签名) |
|
典型产物 |
|
|
2.2 ArkAgent 的三模块产物
ArkAgent 工程有三个模块,对应两种产物:
┌─────────────────────────────────────────────────────────────┐
│ ArkAgent 工程 │
├─────────────────────────────────────────────────────────────┤
│ agent_core/ → agent_core.har (@arkagent/core@1.0.0) │
│ agent_eval/ → agent_eval.har (@arkagent/eval@1.0.0) │
│ entry/ → entry-default-signed.hap (示例 App) │
└─────────────────────────────────────────────────────────────┘
在 build-profile.json5 里,三个模块都被注册为 products 的构建目标:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true // ← HAR 消费的关键开关
}
}
}
]
},
"modules": [
{ "name": "entry", "srcPath": "./entry" },
{ "name": "agent_core", "srcPath": "./agent_core" },
{ "name": "agent_eval", "srcPath": "./agent_eval" }
]
}
关键点:
-
agent_core和agent_eval的hvigorfile.ts用的是harTasks(HAR 构建插件),而根工程和entry用的是appTasks/entryTasks。这决定了assembleHar能否产出 HAR。 -
useNormalizedOHMUrl: true是 HAR 能被外部正确解析的前提,它让模块路径采用规范化 OHM URL,避免大小写或路径格式差异导致消费失败。 -
targetSdkVersion: "6.1.1(24)"表示 API 24,这是 HAR 元数据里compatibleSdkVersion的来源。
2.3 模块的 hvigorfile 决定产物类型
HAR 模块和 HAP 模块的 hvigorfile.ts 内容不同,这是 hvigor 决定走哪条构建流水线的依据:
// agent_core/hvigorfile.ts 和 agent_eval/hvigorfile.ts —— HAR 模块
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks, // ← harTasks 才会产出 .har
plugins: []
}
// 根工程 hvigorfile.ts —— App 壳
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks,
plugins: []
}
如果一个模块的 hvigorfile.ts 里写的是 harTasks,hvigorw assembleHar 才会为它产出 HAR;如果写的是 entryTasks/featureTasks,那只有 assembleHap 会产出 HAP。混用会导致"命令跑了但没产物"的迷惑现象。
三、模块依赖铁律:core / eval / entry 的有向无环图
HAR 打包最容易翻车的地方不是构建命令,而是依赖方向。依赖方向错了,轻则循环依赖编译失败,重则 SDK 边界污染、产物膨胀。ArkAgent 把模块依赖钉死成一条单向链。
3.1 依赖方向图
┌──────────────────────┐
│ entry (HAP) │ ← 示例 App / 端到端验收
│ 烘焙助手 + 评测 UI │
└──────────┬───────────┘
┌────┴────┐
entry → core entry → eval(仅评测场景)
▲ │
│ eval → core
│ │
┌─────┴───────────┴─────┐
│ agent_core (HAR) │ ← Agent Runtime
│ LLM / 工具 / 状态 │ ★ 不得依赖 entry 或 eval
└───────────────────────┘
▲
│
┌─────────┴─────────────┐
│ agent_eval (HAR) │ ← 评估框架
│ Record/Replay/Metrics│ 依赖 core,但 core 不依赖它
└───────────────────────┘
三条铁律(来自 ADR-0001 模块边界 + ADR-0015 Eval 边界):
|
规则 |
允许 |
禁止 |
|---|---|---|
|
entry → core |
✅ |
—— |
|
entry → eval(仅评测场景) |
✅ |
—— |
|
eval → core |
✅ |
—— |
|
core → eval |
—— |
❌ 反向依赖 |
|
core → entry |
—— |
❌ 反向依赖 |
3.2 oh-package.json5 里的依赖声明
依赖方向直接体现在每个模块的 oh-package.json5 里。先看生产者(HAR 模块)的声明:
// agent_core/oh-package.json5 —— 不依赖任何 ArkAgent 模块
{
"name": "@arkagent/core",
"version": "1.0.0",
"description": "HarmonyOS local-first agent runtime: state, hooks, tool approval, loop detection, multimodal providers.",
"main": "src/main/ets/Index.ets",
"author": "ArkAgent",
"license": "MIT",
"dependencies": {} // ← 空,core 是底层
}
// agent_eval/oh-package.json5 —— 依赖 core
{
"name": "@arkagent/eval",
"version": "1.0.0",
"description": "ArkAgent evaluation, recording/replay, metrics, reporting and observability.",
"main": "src/main/ets/Index.ets",
"author": "ArkAgent",
"license": "MIT",
"dependencies": {
"@arkagent/core": "1.0.0" // ← eval 用 SemVer 声明对 core 的依赖
}
}
注意这里有个微妙之处:agent_eval 在仓库内部用 "@arkagent/core": "1.0.0" 声明依赖(SemVer 版本号),而不是 file:../agent_core。这是因为 SemVer 声明更符合"发布后的真实消费场景"——外部消费者也是按版本号理解的。但在仓库内部构建时,hvigor 会通过 monorepo 的模块注册把 1.0.0 解析到本地的 agent_core 模块。这个机制只在 monorepo 内有效,外部消费者直接拿 eval.har 用会遇到解析不了的问题(后面踩坑会讲)。
再看消费者(HAP 模块)的声明:
// entry/oh-package.json5 —— 用 file: 引用源码模块
{
"name": "entry",
"version": "1.0.0",
"dependencies": {
"@arkagent/core": "file:../agent_core", // ← monorepo 内部用源码路径
"@arkagent/eval": "file:../agent_eval"
}
}
entry 在仓库内部开发时用 file:../agent_core 直接引用源码模块,这样改 core 代码 entry 立即生效,不用每次重新打 HAR。但发布给外部消费者时绝不能用这种相对路径,因为外部根本没有 ../agent_core 这个目录。
3.3 自动化守卫:check-eval-deps.sh
依赖铁律不能只靠人记,必须有自动化检查兜底。ArkAgent 用一个简单的脚本扫描 agent_core/src 下所有 .ets 文件,发现任何对 @arkagent/eval 的引用就 fail:
#!/usr/bin/env bash
# scripts/check-eval-deps.sh
# Fail if agent_core imports agent_eval (dependency inversion check).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if rg -n "@arkagent/eval|agent_eval" "$ROOT/agent_core/src" --glob '*.ets' 2>/dev/null; then
echo "FAIL: agent_core must not depend on agent_eval" >&2
exit 1
fi
echo "OK: agent_core does not depend on agent_eval"
这个脚本是发布门禁的一部分。rg(ripgrep)扫描源码,一旦命中就立即退出非 0。设计上只检查 agent_core/src(core 的实现代码),不检查 eval——因为方向是单向的,只需要守住"core 不能反向依赖 eval"这一条。
为什么不用 hvigor 的依赖图检查?因为 hvigor 的依赖图只能识别 oh-package.json5 里声明的依赖,识别不了源码 import。理论上 core 可以在 dependencies 里不声明 eval,却在某个 .ets 文件里偷偷 import { EvalRunner } from '@arkagent/eval'——这种"未声明但 import"的情况在 ArkTS 编译期可能因为模块可见性而暂时不报错,但发布后会成为定时炸弹。源码扫描是最后一道防线。
四、assembleHar 命令行构建:每一步都有讲究
理解了产物和依赖,现在进入真正的构建环节。ArkAgent 的标准构建命令长这样:
# agent_core HAR 构建
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHar --no-daemon
# 标准 debug HAP 构建
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHap --no-daemon
# 全部本地单元测试
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw test --no-daemon
这三条命令在 2026-07-12 于 HarmonyOS 6.1.1 / API 24 工程上验证通过。看起来只是"跑个命令",但每个参数都有理由。
4.1 为什么是绝对路径的 hvigorw?
DevEco Studio 自带了 hvigor 工具链,位于 /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw。命令行构建必须显式指向这个路径,原因有两个:
-
普通 shell 没有 hvigorw 的 PATH:DevEco Studio 内部的终端会自动配置工具链路径,但外部 shell(iTerm、Terminal、CI runner)不会。
-
保证版本一致:用 IDE 内置的 hvigorw,保证命令行构建和 IDE 构建用的是同一个工具链版本,避免"IDE 能编译、命令行不能"的诡异差异。
如果你的 DevEco Studio 装在别的位置,调整这两个绝对路径即可,但 DEVECO_SDK_HOME 必须指向安装包内的 Contents/sdk。
4.2 ⚠️ 踩坑一:DEVECO_SDK_HOME 与 daemon 继承的无效变量
症状:首次在命令行跑 hvigorw assembleHap,Hvigor daemon 正常启动,但配置阶段返回 00303217 Configuration Error,提示 shell 里的 DEVECO_SDK_HOME 无效。明明环境变量已经 export 了,为什么还说无效?
根因:这是 HAR spike(docs/research/spikes/phase-00-har.md)阶段 0 就踩到的坑。Hvigor 默认用 daemon 模式加速构建——首次启动一个 daemon 进程,后续构建复用这个进程。问题在于:daemon 进程在首次启动时捕获了当时的环境变量快照。如果你第一次启动 daemon 时 DEVECO_SDK_HOME 没设(或指向旧路径),这个 daemon 就会一直带着错误的 SDK 路径。后续你再 export DEVECO_SDK_HOME=正确路径,当前 shell 是对的,但已有的 daemon 进程仍然用旧的环境变量快照。
DevEco Studio 内部已经正确配置了 SDK,但普通 shell 没有指向那个目录;旧 daemon 也可能继承错误环境。两者叠加,就出现了"环境变量设了但 daemon 不认"的现象。
修复:两个动作组合起来才能稳定解决:
# 1. 单次命令内显式设置 SDK 路径(不依赖 shell 的 export)
# 2. 加 --no-daemon,强制本次构建不复用任何已有 daemon
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHar --no-daemon
--no-daemon 的作用是:本次构建用一个全新的、临时的 hvigor 进程,它会读取当前命令行传入的环境变量,不继承任何历史 daemon 的状态。构建完进程就退出,不留下"脏 daemon"。
被拒绝的方案:
-
❌
kill掉所有 hvigor daemon 再构建:治标不治本,下次忘了又翻车,而且 daemon 进程不好精准识别。 -
❌ 把
DEVECO_SDK_HOME写进~/.zshrc永久 export:会让所有 HarmonyOS 工程(可能用不同 SDK 版本)共享同一个 SDK 路径,多版本切换时翻车。 -
❌ 只在 IDE 里构建:CI 跑不了,发布门禁脚本也没法复用。
显式环境变量 + --no-daemon 是最可靠的组合,ArkAgent 所有构建命令、所有发布门禁脚本统一采用这套写法。构建结果稳定为 BUILD SUCCESSFUL,约 6-7 秒。
4.3 三条命令的职责边界
|
命令 |
作用 |
产出 |
适用场景 |
|---|---|---|---|
|
|
编译 HAR 模块,打包成 .har |
|
发布 SDK、外部消费 |
|
|
编译 entry 模块,打包+签名成 .hap |
|
装机、端到端验收 |
|
|
跑本地单元测试(entry + core) |
测试报告 |
质量门禁 |
一个容易混淆的点:根目录跑 assembleHar 会同时为 agent_core 和 agent_eval 两个 HAR 模块产出 HAR,因为它们都注册在 build-profile.json5 的 modules 里。而 assembleHap 只会为 entry 产出 HAP。test 在根目录跑会同时执行 entry 和 agent_core 的本地测试——阶段 1 验证结果是 32 个 Core 用例全部通过。
4.4 产物路径
构建成功后,HAR 产物位于:
agent_core/build/default/outputs/default/agent_core.har
agent_eval/build/default/outputs/default/agent_eval.har
注意路径里有 default(product name)和 default(target name)两层,这对应 build-profile.json5 里的配置。debug/release 模式可能产出不同子路径,但 HAR 模块本身通常只配 default target。
实测产物大小(2026-07-13 第七轮构建):
|
HAR |
大小 |
说明 |
|---|---|---|
|
|
~404 KB |
Runtime 主体,含 LLM/工具/状态/记忆等 |
|
|
~154 KB |
评估框架,依赖 core |
core 比 eval 大是合理的——core 是 Runtime 主体,eval 是建立在 core 之上的评估层。
五、HAR 产物内部结构: HAR 里到底装了什么?
很多开发者把 HAR 当成一个"黑盒 zip",出了问题不知道往里看。其实 HAR 本质上是一个 gzip tar 包,解开后结构清晰。理解内部结构是诊断消费问题的前提。
5.1 解包看结构
# HAR 本质是 tar.gz
tar -xzf agent_core.har -C /tmp/core-inspect
解包后的目录结构:
package/
├── oh-package.json5 ← 包元数据(name/version/依赖/构建标志)
├── ets/
│ ├── modules.abc ← ArkTS 字节码(编译产物,核心)
│ └── sourceMaps.map ← source map(调试用)
├── src/main/
│ ├── module.json ← 模块清单(type: har)
│ └── ets/
│ ├── Index.d.ets ← 公共 API 类型声明(消费方 import 的入口)
│ ├── codec/DomainCodec.d.ets
│ ├── controller/AgentController.d.ets
│ ├── core/Json.d.ets
│ ├── domain/Message.d.ets
│ ├── runtime/AgentRuntime.d.ets
│ ├── tool/ToolRegistry.d.ets
│ └── ... ← 每个公共导出对应一个 .d.ets
└── libs/ ← HAR 依赖的 native so(如果有)
5.2 三个关键文件
|
文件 |
作用 |
消费方如何使用 |
|---|---|---|
|
|
ArkTS 编译后的字节码,HAR 的"可执行体" |
运行时由 OHM(OpenHarmony Module)加载 |
|
|
公共 API 的类型声明 |
编译期用于类型检查,消费方 |
|
|
包元数据(name/version/依赖/构建标志) |
ohpm 解析依赖、hvigor 决定构建行为 |
modules.abc 和 Index.d.ets 是 HAR 能被消费的两个充要条件——缺任何一个,消费者要么编译不过(缺声明),要么运行不了(缺字节码)。verify-har-consumer.sh 专门检查这两个文件的存在。
5.3 oh-package.json5 元数据:HAR 的"身份证"
解包后 HAR 内的 oh-package.json5 比源码里的丰富很多,多出来的是构建期注入的元数据。下面是 agent_core.har 解包后的真实内容(格式化后):
{
"name": "@arkagent/core",
"version": "1.0.0",
"description": "HarmonyOS local-first agent runtime: ...",
"author": "ArkAgent",
"license": "MIT",
"dependencies": {},
"types": "src/main/ets/Index.d.ets", // ← 类型声明入口
"artifactType": "obfuscation",
"metadata": {
"byteCodeHar": true, // ← 字节码 HAR(区别于源码 HAR)
"sourceRoots": ["./src/main"],
"debug": true,
"dependencyPkgVersion": {}, // ← 依赖版本快照
"declarationEntry": [],
"useNormalizedOHMUrl": true // ← 规范化 OHM URL
},
"compatibleSdkVersion": 23, // ← 对应 API 23 (6.1.0)
"compatibleSdkType": "HarmonyOS",
"obfuscated": false // ← 未混淆
}
agent_eval.har 的元数据类似,但 dependencies 和 dependencyPkgVersion 非空:
{
"name": "@arkagent/eval",
"version": "1.0.0",
"dependencies": {
"@arkagent/core": "1.0.0" // ← 嵌套依赖声明
},
"metadata": {
"byteCodeHar": true,
"dependencyPkgVersion": {
"@arkagent/core": "1.0.0" // ← 依赖版本快照
},
"useNormalizedOHMUrl": true
},
"compatibleSdkVersion": 23
}
关键元数据解读:
|
字段 |
含义 |
消费影响 |
|---|---|---|
|
|
HAR 内是字节码而非源码 |
消费方无需重新编译 HAR 内部代码,只编译自己的代码 |
|
|
模块路径规范化 |
避免大小写/路径格式导致的解析失败 |
|
|
兼容的最低 API 版本 |
消费方工程的 SDK 不能低于此版本 |
|
|
HAR 构建时的依赖版本快照 |
消费方需提供兼容版本(这里是 core@1.0.0) |
|
|
未混淆 |
调试友好;release 发布可考虑混淆 |
byteCodeHar: true 是个重要信号——它意味着 HAR 里装的是 ArkTS 字节码(.abc),消费方编译时不会重新编译 HAR 内部的代码,只编译消费方自己的代码并链接到 HAR 的字节码。这让 HAR 消费"又快又稳":快是因为不用重编译 SDK,稳是因为 SDK 的字节码是生产者锁定的,消费方环境差异不会影响 SDK 内部行为。
六、本地依赖声明:file:./libs 的正确写法
构建出 HAR 后,下一步是让消费方(你的 App 工程)能用上它。在鸿蒙里,本地 HAR 依赖用 file: 协议声明。
6.1 monorepo 内部 vs 外部消费的两种写法
ArkAgent 在仓库内部和外部消费用了两种不同的 file: 写法,对应两种场景:
场景 A:monorepo 内部开发(entry 消费源码模块)
entry/oh-package.json5:
"@arkagent/core": "file:../agent_core" ← 引用源码目录,改 core 立即生效
场景 B:外部工程消费(独立 App 引用 HAR 产物)
MyApp/entry/oh-package.json5:
"@arkagent/core": "file:./libs/agent_core.har" ← 引用 HAR 文件
为什么 monorepo 内部用源码目录而不是 HAR? 因为开发期 core 还在频繁改动,每次改完重新打 HAR 太慢。file:../agent_core 直接引用源码模块,core 改一行 entry 立即重新编译生效。这种"源码依赖"只在 monorepo 内有效,因为 hvigor 通过 build-profile.json5 的 modules 注册表知道 ../agent_core 是个本地模块。
为什么外部消费必须用 HAR 文件? 因为外部工程没有 agent_core 源码目录,file:../agent_core 会指向一个不存在的路径。外部消费方拿到的就是一个 .har 文件,必须用 file:./libs/agent_core.har 指向它。
6.2 外部消费的完整接入步骤
MyHarmonyApp/
└── entry/
├── libs/
│ └── agent_core.har ← 把 HAR 复制到这里
├── oh-package.json5 ← 声明 file:./libs/agent_core.har
└── src/
entry/oh-package.json5:
{
"name": "entry",
"version": "1.0.0",
"dependencies": {
"@arkagent/core": "file:./libs/agent_core.har"
}
}
安装依赖:
/Applications/DevEco-Studio.app/Contents/tools/ohpm/bin/ohpm install
验证接入(能编译通过就说明 HAR 接入成功):
import { AgentRuntime } from '@arkagent/core'
6.3 同时消费 core 和 eval 的嵌套依赖问题
如果外部工程同时需要 core 和 eval,会撞到嵌套依赖问题。eval 的 oh-package.json5 内部声明了:
"dependencies": {
"@arkagent/core": "1.0.0"
}
ohpm 在解析 eval 的依赖时,会尝试从 OHPM registry 下载 @arkagent/core@1.0.0——但你的包根本没发布到 OHPM,于是解析失败。这是 HAR 分发里最经典的坑,下一章详细讲。
七、独立消费者验证:verify-har-consumer.sh 的设计哲学
这是本文最重要的一章。ArkAgent 阶段 9 发布前专门重写了 verify-har-consumer.sh,用独立临时工程真实编译 HAR。这个脚本是发布门禁的核心,它的设计哲学直接回答了"HAR 到底能不能用"这个问题。
7.1 为什么需要独立消费者验证?
|
验证层次 |
能发现什么问题 |
不能发现什么问题 |
|---|---|---|
|
本仓库 |
HAR 能产出、本仓库编译无错 |
外部消费时的依赖解析、嵌套依赖、路径问题 |
|
本仓库 |
源码 API 正确 |
外部工程没有 |
|
独立消费者真实编译 |
上述所有问题 + 嵌套依赖 + ohpm 解析 + 路径隔离 |
—— |
只有独立消费者验证才能覆盖"本仓库编译通过但外部用不了"的所有情况。因为本仓库里有 monorepo 的源码路径兜底,掩盖了 HAR 作为独立产物的真实状态。
7.2 ⚠️ 踩坑二:verify-har 的"假成功"
症状:阶段 9 第六轮验收前,verify-har-consumer.sh 报"通过",但 Codex 复查时发现消费者根本没真正编译——ohpm install 实际上失败了,脚本却用 manual symlink 绕过失败,然后只检查了 modules.abc 文件存在就判定成功。
根因:旧版脚本的逻辑是:
-
ohpm install失败 → 手动ln -s创建符号链接到 oh_modules -
只检查
oh_modules/@arkagent/core/ets/modules.abc文件存在 -
判定"成功"
这套逻辑的问题:ohpm install 失败意味着依赖图本身有问题(比如嵌套依赖解析不了),manual symlink 只是让"文件存在",并没有真正解决依赖关系。更严重的是,symlink 可能指向 monorepo 源码(../agent_core),这等于让消费者偷偷用了源码而不是 HAR 产物——完全违背了"验证 HAR 能被消费"的初衷。只查 modules.abc 存在更是形同虚设,文件在 HAR 里本来就有,复制过去就在了,跟"能否编译"毫无关系。
修复:第七轮重写脚本,三条硬约束:
-
ohpm install 失败即失败,没有任何 manual symlink fallback 成功路径。
-
必须跑真实的
hvigor assembleHar编译消费代码(import { AgentRuntime }/EvalRunner),BUILD SUCCESSFUL才算通过。 -
检查 oh_modules 解析结果不能指向 monorepo 源码(
/agent_core、/agent_eval路径直接 fail)。
# ohpm install 失败即失败——无 fallback
set +e
(
cd "$TMP"
"$OHPM_BIN" install
cd "$TMP/consumer"
"$OHPM_BIN" install
) > "$TMP/ohpm-out.txt" 2>&1
OHPM_RC=$?
set -e
if [[ $OHPM_RC -ne 0 ]]; then
log "FAIL: ohpm install failed (rc=$OHPM_RC) — no symlink fallback"
exit 1
fi
# 真实编译——必须 BUILD SUCCESSFUL
set +e
(
cd "$TMP"
"$HVIGORW" assembleHar --no-daemon
) > "$TMP/hvigor-out.txt" 2>&1
HV_RC=$?
set -e
if [[ $HV_RC -ne 0 ]]; then
log "FAIL: independent consumer hvigor assembleHar failed (rc=$HV_RC)"
exit 1
fi
if ! grep -q "BUILD SUCCESSFUL" "$TMP/hvigor-out.txt"; then
log "FAIL: hvigor finished without BUILD SUCCESSFUL"
exit 1
fi
# 解析结果不能指向 monorepo 源码
for pkg in core eval; do
target="$TMP/consumer/oh_modules/@arkagent/$pkg"
if [[ -L "$target" ]]; then
resolved="$(readlink "$target")"
case "$resolved" in
*"/agent_core"|*"/agent_core/"*|*"/agent_eval"|*"/agent_eval/"*)
log "FAIL: resolved to monorepo source"
exit 1
;;
esac
fi
done
7.3 脚本全流程拆解
verify-har-consumer.sh 完整流程分 8 步:
┌─────────────────────────────────────────────────────────────┐
│ Step 1: mktemp 建独立临时工程(/tmp/arkagent-har-consumer-XXXXXX)│
│ trap EXIT 时 rm -rf,绝不在 repo 落 vendor HAR │
├─────────────────────────────────────────────────────────────┤
│ Step 2: monorepo assembleHar 产出两个 HAR │
│ find agent_core/build & agent_eval/build 找到 .har │
├─────────────────────────────────────────────────────────────┤
│ Step 3: tar -xzf 解包 HAR → vendor_pkgs/{core,eval} │
│ 检查 modules.abc + Index.d.ets + name@1.0.0 元数据 │
├─────────────────────────────────────────────────────────────┤
│ Step 4: 改写 eval 嵌套依赖 │
│ eval/oh-package.json5 里 "@arkagent/core":"1.0.0" │
│ → "@arkagent/core":"file:../core" (可复现改写) │
├─────────────────────────────────────────────────────────────┤
│ Step 5: scaffold 临时 Harmony 工程 │
│ build-profile.json5 + AppScope + consumer 模块 │
│ consumer 依赖 file:../vendor_pkgs/{core,eval} │
├─────────────────────────────────────────────────────────────┤
│ Step 6: ohpm install(root + consumer) │
│ 失败即 fail,无 symlink fallback │
├─────────────────────────────────────────────────────────────┤
│ Step 7: hvigor assembleHar(独立消费者真实编译) │
│ 编译 import { AgentRuntime, EvalRunner } 代码 │
│ BUILD SUCCESSFUL 才算通过 │
├─────────────────────────────────────────────────────────────┤
│ Step 8: 证据日志(剥离 $ROOT/$HOME/$TMP 绝对路径) │
│ temp 目录 trap 删除,repo 不留 vendor HAR │
└─────────────────────────────────────────────────────────────┘
7.4 嵌套依赖改写:解决"未发布 SemVer"的关键技巧
这是整个脚本里最精妙的一步。eval 的 HAR 内部声明了对 core 的 SemVer 依赖:
// eval HAR 内的 oh-package.json5
"dependencies": {
"@arkagent/core": "1.0.0" // ← SemVer,但 core 没发布到 OHPM
}
ohpm 看到这个声明会去 OHPM registry 找 @arkagent/core@1.0.0,找不到就失败。脚本用一段 Python 把这个声明改写成相对路径:
# 改写 eval 的 oh-package.json5
p = Path(sys.argv[1]) # vendor_pkgs/eval/oh-package.json5
t = p.read_text()
t2, n = re.subn(
r'"@arkagent/core"\s*:\s*"[^"]*"',
'"@arkagent/core": "file:../core"',
t,
)
if n < 1:
raise SystemExit("FAIL: could not rewrite @arkagent/core dependency")
p.write_text(t2)
改写后:
// eval HAR 内的 oh-package.json5(改写后)
"dependencies": {
"@arkagent/core": "file:../core" // ← 指向同级的 vendor_pkgs/core
}
为什么这么改?因为 vendor_pkgs 目录结构是:
vendor_pkgs/
├── core/ ← 解包自 agent_core.har 的 package/
└── eval/ ← 解包自 agent_eval.har 的 package/
eval 用 file:../core 就能解析到同级的 core 目录,无需 OHPM registry。这个改写是可复现的——脚本每次运行都做同样的替换,不依赖任何外部状态。这也是为什么脚本要求 n >= 1(至少替换一处),如果替换数为 0 说明 eval 的依赖声明格式变了,脚本会 fail 提醒维护者更新。
这个改写技巧也是外部消费者的推荐做法。docs/api/versioning.md 明确写了外部工程的标准步骤:
# 外部工程消费 HAR 的推荐步骤
mkdir -p vendor_pkgs
# tar -xzf *.har 后将 package/ 拷入 vendor_pkgs/{core,eval}
# 改写 eval/oh-package.json5 中 @arkagent/core → file:../core
# consumer oh-package.json5:
# "@arkagent/core": "file:../vendor_pkgs/core",
# "@arkagent/eval": "file:../vendor_pkgs/eval"
ohpm install
hvigorw assembleHar --no-daemon # 或 assembleHap
7.5 消费者的编译表面(compile surface)
临时工程的 consumer/src/main/ets/Index.ets 不是空文件,它真实 import 了 core 和 eval 的公共 API:
/**
* Minimal consumer compile surface: public APIs from packaged HARs only.
* Must not resolve monorepo source paths (../agent_core).
*/
import { AgentRuntime, ModelConfig, JsonObject } from '@arkagent/core'
import {
EvalRunner, EvalRunConfig, EchoHarnessFactory, EvalEnvironment, FakeEvalClock
} from '@arkagent/eval'
export function consumerSmoke(): string {
const env = new EvalEnvironment(new FakeEvalClock())
const runner = new EvalRunner(env, new EchoHarnessFactory())
const cfg = new EvalRunConfig('consumer-smoke', 1, 1000)
void runner
void cfg
void AgentRuntime
void ModelConfig
void JsonObject
return 'har-consumer-ok'
}
这段代码的设计意图:
-
覆盖 core 和 eval 两个包:任一包的导出有问题都会编译失败。
-
import 真实类名:
AgentRuntime、EvalRunner等是 SDK 的核心公共 API,能编译通过说明Index.d.ets类型声明正确。 -
实例化对象:不只 import,还要
new出来,确保字节码链接正常(不只是声明对,运行时符号也对)。 -
注释明确"不得解析 monorepo 源码路径":这是给脚本里的路径守卫检查做语义注解。
7.6 证据可移植性:剥离绝对路径
脚本最后一步是证据日志处理。CI 环境和开发者本机的路径不同(/Users/wangzhe/... vs /home/runner/...),如果证据日志里带绝对路径,CI 上无法复现对比。脚本用 log() 函数把三个敏感路径替换成占位符:
log() {
local line="$1"
line="${line//$ROOT/\$ROOT}" # 仓库根 → $ROOT
line="${line//$HOME/\$HOME}" # 用户家目录 → $HOME
line="${line//$TMP/\$TMP}" # 临时目录 → $TMP
echo "$line" | tee -a "$EVIDENCE_LOG"
}
这样证据日志在任何机器上都是一致的格式,便于 CI 比对和 issue 复现。这也是阶段 9 第六轮踩坑"证据含本机绝对路径"的修复。
八、版本管理:SemVer 与 1.0 封板
HAR 是产物,版本是产物的"身份"。ArkAgent 用 SemVer 管理版本,1.0.0 是首个正式封板的版本。
8.1 SemVer 策略
|
版本号变化 |
触发条件 |
示例 |
|---|---|---|
|
MAJOR(x.0.0) |
|
删除公共类、改方法签名 |
|
MINOR(1.x.0) |
向后兼容的新功能 |
新增工具、新增 Provider Profile |
|
PATCH(1.0.x) |
bug 修复、文档、非破坏性内部重构 |
修复流式解码 bug |
1.0.0 冻结了 docs/api/ 描述的公共 API 表面和包 Index 导出。这意味着 1.0.x 的 patch 更新不会动公共 API,消费者可以放心升级。
8.2 版本声明的三个位置
版本号不是只在 HAR 里出现,它需要三个位置保持一致:
|
位置 |
声明 |
作用 |
|---|---|---|
|
|
|
源码模块版本,构建时注入 HAR |
|
HAR 内 |
|
HAR 产物的版本(构建期注入) |
|
|
|
eval 对 core 的依赖版本 |
构建期,hvigor 会把源码模块的 version 注入到 HAR 的元数据里。verify-har-consumer.sh 专门检查 HAR 内的版本:
name_expect="@arkagent/$side"
if ! grep -q "$name_expect" "$TMP/vendor_pkgs/$side/oh-package.json5" || \
! grep -q '1.0.0' "$TMP/vendor_pkgs/$side/oh-package.json5"; then
log "FAIL: $name_expect@1.0.0 not found in package metadata"
exit 1
fi
8.3 1.0 发布的完整门禁
ArkAgent 阶段 9(1.0 发布)的验收门禁表(2026-07-13 Codex 第七轮终验):
|
门禁项 |
结果 |
|---|---|
|
clean + assembleHar |
通过 |
|
root test |
通过(BUILD SUCCESSFUL) |
|
assembleHap |
通过 |
|
check-eval-deps.sh |
OK(core 不依赖 eval) |
|
verify-har-consumer.sh |
真实 compile OK(mktemp + ohpm + hvigor assembleHar BUILD SUCCESSFUL) |
|
scan-hap-secrets.sh |
OK clean(HAP 无 API Key 泄漏) |
|
git diff --check |
干净 |
|
用户功能在线/真机 |
2026-07-13 已完成 |
|
发布安全抽检(Codex) |
通过 |
注意门禁里有两个构建验证:assembleHar(本仓库能产出 HAR)和 verify-har-consumer.sh(外部能消费 HAR)。两者都通过才允许封板。这正是"本仓库编译通过 ≠ 外部能用"这条红线的工程化落地。
九、最佳实践清单
9.1 模块依赖
-
✅ core 是底层,不依赖任何 ArkAgent 模块。
-
✅ eval 依赖 core,用 SemVer 声明(
"@arkagent/core": "1.0.0")。 -
✅ entry 可依赖 core 和 eval,monorepo 内用
file:../module。 -
✅ 用
check-eval-deps.sh源码扫描兜底"未声明但 import"的反向依赖。 -
❌ core 不得 import 任何
@arkagent/eval符号。 -
❌ 不要在 HAR 模块的
dependencies里写file:相对路径(发布后外部解析不了)。
9.2 构建命令
-
✅ 命令行构建显式设置
DEVECO_SDK_HOME指向Contents/sdk。 -
✅ 所有构建命令统一加
--no-daemon。 -
✅ 用 IDE 内置的绝对路径
hvigorw,保证工具链版本一致。 -
✅ HAR 模块的
hvigorfile.ts用harTasks,HAP 模块用对应 task。 -
❌ 不要依赖 shell 的
export DEVECO_SDK_HOME(daemon 继承问题)。 -
❌ 不要混用不同来源的 hvigorw(IDE 内置 vs npm 全局安装)。
9.3 HAR 消费
-
✅ 外部消费用
file:./libs/xxx.har指向 HAR 文件。 -
✅ 同时消费 core + eval 时,改写 eval 的嵌套依赖为
file:../core。 -
✅ 消费方工程
build-profile.json5开启useNormalizedOHMUrl: true。 -
✅ 消费方 SDK 版本不低于 HAR 的
compatibleSdkVersion。 -
❌ 外部工程不要用
file:../agent_core(那是 monorepo 内部路径)。 -
❌ 不要假设 HAR 内有源码(
byteCodeHar: true意味着只有字节码)。
9.4 发布门禁
-
✅ 发布前跑
verify-har-consumer.sh,独立工程真实编译。 -
✅ ohpm install 失败即 fail,禁止 manual symlink fallback。
-
✅ 检查 oh_modules 解析不指向 monorepo 源码路径。
-
✅ 证据日志剥离
$ROOT/$HOME/$TMP绝对路径。 -
✅ temp 工程 trap 删除,repo 不留 vendor HAR。
-
❌ 不要只检查
modules.abc文件存在就判定成功。 -
❌ 不要把临时工程的 vendor HAR 提交进 repo。
9.5 版本管理
-
✅ SemVer:MAJOR 破坏性 / MINOR 新功能 / PATCH 修复。
-
✅ 源码 oh-package.json5 的 version 与 HAR 内 version 一致。
-
✅ 1.0.0 封板后 patch 不动公共 API。
-
✅ 依赖声明用精确 SemVer(
1.0.0),不用^/~范围。 -
❌ 不要在 HAR 的 dependencies 里留未发布版本号(外部解析不了)。
十、常见错误对照表
|
错误做法 |
问题 |
正确做法 |
|---|---|---|
|
外部工程用 |
外部没有源码目录,解析失败 |
用 |
|
构建命令省略 |
daemon 继承无效 SDK 路径, |
命令行显式设置 + |
|
HAR 模块 |
|
用 |
|
ohpm install 失败后手动 symlink |
掩盖依赖图问题,假成功 |
失败即 fail,修复依赖声明 |
|
只检查 |
文件复制过去就在了,跟编译无关 |
跑真实 |
|
eval HAR 内留 |
外部 ohpm 找不到 registry 包 |
改写为 |
|
core 源码偷偷 import |
反向依赖,发布后污染边界 |
|
|
消费方关闭 |
模块路径大小写/格式差异导致解析失败 |
开启 |
|
证据日志含 |
CI 无法复现对比 |
剥离为 |
|
monorepo 内 entry 用 HAR 而非源码 |
改 core 要重新打 HAR,开发慢 |
开发期用 |
|
把临时 vendor HAR 提交进 repo |
repo 膨胀,版本混乱 |
temp 工程 trap 删除 |
|
HAR |
外部消费者路径不存在 |
用 SemVer,外部消费时改写 |
|
|
|
在 |
十一、验证清单(发布前 Review)
模块依赖
-
agent_core/oh-package.json5的dependencies为空或不含 ArkAgent 模块 -
agent_eval/oh-package.json5声明"@arkagent/core": "1.0.0" -
check-eval-deps.sh输出OK: agent_core does not depend on agent_eval -
依赖方向是单向的:entry → core/eval,eval → core
HAR 构建
-
assembleHar --no-daemon产出agent_core.har和agent_eval.har -
HAR 路径
build/default/outputs/default/*.har -
HAR 解包后有
ets/modules.abc+src/main/ets/Index.d.ets -
HAR 内
oh-package.json5的version是预期版本 -
HAR 内
metadata.byteCodeHar为true
独立消费者验证
-
verify-har-consumer.sh在mktemp临时工程跑通 -
ohpm install 成功(无 symlink fallback)
-
hvigor assembleHar
BUILD SUCCESSFUL -
oh_modules 解析不指向
/agent_core或/agent_eval -
证据日志无绝对路径(
$ROOT/$HOME/$TMP占位符) -
temp 目录已清理,repo 无 vendor HAR 残留
HAP 构建
-
assembleHap --no-daemon产出签名 HAP -
scan-hap-secrets.sh无 API Key 命中 -
HAP 内无
provider_keys.local.json等敏感文件
版本一致性
-
源码
oh-package.json5version == HAR 内 version -
eval 对 core 的依赖版本与 core 实际版本一致
-
changelog 与版本号对应
十二、构建验证
12.1 HAR 构建
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHar --no-daemon
构建结果:
> hvigor version: ...
> hvigor assembleHar --no-daemon
...
CompileArkTS passed
PackageHar passed
BUILD SUCCESSFUL
产物:
agent_core/build/default/outputs/default/agent_core.har (~404 KB)
agent_eval/build/default/outputs/default/agent_eval.har (~154 KB)
12.2 HAP 构建
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHap --no-daemon
构建结果:
CompileArkTS passed
PackageHap passed
BUILD SUCCESSFUL
12.3 独立消费者验证
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
bash scripts/verify-har-consumer.sh
关键输出(证据日志,路径已脱敏):
verify-har-consumer 2026-07-13T...Z
mode=real-hvigor-compile temp=$TMP
==> assembleHar (monorepo producers)
OK: found agent_core.har and agent_eval.har
OK: CORE_SIZE=... EVAL_SIZE=...
OK: @arkagent/core package surface (modules.abc + Index.d.ets + 1.0.0)
OK: @arkagent/eval package surface (modules.abc + Index.d.ets + 1.0.0)
OK: eval declares @arkagent/core (file:../core after rewrite)
OK: consumer depends only on file:../vendor_pkgs/* from extracted HARs
==> ohpm install (consumer module)
ohpm: ...
==> hvigor assembleHar (independent consumer)
hvigor: ...
hvigor: BUILD SUCCESSFUL
OK: consumer assembleHar produced output HAR
OK: verify-har-consumer completed (real compile)
12.4 依赖方向守卫
bash scripts/check-eval-deps.sh
输出:
OK: agent_core does not depend on agent_eval
12.5 修复迭代记录
阶段 9 的 HAR 消费者验证经历了多轮修复,每轮的失败原因和修复:
|
轮次 |
失败现象 |
根因 |
修复 |
|---|---|---|---|
|
早期 |
verify-har 报通过但外部不能用 |
ohpm 失败后 symlink + 只查 abc 文件存在 |
重写脚本:ohpm 失败即 fail + 真实 hvigor 编译 |
|
早期 |
ohpm 解析 |
core 未发布到 OHPM registry |
解包后改写 eval 依赖为 |
|
早期 |
证据日志含 |
log 函数直接写绝对路径 |
|
|
早期 |
temp 目录的 vendor HAR 残留进 repo |
无清理机制 |
|
|
第七轮 |
全部门禁通过 |
—— |
1.0 封板 |
十三、写在最后
HAR 打包与分发表面上是"跑一条 assembleHar 命令",背后却是一整套工程纪律:模块依赖要有向无环、构建环境要显式隔离、HAR 产物要内部自洽、消费验证要独立真实、版本管理要可追溯。任何一环松懈,都会在发布那天变成"本仓库能跑但外部用不了"的黑洞。
ArkAgent 1.0 的发布门禁之所以把 verify-har-consumer.sh 列为硬性门禁,是因为我们吃过"假成功"的亏——ohpm 失败被 symlink 掩盖、文件存在被当成编译通过。这种假成功比明确的失败危险十倍,因为它会让团队误以为可以发布。真正的发布信心来自"在完全没有 monorepo 上下文的临时工程里,真实编译通过"。
记住这几条口诀:
HAR 是字节码包,abc 加上 d.ets,缺一不可消费。 HAP 是安装包,签名才能装机,HAR 永不独立跑。 依赖方向单向流,core 不靠 eval,eval 只能向上求。 构建必带 SDK 路径,no-daemon 斩旧环境,daemon 不背历史锅。 本仓编译不算数,独立工程真编译,ohpm 失败即失败。 嵌套依赖巧改写,SemVer 变 file:,未发布也能消费。 证据剥离绝对路径,temp 清理不留痕,CI 处处可复现。 SemVer 三段义,major 破 minor 新 patch 修,1.0 封板不动 API。
本文是 ArkAgent 鸿蒙教程系列的第三篇。前两篇讲了架构全景和快速接入,这篇聚焦"把 SDK 打包出去给别人用"这个发布维度。当你能稳定地用一条命令产出 HAR、用独立消费者验证它真能用、用 SemVer 管理它的演进——你的鸿蒙 SDK 就具备了真正可分发的工程基础。
更多推荐





所有评论(0)