开源鸿蒙平台 CMP 三方库「LlmTypewriter」适配全流程
本文记录
LlmTypewriter接入 OpenHarmony 的完整过程,覆盖 Compose 源码集盘点、零依赖引擎下沉、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 四 Tab 页面、HAP 构建和真机验收。
LlmTypewriter是面向 LLM 应用的 Compose Multiplatform 流式打字机库:把大模型输出的Flow<String>token 流渲染成逐字符揭示的打字机效果,配上渐进式 Markdown(prefix-stable 解析器)、语法高亮代码块、三种速度曲线(linear / easeOut / natural)、闪烁光标、点击跳过、中途停止/恢复与无障碍 live region。本次适配与 KStore 的"真编译整库"路线(kotlinx ohos 制品线齐备,引擎原样编译)有本质区别:LlmTypewriter是 Compose UI 库,11 个源文件里 9 个依赖 Compose(含 material3),而当前工具链线(HarmonyOS Kotlin2.2.21-1.0.0,eazytec 私服上没有 ohos 的 kotlinx-coroutines 制品)编不动任何 Compose 代码。因此走的是社区早期已过审的 "引擎下沉 + composeMain 隔离"路线(qrose / haze / PreCompose / voyager 同款):把纯 Kotlin 的核心引擎——状态机(TypewriterEngine)、速度曲线(SpeedCurve)、流式 Markdown 解析器(MarkdownStreamParser)、代码高亮 tokenizer(CodeTokens)——留在commonMain并保持零外部依赖,9 个 Compose UI 文件原样移入src/composeMain(不注册源集、不编译、不删除),鸿蒙侧通过 Kotlin/Native 动态库 +@CNameC ABI + CMake N-API 把引擎暴露给 ArkTS,ArkUI 四 Tab 页面作为验收宿主。一个真实的流式打字机状态机运行在真机上:ArkTS 喂 token、引擎逐字吐字、SpeedCurve 决定节奏、解析器实时渲染 Markdown。
项目地址: AtomGit/oh-tpc/llm-typewriter.git
上游仓库: NadeemIqbal/llm-typewriter
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
LlmTypewriter 为 Compose Multiplatform 应用提供"LLM 流式文本 → 打字机渲染"这一件事的完整答案:StreamingTypewriter(tokens: Flow<String>) 一个组件接住 token 流,StreamingTypewriterState 是唯一的真相源——token 流只往 appendToken 推字符,揭示循环按 SpeedCurve 给出的节奏从缓冲区逐字拉出,这个分离让状态机能扛住重组、支持 headless 测试;TypewriterRenderer 把揭示文本交给 Plain 或 Markdown 渲染器,MarkdownStreamParser 是 prefix-stable 的(同一输入前缀永远产生同一 token 前缀,只有尾部未闭合 token 随流重分类),所以流式渲染不闪烁;CodeHighlighter 是无正则的单遍扫描器,覆盖 Kotlin/JS/Python 的关键字、字符串、数字与注释;TypewriterCursor 提供方块/竖线/下划线光标,ThinkingDots 是思考指示器。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。鸿蒙上的 LLM 应用同样需要这套流式渲染能力,但上游构建(Kotlin 2.3.21 / Compose 1.10.3 / Gradle 9.5.0 / AGP 9.2.0)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以滚动打字,却无法证明打字机状态机、速度曲线与流式解析器在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.21 / Compose 1.10.3 / Gradle 9.5.0 与鸿蒙工具链无法共存于一个 Gradle 构建;HarmonyOS Kotlin 2.2.21-1.0.0 编不了 Compose 1.10.3,必须降 Gradle 8.14.1。 |
| UI 与引擎同源 | 11 个 commonMain 文件里 9 个 import Compose(含 mutableStateOf、SpanStyle、material3),状态机 StreamingTypewriterState 的算法与 Compose 观察层写在同一个类里。 |
| 依赖矩阵 | 引擎侧 Helpers.kt 依赖 kotlinx-coroutines;eazytec 2.2.21 线上没有 ohos 的 coroutines/serialization 制品(Nexus 检索为空)。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 流式语义跨不了桥 | Flow<String> 收集、LaunchedEffect 揭示循环是 Compose 侧概念,C ABI 只能同步一来一回。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:纯 Kotlin 引擎的下沉与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。打字与解析语义完全由上游原版算法负责(引擎孪生体用同一套断言钉住),ArkTS 只负责喂 token、调 tick、解析 JSON 和展示结果。
1.2 库提供的能力
Demo 提供一个真实的流式打字机会话,对应桥接的全部入口:
- 喂入(append):
engine.appendToken(token)把 token 推进揭示缓冲——ArkTS 定时器模拟 LLM 按 token 出字(拉丁整词、CJK 两字一组); - 揭示(tick):每 tick 揭示一个字符并返回
nextDelayMs——节奏来自上游SpeedCurve原版实现(句末标点 6×、逗号分号 3×、换行 5×、空白 1.4×); - 控制(stop/resume/skip/reset):中途停止、继续、点击跳到缓冲尾、重来——对应上游
StreamingTypewriterState的完整控制面; - 解析(parse):对已揭示文本实时跑上游 prefix-stable Markdown 解析器,ArkUI Span 体系渲染粗体/斜体/行内代码/删除线/链接/标题/围栏代码块;
- 高亮(highlight):对已揭示代码实时跑上游 tokenizer,按区间上色(Kotlin/JS/Python 三语言切换);
- 版本握手(info):返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
revealed / phase / pending | 已揭示文本 / 五态相位(Idle/Revealing/Waiting/Done/Stopped)/ 未揭示字符数。 |
char / nextDelayMs | tick 返回本次揭示的字符与下次应等待的毫秒。 |
tokens / spans | parse / highlight 返回的语义化渲染数据。 |
sourceComplete / isStreaming / softCap | 源完成标志 / 是否在播 / 缓冲软上限。 |
引擎侧完整能力(缓冲软上限、相位迁移、跳过/停止/恢复、prefix-stable 解析、无正则高亮)全部随编译进入 .so,JVM 侧保留 53 项 jvmTest 交叉验证用例(其中 TypewriterEngineTest 与上游 StreamingTypewriterStateTest 同一套断言),证明 ohos .so 里执行的打字机逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 纯 Kotlin 文件(SpeedCurve / MarkdownStreamParser)原样保留在 commonMain;状态机算法以 Compose-free 孪生体(TypewriterEngine)1:1 下沉,TypewriterPhase 枚举、代码 tokenizer 从 Compose 文件中原样抽取;Compose UI 9 文件移入 src/composeMain 不编译不删除。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 sample/*(Android/iOS/桌面/Web 示例)留盘不进构建。 |
| 桥接稳定 | 单个 C ABI + UTF-8 JSON(11 个 op 一口进),不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | commonMain 零外部依赖(无 Compose、无 coroutines)——这是 eazytec 2.2.21 线上 kotlinx 制品缺位下的唯一解;桥的 JSON 编解码手写 MiniJson。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:流式(曲线/速度/停止/跳过)、Markdown、代码高亮、状态。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
LlmTypewriter的价值分层里,只有引擎层能(也应该)跨到鸿蒙;ArkTS Demo 把桥暴露的打字机闭环画出来,等社区 CMP 工具链对齐后composeMain的 9 个文件原样接回。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点 11 个源文件的 Compose 依赖度、确认 eazytec 2.2.21 线制品边界、确定交付物
第 2 阶段:源码集手术 ── 9 个 Compose 文件 git mv 进 composeMain、TypewriterPhase/CodeTokens 抽取、TypewriterEngine 孪生下沉
第 3 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 钉版、LlmTypewriter 公共入口
第 4 阶段:桥协议建模 ── 11 个 op 的 JSON 契约、tick 揭示语义、错误通道
第 5 阶段:原生桥接 ── @CName C ABI、手写 MiniJson、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 四 Tab 页面、真机运行、ArkTS 严格模式排雷和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
llm-typewriter/ 核心库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
llm-typewriter/src/commonMain/ 引擎层:SpeedCurve / MarkdownStreamParser(上游原样)
+ TypewriterEngine / TypewriterPhase / CodeTokens / LlmTypewriter(本次下沉与新增)
llm-typewriter/src/composeMain/ 上游 Compose UI 9 文件(不注册源集,不编译,不删除)
llm-typewriter/src/commonTest/ 53 项交叉验证(4 个测试类,JVM 跑)
llm-typewriter/src/composeTest/ 上游 StreamingTypewriterStateTest 原样保留(不编译)
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 四 Tab 页面
sample/ 上游四平台示例,留盘不进 settings
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份,中文主文档 25 节)
README.upstream.md 上游原文保存
docs/ hero.gif + 本文 + img/ 效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :llm-typewriter 与 :example:nativeApp,rootProject.name = "ohos_llm_typewriter"。上游的 android / compose / vanniktech 发布插件与四平台 sample 模块全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 写进 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(上游 9.5.0 降级;2.2.21 旧线必须 8.x) |
| 依赖 | commonMain 零依赖(2.2.21 线无 ohos kotlinx 制品,Nexus 检索验证) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.llmtypewriter |
export JAVA_HOME="/path/to/jdk-21" # Windows: C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot
java -version
1.3 源码依赖度盘点
| 文件 | Compose 依赖 | 处置 |
|---|---|---|
SpeedCurve.kt | 零(纯 fun interface) | 原样保留 commonMain |
MarkdownStreamParser.kt | 零(MdToken 全 public) | 原样保留 commonMain |
StreamingTypewriterState.kt | mutableStateOf 等观察层 | 算法 1:1 抽成 TypewriterEngine;原文件移 composeMain |
CodeHighlighter.kt | Color / SpanStyle / AnnotatedString | tokenizer 抽成 CodeTokens;AnnotatedString 构建留 composeMain |
Helpers.kt | kotlinx-coroutines(Flow 工厂) | 移 composeMain(桥不需要 Flow,ArkTS 定时器喂 token) |
StreamingTypewriter.kt / Cursor.kt / TypewriterRenderer.kt / MarkdownStyles.kt / LlmTypewriterDefaults.kt / ThinkingIndicator.kt | Compose UI / material3 | 移 composeMain |
这次盘点得出两个决定性结论:其一,状态机的 Compose 依赖只有观察层——mutableStateOf 包着的 6 个字段背后是纯 Kotlin 的缓冲区/相位迁移算法,可以无损剥离;其二,parseStreamingMarkdown 与 highlightCode 是 internal——跨模块的桥够不着,必须像 qrose 加 Qrose.encodeQr 一样在 commonMain 加一个公共入口。
1.4 页面边界
标题区 LLM 流式打字机 · Kotlin/Native ohosArm64/ohosX64
Tab 切换 流式 / Markdown / 代码 / 状态
流式 Tab 模拟 LLM 按词喂入、逐字揭示、"▌"光标 900ms 闪烁、点文本跳过、
停止/继续/重播按钮、linear/easeOut/natural 曲线选择、5-60ms 速度滑条、相位状态行
Markdown Tab 逐字流式揭示 + 实时 parse:标题分级、粗斜体、行内代码、删除线、
蓝色链接、深色围栏代码块(未闭合时尾部显示光标)
代码 Tab Kotlin/JS/Python 三语言示例流式揭示 + 实时 highlight 区间上色
状态 Tab info 版本握手、11 个 op 速查表、CPF-KMP-CMP 路径说明、仓库链接
页面启动时先调 op:info 做握手;切 Tab 自动暂停当前 demo(清定时器 + 引擎 stop)。
第 2 阶段:源码集手术
2.1 composeMain 隔离(qrose 审核模式)
9 个 Compose 文件 git mv 进 src/composeMain(保留历史),不注册源集即不编译——这是社区已过审的处理方式:既不在鸿蒙上强行编译编不动的代码,也不删除上游文件;等社区 CMP 工具链与上游版本对齐后,把目录接回源集树即可恢复。
git mv StreamingTypewriter.kt StreamingTypewriterState.kt Cursor.kt TypewriterRenderer.kt \
MarkdownStyles.kt LlmTypewriterDefaults.kt ThinkingIndicator.kt Helpers.kt CodeHighlighter.kt \
../composeMain/kotlin/io/github/nadeemiqbal/llmtypewriter/
2.2 引擎孪生:TypewriterEngine
上游 StreamingTypewriterState 的算法与 mutableStateOf 观察层写在同一个类里,Compose runtime 编不进 ohos KLIB。处置是把算法 1:1 抽成 Compose-free 孪生体(普通字段替代 mutableStateOf,pendingChars 从"写冗余状态"改为计算属性,其余逐行对应),原文件移 composeMain 保持字节级不动:
// llm-typewriter/src/commonMain/.../TypewriterEngine.kt(本次新增)
class TypewriterEngine(
initialBufferSoftCap: Int = LlmTypewriter.DefaultBufferSoftCap,
) {
private val buffer = StringBuilder()
var revealed: String = ""
private set
var phase: TypewriterPhase = TypewriterPhase.Idle
private set
var sourceComplete: Boolean = false
private set
val pendingChars: Int get() = buffer.length - revealed.length
val isFinished: Boolean get() = phase == TypewriterPhase.Done
var bufferSoftCap: Int = initialBufferSoftCap // 软上限:纯内存提示,永不丢字
fun appendToken(token: String) {
if (token.isEmpty()) return
buffer.append(token)
if (phase == TypewriterPhase.Idle || phase == TypewriterPhase.Waiting) {
phase = TypewriterPhase.Revealing
}
}
fun revealNext(): Char? { /* 揭示一个字符;缓冲空则按 sourceComplete 迁 Waiting/Done */ }
fun completeSource() { /* 源完成;若已追平立即 Done */ }
fun skipToEnd() { /* 全量揭示(点击跳过) */ }
fun stop() { /* 相位 → Stopped */ }
fun resume() { /* 从 Stopped 恢复到 Revealing/Waiting/Done */ }
fun reset() { /* 清空一切 */ }
}
"孪生而非重构"是刻意选择:composeMain 不参与编译,对它做任何重构都无法在 fork 构建里验证;保持上游文件不动(仅抽出 TypewriterPhase 枚举到 commonMain)、给孪生体配同一套断言,风险最小、可审计性最强。
2.3 抽取与公共入口
TypewriterPhase五态枚举从StreamingTypewriterState.kt原样抽取到 commonMain(qrose 抽Code128Type同款操作);CodeSpan/CodeSpanKind/CodeLanguage/highlightCode从CodeHighlighter.kt原样抽取为CodeTokens.kt(纯部分零改动,AnnotatedString构建器留 composeMain);- 新增公共入口(桥与一切非 Compose 消费方的门面):
// llm-typewriter/src/commonMain/.../LlmTypewriter.kt(本次新增)
public object LlmTypewriter {
public const val DefaultBaseDelayMs: Long = 18L // 与上游 LlmTypewriterDefaults 对齐
public const val DefaultCursorBlinkMs: Long = 900L
public const val DefaultBufferSoftCap: Int = 4000
public fun parseMarkdown(text: String): List<MdToken> = parseStreamingMarkdown(text)
public fun highlight(code: String, language: String?): List<CodeSpan> =
highlightCode(code, CodeLanguage.parse(language))
}
2.4 测试迁移
上游 StreamingTypewriterStateTest(测 Compose 类)移入 src/composeTest 原样保留;同一套断言移植为 TypewriterEngineTest(15 项,含钉住 reveal 循环节奏语义的专项用例)。MarkdownStreamParserTest / SpeedCurveTest / CodeHighlighterTest 测的全是纯逻辑,零改动留在 commonTest。
第 3 阶段:目标与依赖打通
3.1 制品线验证(决定路线的成败手)
先查 eazytec Maven(https://maven.eazytec-cloud.com)上 2.2.21 线有没有 ohos 的 kotlinx 制品——Nexus REST 检索 org.jetbrains.kotlinx:kotlinx-coroutines-core 的 klib 制品为空。结论:commonMain 必须收敛到零外部依赖,coroutines 依赖的 Helpers.kt(staticFlowOf / wordTokenFlowOf 两个 Flow 工厂)随 Compose 文件一起进 composeMain——桥的喂入由 ArkTS 定时器承担,Flow 在鸿蒙侧本就没有对应语义。这与 KStore 适配(2.4.20 线 kotlinx 四件套齐备、真编译整库)形成路线分野:路线选择必须基于当天的制品可达性。
3.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)
社区已过审的钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 同款)。这条线的代价是 Gradle 必须从上游 9.5.0 降到 8.14.1(qrose 先例),换来的是 ohos target 开箱即用。上游源码虽是 Kotlin 2.3.21 编的,但引擎部分只用基础语言特性,2.2.21 源码级兼容;Compose 1.10.3 的部分本来就隔离在 composeMain。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_llm_typewriter"
include(":llm-typewriter")
include(":example:nativeApp")
3.3 编译引擎
llm-typewriter/build.gradle.kts 收敛为纯 KMP 模块(android/compose/vanniktech 全部剥离):
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 零依赖不变量
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
根构建全局禁用 ohos 测试任务(ohos target 的 test runner 未接线,纯逻辑测试统一 JVM 跑):
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
实测:
./gradlew :llm-typewriter:jvmTest # BUILD SUCCESSFUL(53 项)
第 4 阶段:桥协议建模
4.1 JSON 契约
io.github.nadeemiqbal.llmtypewriter/**(引擎,commonMain 零依赖)
↓ TypewriterEngine / SpeedCurve / MarkdownStreamParser / CodeTokens
Bridge.kt(@CName × 1,扁平 JSON 进出)
↓ UTF-8 JSON
N-API llmTypewriterCommand()
↓
ArkUI Index.ets(四 Tab)
请求与响应(11 个 op 一口进):
请求 : {"op":"append","token":"Hel"}
{"op":"tick","baseDelayMs":18,"curve":"natural"}
{"op":"complete"} {"op":"skip"} {"op":"stop"} {"op":"resume"} {"op":"reset"}
{"op":"config","softCap":4000}
{"op":"parse","text":"# 标题 **粗体**"}
{"op":"highlight","code":"fun x()","language":"kotlin"}
{"op":"info"}
响应 : {"ok":true,"revealed":"...","phase":"Revealing","pending":3,"sourceComplete":false,
"isStreaming":true,"isFinished":false,"softCap":4000}
tick 额外带 {"char":"o","nextDelayMs":32}
parse 额外带 {"tokens":[{"kind":"heading","level":2,"text":".."},...]}
highlight 额外带 {"spans":[{"start":0,"end":3,"kind":"keyword"},...]}
{"ok":false,"error":"..."}
token 的 kind 覆盖上游 MdToken 全家族:plain / italic / bold / boldItalic / inlineCode / strikethrough / link(label,url) / heading(level,text) / newline / codeBlock(language,content,closed);span 的 kind 为 keyword / string / number / comment(plain 区间不输出,ArkTS 侧底色渲染)。
4.2 tick 的揭示语义(桥的灵魂)
上游 Compose 揭示循环的每一拍是:
val prev = state.lastRevealed
val ch = state.revealNext() ?: break
val delayMs = speedCurve.delayForNext(baseDelayMs, prev, ch)
if (delayMs > 0) delay(delayMs)
桥的 tick op 就是这一拍的同步化:先揭示一个字符,再返回"下次应等待的毫秒"(由 SpeedCurve 原版实现按 prev × ch 算出)。ArkTS 侧用 setTimeout 链驱动——每次 tick 后按 nextDelayMs 调度下一次,句末自动放慢、空白自动提速,节奏与上游 Compose 版完全一致。TypewriterEngineTest 里有专项用例钉住这组输入输出。
4.3 错误通道
导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("LlmTypewriterCommand")
fun llmTypewriterCommand(request: String): String = try {
handle(MiniJson.parseFlatObject(request))
} catch (t: Throwable) {
err(t.message ?: t.toString())
}
非法 JSON、缺 op、不支持的 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libllmtypewriter_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosllmtypewriter.so)
│ LlmTypewriterCommand
▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
@CName("LlmTypewriterCommand")
fun llmTypewriterCommand(request: String): String // 11 个 op 一口进
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
LlmTypewriterCommand | llmTypewriterCommand(req) | 引擎全部操作面 + parseMarkdown + highlight + 版本握手 |
单符号设计:打字机是"一次一拍"的交互模型(区别于 KStore 的 put/get/delete 三向分发),op 字段内部分发即可,C ABI 面最小化。
5.3 MiniJson:为什么手写不上 kotlinx.serialization
C 签名永远 const char* -> const char*,桥只需要"扁平对象解析 + 转义字符串写出"两件事;且 2.2.21 线上没有 ohos 的 kotlinx 制品(第 3.1 节验证)。手写 MiniJson(约 120 行:引号状态机 + 转义表 + Long/Double 识别)与 qrose 的选择同源——依赖纪律优先于通用性。
5.4 C++ N-API 与 CMake
napi_property_descriptor desc[] = {
{"llmTypewriterCommand", nullptr, NapiLlmTypewriterCommand, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosllmtypewriter_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。头文件 libohosllmtypewriter_api.h 按社区已审仓库同款手写。
add_library(ohosllmtypewriter SHARED IMPORTED)
set_target_properties(ohosllmtypewriter PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(llmtypewriter_napi SHARED napi_init.cpp)
target_link_libraries(llmtypewriter_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosllmtypewriter)
OHOS_ARCH 为空或 libohosllmtypewriter.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录。Gradle 侧 copyKnSo* 任务把 KN so 与对应 triple 的 libc++_shared.so(从 ~/.konan/dependencies)回填 entry/libs/<abi>/。
export const llmTypewriterCommand: (request: string) => string;
第 6 阶段:ArkUI 宿主
6.1 喂入与揭示循环
private doTick(): void {
const tickReq: Record<string, Object> =
{ 'op': 'tick', 'baseDelayMs': this.baseDelayMs, 'curve': this.curve };
const r: Record<string, Object> = this.bridge(tickReq);
const s = TypewriterState.fromResponse(r);
this.applyState(s); // @State 刷新相位行
if (this.activeDemo === 'stream') {
this.streamText = s.revealed; // 逐字上屏
}
if (s.phase === 'Done' || s.phase === 'Stopped') { return; }
this.scheduleReveal(Number(r['nextDelayMs'] ?? this.baseDelayMs)); // setTimeout 链
}
喂入端 setInterval 每 45-80ms op:append 一个 token(模拟 LLM 出字节奏);揭示端 setTimeout 链按 nextDelayMs 自适应。光标"▌"用独立 900ms 定时器闪烁(DefaultCursorBlinkMs 同款),phase === 'Done' 时隐藏;点击文本区触发 op:skip——tap-to-skip 与上游语义一致。
6.2 Markdown 与代码 Tab
两个 Tab 都是"tick 揭示 → 对已揭示文本实时 parse/highlight → 重建渲染模型":Markdown Tab 把 token 序列映射到 ArkUI Span(标题按 level 放大加粗、行内代码等宽加底色、链接蓝色下划线、围栏代码块深色背景且未闭合时尾部显示光标);代码 Tab 把 spans 区间切成着色段(keyword 紫、string 绿、number 橙、comment 灰),Kotlin 示例直接用库自己的 naturalDelay 函数源码。prefix-stable 特性在这里被直接验收:每次重绘前文不跳动。
6.3 ArkTS 严格模式排雷
DevEco 首次编译暴露 14 个严格模式错误,全部是 Compose 思维的惯性代价:
- 对象字面量作函数实参(12 处
arkts-no-untyped-obj-literals):this.bridge({ op: 'info' })不合法——字面量只能对应显式声明的类/接口,Record参数位不行而带类型标注的局部变量声明可以。修法:每处先const req: Record<string, Object> = { 'op': 'info' };再传; - 成员名撞基类(
tabIndex):CustomComponent基类已有同名通用属性方法,改名activeTab; Text没有onLongPress属性:长按不是通用事件,改.gesture(LongPressGesture().onAction(...));promptAction.showToast已弃用:改this.getUIContext().getPromptAction().showToast(...)。
四、完整代码对照
4.1 整体架构
io.github.nadeemiqbal.llmtypewriter/**(commonMain,零依赖)
│ TypewriterEngine(状态机孪生) / TypewriterPhase / SpeedCurve(上游原样)
│ MarkdownStreamParser + MdToken(上游原样) / CodeTokens(tokenizer 抽取) / LlmTypewriter(入口)
src/composeMain/**(上游 Compose UI,9 文件,不编译)
│ StreamingTypewriter / TypewriterCursor / ThinkingDots / MarkdownRenderer ...
Bridge.kt + MiniJson
│ LlmTypewriterCommand(@CName × 1)
libllmtypewriter_napi.so
│ llmTypewriterCommand
Index.ets
│ JSON parse + setTimeout 链 + @State 更新
▼
ArkUI 真机四 Tab 页面
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_llm_typewriter、sample/* 移出 |
gradle/libs.versions.toml | 裁剪至 Kotlin 2.2.21-1.0.0 单插件 |
build.gradle.kts(根) | 版本 0.1.1-ohos.1、全局禁用 Ohos*Test* 任务 |
llm-typewriter/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,零依赖 |
commonMain/.../TypewriterEngine.kt | 新增:状态机孪生(算法与上游 1:1) |
commonMain/.../TypewriterPhase.kt / CodeTokens.kt | 抽取:五态枚举 / 纯 tokenizer |
commonMain/.../LlmTypewriter.kt | 新增:公共入口(parse / highlight / 默认常量) |
composeMain/(9 文件) | 上游 Compose UI,git mv 保留历史 |
commonTest/.../TypewriterEngineTest.kt | 新增:上游 State 测试同断言移植(15 项) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 1 个 C ABI、11 op、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 四 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(25 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | TypewriterEngine.appendToken / completeSource | token 入缓冲 / 源完成 |
| 引擎 | TypewriterEngine.revealNext / lastRevealed | 揭示一字符 / 曲线用上一字符 |
| 引擎 | skipToEnd / stop / resume / reset | 跳过 / 停止 / 恢复 / 重来(上游控制面全量) |
| 引擎 | TypewriterPhase 五态 | Idle / Revealing / Waiting / Done / Stopped |
| 曲线 | SpeedCurve.Linear / EaseOut / Natural | 常量 / 空白微缓 / 拟人节奏(上游原样) |
| 解析 | LlmTypewriter.parseMarkdown | prefix-stable 流式 Markdown token 化 |
| 高亮 | LlmTypewriter.highlight | Kotlin/JS/Python 区间上色(无正则单遍) |
| Native | LlmTypewriterCommand | C ABI JSON 导出(11 op) |
| N-API | llmTypewriterCommand(req) | 向 ArkTS 暴露能力 |
| ArkTS | doTick() | setTimeout 链驱动揭示循环 |
4.4 ArkTS 与 Kotlin 的边界
const appendReq: Record<string, Object> = { 'op': 'append', 'token': 'Hel' };
const r1 = JSON.parse(llmTypewriterCommand(JSON.stringify(appendReq)));
// r1.ok === true; r1.phase === 'Revealing'; r1.pending === 3
const tickReq: Record<string, Object> =
{ 'op': 'tick', 'baseDelayMs': 18, 'curve': 'natural' };
const r2 = JSON.parse(llmTypewriterCommand(JSON.stringify(tickReq)));
// r2.char === 'H'; r2.nextDelayMs === 18('H' 是普通字符 → 基础延迟)
"tick" -> {
val base = MiniJson.long(req, "baseDelayMs") ?: LlmTypewriter.DefaultBaseDelayMs
val curve = when (MiniJson.str(req, "curve")) {
"linear" -> SpeedCurve.Linear
"easeOut" -> SpeedCurve.EaseOut
else -> SpeedCurve.Natural
}
val prev = engine.lastRevealed // 上游 reveal 循环第一行
val ch = engine.revealNext() // 第二行
val nextDelayMs = if (ch != null) curve.delayForNext(base, prev, ch) else base
// nextDelayMs 即上游循环第三行的 delay() 参数,交回 ArkTS 调度
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:引擎孪生而不是重构 StreamingTypewriterState
把上游状态类改成"委托 TypewriterEngine"的写法看似更优雅,但 composeMain 不参与编译,任何对它的重构都无法在本构建里验证——改错了要等社区 CMP 工具链接回时才爆炸。孪生体配同一套断言(15 项 TypewriterEngineTest),上游文件保持字节级不动,风险与可审计性都最优。这是"算法不复制到 ArkTS、但允许 Kotlin 侧有经过测试钉住的孪生体"的平衡点。
决策 2:commonMain 零依赖,Helpers.kt 随 Compose 隔离
eazytec 2.2.21 线上没有 ohos 的 kotlinx-coroutines。与其等制品,不如让引擎彻底零依赖——打字机的核心语义(缓冲/相位/节奏/解析/高亮)本来就不需要 Flow;Flow 只是 Compose 侧的喂入便利。ArkTS Demo 用定时器喂 token,语义等价。等 2.4.20 线(KStore 验证的 kotlinx 制品线)适配到本库时,可以把 Helpers.kt 接回并补 suspend 桥。
决策 3:根构建替换(cmp_qrose 模式),sample/* 留盘
上游根构建(android / compose / vanniktech 发布 / AGP 9)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。上游 sample/(Android/iOS/桌面/Web 四平台示例)与独立 Xcode 工程留盘不进 settings.gradle.kts——它们的存在方式只有这一种,文档明确"不要在鸿蒙构建里 sync 它们"。
决策 4:单 C ABI + 11 op 内部分发
打字机是"一次一拍"的交互:ArkTS 每 tick 调一次、拿一个字符和一个延迟。这种形态下把 op 全部收进一个 LlmTypewriterCommand 符号,C ABI 面最小、N-API 注册最简、未来加 op 不用动 C 层。与 KStore 的双符号(业务分发 + 版本握手)相比,本库连版本握手也走 op 通道。
决策 5:jvmTest 交叉验证 + 引擎断言钉死
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;53 项 jvmTest 里 38 项是上游原测试文件(解析器 20 / 高亮 10 / 曲线 8,零改动),15 项是孪生引擎与上游 State 测试的同断言移植——"内核没漂"有两层证据:上游纯逻辑测试原样全绿 + 孪生体对齐上游测试全绿。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
./gradlew :llm-typewriter:jvmTest
53 项用例全部通过:
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
TypewriterEngineTest | 15 | 状态机孪生:相位迁移矩阵、pending 计数、跳过/停止/恢复/重来、Done 后追加、tick 节奏语义 |
MarkdownStreamParserTest | 20 | 上游原样:全部 token 家族、prefix-stability、未闭合降级 |
CodeHighlighterTest | 10 | 上游原样:语言别名、关键字/字符串/数字/注释区间、plain 合并 |
SpeedCurveTest | 8 | 上游原样:三种曲线的延迟倍率 |
6.3 HAP 验证
:llm-typewriter:jvmTest: BUILD SUCCESSFUL(53 项)
:example:nativeApp:linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: LlmTypewriterCommand / libohosllmtypewriter_symbols 均为全局 T 符号
copyKnSoArm64 / copyKnSoX64: 三件套回填 entry/libs/<abi>/
CompileArkTS: finished(14 项严格模式错误清零后)
PackageHap: finished
install bundle successfully / start ability successfully
KN so(debugShared,未 strip)arm64 约 3.8MB、x86_64 约 3.5MB——明显小于 KStore 的 14.8MB/13.2MB,因为引擎零依赖、不带 coroutines 运行时;配对 libc++_shared.so 约 1.8MB/1.7MB。
6.4 功能用例
- 启动后状态 Tab 调
op:info,版本行显示0.1.1-ohos.1(来自真实 Native 调用,非静态文字); - 流式 Tab 启动:正文逐字揭示、"▌"光标 900ms 闪烁、状态行实时滚动
phase | pending | isStreaming | softCap; - 句末标点处明显放慢(natural 曲线 6×)、空白处提速——节奏与
SpeedCurveTest的倍率断言一致; - 点击正在揭示的文本:立即跳到缓冲尾(tap-to-skip),
pending归零; - 停止后再继续:从停住的位置精确续播,
Stopped幽灵态消失; - Markdown Tab:标题/粗斜体/行内代码/删除线/链接/围栏代码块全部正确渲染,且前文不随流闪烁(prefix-stable 直接验收);
- 代码 Tab:Kotlin/JS/Python 三语言切换,关键字/字符串/数字/注释按色渲染,颜色随流推进稳定;
- 切换 Tab 自动暂停当前 demo(定时器清理 + 引擎 stop),无泄漏定时器。
6.5 验证结论
打字机引擎、速度曲线、流式解析器、代码 tokenizer、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,四 Tab 页面展示了真实的流式打字机会话:逐字揭示、拟人节奏、跳过/停止/恢复、实时 Markdown 与代码高亮(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/llm-typewriter.git
cd llm-typewriter
export JAVA_HOME="/path/to/jdk-21"
./gradlew :llm-typewriter:jvmTest # 53 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.llmtypewriter -m entry
hdc shell hilog | grep LlmTypewriter
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:llm-typewriter真正编译成ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Gradle 版本跟着工具链线走:2.2.21 旧线必须降 8.14.1(上游 9.5.0 会炸),2.4.20 新线才支持 9.x——KStore 与本仓的经验不能互相套用;
mutableStateOf编不过 ohos 不是重构状态类的理由,是抽孪生体的理由:composeMain 不编译,重构无法验证;孪生 + 同断言测试才是可审计的等价性证明;- 跨模块调用 internal API(
parseStreamingMarkdown/highlightCode)必须加公共入口对象,不要把 internal 改 public 了事——入口对象同时承载默认常量与文档语义; - 对象字面量作函数实参在 ArkTS 严格模式不合法(
arkts-no-untyped-obj-literals,本仓一次性踩了 12 处):Record参数位不行,带类型标注的局部变量声明可以——写法差异就一行,规则差异是本质; - ArkTS 成员名不能与
CustomComponent基类通用属性重名(tabIndex撞.tabIndex()),状态命名要避开基类方法面; Text没有onLongPress属性——长按用LongPressGesture;promptAction.showToast已弃用——用this.getUIContext().getPromptAction().showToast();- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可; - 签名材料由 DevEco 自动写入
build-profile.json5,仓库内signingConfigs保持空数组,证书绝不进入 AtomGit; - 提交规范:作者
Everlasting、组织oh-tpc、文档与提交说明里不出现第三方镜像站品牌名,仓库链接只保留 AtomGit。
8.2 已知问题
- Compose UI 层(
StreamingTypewriter/TypewriterCursor/ThinkingDots/ Markdown 渲染器等 9 文件)不在 ohos 编译,ArkTS 侧以等价 ArkUI 组件呈现;等社区 CMP 工具链与 Compose 1.10 对齐后接回composeMain; staticFlowOf/wordTokenFlowOf(Flow 工厂)随 Compose 隔离,鸿蒙侧由 ArkTS 定时器模拟喂入;- 引擎实例是 so 内进程级单例(Demo 单实例、UI 线程调用),多实例场景需要把 engine 挂进 op 协议(带 handle);
CyclingTypewriterText的删除动画依赖 Compose 侧deleteLastRevealedhack,未纳入桥面;- android / ios / wasmJs target 与四平台 sample 剥离(源文件留在树里),发布链路一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.llmtypewriter匹配的签名。
8.3 未来优化方向
- 等 2.4.20 线 kotlinx 制品可用后,评估
Helpers.kt接回 + suspend 桥(runBlocking收 Flow); - 桥协议补多引擎实例(handle 化)与
updates式状态推送(N-API 线程安全回表); - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(x86_64)验收矩阵与自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 llm-typewriter(11 文件 commonMain,9 个依赖 Compose)
→ 源码依赖度盘点(引擎/UI 分界)
→ 验证 eazytec 2.2.21 线制品边界(无 ohos kotlinx → 零依赖路线)
→ 引擎孪生下沉(TypewriterEngine + 枚举/tokenizer 抽取 + 公共入口)
→ composeMain 隔离 9 文件(不编译不删除)
→ 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(cmp_qrose 模式根构建替换)
→ ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
→ C ABI(@CName × 1,11 op)+ 手写 MiniJson + tick 揭示语义
→ C++ N-API(DisposeString 纪律)
→ ArkTS 严格模式排雷(字面量/命名/手势/toast)
→ ArkUI 真机四 Tab 验收(流式 / Markdown / 高亮 / 状态)
9.2 封装层次
- 引擎层:
TypewriterEngine状态机孪生(上游算法 1:1)+SpeedCurve(上游原样); - 解析层:
MarkdownStreamParserprefix-stable 流式解析(上游原样)+CodeTokenstokenizer(抽取); - Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON(11 op);
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:喂入定时器、tick 揭示链、JSON 校验、渲染模型与四 Tab 页面状态;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- UI 库的适配单位是"能力"不是"库":
LlmTypewriter的 Compose 面永远编不进 ohos,但它的引擎面(状态机/节奏/解析/高亮)是纯 Kotlin——先把"这个库到底卖什么"拆成能力清单,再决定哪些下沉、哪些隔离、哪些由 ArkTS 等价实现; - 孪生优于重构,当且仅当配了同断言测试:不编译的代码改一行都是盲改;孪生体 + 上游测试原样移植,等价性有证据链。反过来,KStore 那种"整库真编译"路线测试零改动更彻底——路线不同,"测试即等价性证明"的原则相同;
- ArkTS 严格模式的坑是模式化的:字面量进
Record参数、成员撞基类、组件没有某个事件属性——写桥的 ArkTS 侧时按这三个模式预排雷,可以一次编译过。
9.4 适配成果
当前适配已完成:
- LlmTypewriter
v0.1.1(Apache-2.0)核心引擎的双 ABI Kotlin/Native 编译(上游纯 Kotlin 文件零改动,孪生/抽取/新增各司其职); - 1 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号),11 op JSON 桥协议; - DevEco Stage ArkUI 四 Tab 验收页面(流式揭示、拟人节奏、跳过/停止/恢复、实时 Markdown 与代码高亮、版本握手);
- 53 项 JVM 交叉验证全绿(38 项上游原测试零改动 + 15 项孪生引擎移植)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 25 节)和本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐


所有评论(0)