本文记录 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 Kotlin 2.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 动态库 + @CName C 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 / nextDelayMstick 返回本次揭示的字符与下次应等待的毫秒。
tokens / spansparse / 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 MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin 21.0.12,org.gradle.java.home 写进 gradle.properties)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle8.14.1(上游 9.5.0 降级;2.2.21 旧线必须 8.x)
依赖commonMain 零依赖(2.2.21 线无 ohos kotlinx 制品,Nexus 检索验证)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.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.ktmutableStateOf 等观察层算法 1:1 抽成 TypewriterEngine;原文件移 composeMain
CodeHighlighter.ktColor / SpanStyle / AnnotatedStringtokenizer 抽成 CodeTokens;AnnotatedString 构建留 composeMain
Helpers.ktkotlinx-coroutines(Flow 工厂)移 composeMain(桥不需要 Flow,ArkTS 定时器喂 token)
StreamingTypewriter.kt / Cursor.kt / TypewriterRenderer.kt / MarkdownStyles.kt / LlmTypewriterDefaults.kt / ThinkingIndicator.ktCompose 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 方法覆盖内核
LlmTypewriterCommandllmTypewriterCommand(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 思维的惯性代价:

  1. 对象字面量作函数实参(12 处 arkts-no-untyped-obj-literals):this.bridge({ op: 'info' }) 不合法——字面量只能对应显式声明的类/接口,Record 参数位不行而带类型标注的局部变量声明可以。修法:每处先 const req: Record<string, Object> = { 'op': 'info' }; 再传;
  2. 成员名撞基类(tabIndex):CustomComponent 基类已有同名通用属性方法,改名 activeTab;
  3. Text 没有 onLongPress 属性:长按不是通用事件,改 .gesture(LongPressGesture().onAction(...));
  4. 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.kt1 个 C ABI、11 op、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(25 节)

4.3 关键 API 对照

层次API作用
引擎TypewriterEngine.appendToken / completeSourcetoken 入缓冲 / 源完成
引擎TypewriterEngine.revealNext / lastRevealed揭示一字符 / 曲线用上一字符
引擎skipToEnd / stop / resume / reset跳过 / 停止 / 恢复 / 重来(上游控制面全量)
引擎TypewriterPhase 五态Idle / Revealing / Waiting / Done / Stopped
曲线SpeedCurve.Linear / EaseOut / Natural常量 / 空白微缓 / 拟人节奏(上游原样)
解析LlmTypewriter.parseMarkdownprefix-stable 流式 Markdown token 化
高亮LlmTypewriter.highlightKotlin/JS/Python 区间上色(无正则单遍)
NativeLlmTypewriterCommandC ABI JSON 导出(11 op)
N-APIllmTypewriterCommand(req)向 ArkTS 暴露能力
ArkTSdoTick()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 项用例全部通过:

测试类用例数钉住什么
TypewriterEngineTest15状态机孪生:相位迁移矩阵、pending 计数、跳过/停止/恢复/重来、Done 后追加、tick 节奏语义
MarkdownStreamParserTest20上游原样:全部 token 家族、prefix-stability、未闭合降级
CodeHighlighterTest10上游原样:语言别名、关键字/字符串/数字/注释区间、plain 合并
SpeedCurveTest8上游原样:三种曲线的延迟倍率

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 功能用例

  1. 启动后状态 Tab 调 op:info,版本行显示 0.1.1-ohos.1(来自真实 Native 调用,非静态文字);
  2. 流式 Tab 启动:正文逐字揭示、"▌"光标 900ms 闪烁、状态行实时滚动 phase | pending | isStreaming | softCap;
  3. 句末标点处明显放慢(natural 曲线 6×)、空白处提速——节奏与 SpeedCurveTest 的倍率断言一致;
  4. 点击正在揭示的文本:立即跳到缓冲尾(tap-to-skip),pending 归零;
  5. 停止后再继续:从停住的位置精确续播,Stopped 幽灵态消失;
  6. Markdown Tab:标题/粗斜体/行内代码/删除线/链接/围栏代码块全部正确渲染,且前文不随流闪烁(prefix-stable 直接验收);
  7. 代码 Tab:Kotlin/JS/Python 三语言切换,关键字/字符串/数字/注释按色渲染,颜色随流推进稳定;
  8. 切换 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :llm-typewriter 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Gradle 版本跟着工具链线走:2.2.21 旧线必须降 8.14.1(上游 9.5.0 会炸),2.4.20 新线才支持 9.x——KStore 与本仓的经验不能互相套用;
  4. mutableStateOf 编不过 ohos 不是重构状态类的理由,是抽孪生体的理由:composeMain 不编译,重构无法验证;孪生 + 同断言测试才是可审计的等价性证明;
  5. 跨模块调用 internal API(parseStreamingMarkdown / highlightCode)必须加公共入口对象,不要把 internal 改 public 了事——入口对象同时承载默认常量与文档语义;
  6. 对象字面量作函数实参在 ArkTS 严格模式不合法(arkts-no-untyped-obj-literals,本仓一次性踩了 12 处):Record 参数位不行,带类型标注的局部变量声明可以——写法差异就一行,规则差异是本质;
  7. ArkTS 成员名不能与 CustomComponent 基类通用属性重名(tabIndex 撞 .tabIndex()),状态命名要避开基类方法面;
  8. Text 没有 onLongPress 属性——长按用 LongPressGesture;promptAction.showToast 已弃用——用 this.getUIContext().getPromptAction().showToast();
  9. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  10. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套(NAPI so + KN so + libc++)缺一不可;
  11. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内 signingConfigs 保持空数组,证书绝不进入 AtomGit;
  12. 提交规范:作者 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 侧 deleteLastRevealed hack,未纳入桥面;
  • 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(上游原样);
  • 解析层:MarkdownStreamParser prefix-stable 流式解析(上游原样)+ CodeTokens tokenizer(抽取);
  • Native 层:生成双 ABI 动态库并通过 1 个 C ABI 输出 JSON(11 op);
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:喂入定时器、tick 揭示链、JSON 校验、渲染模型与四 Tab 页面状态;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. UI 库的适配单位是"能力"不是"库":LlmTypewriter 的 Compose 面永远编不进 ohos,但它的引擎面(状态机/节奏/解析/高亮)是纯 Kotlin——先把"这个库到底卖什么"拆成能力清单,再决定哪些下沉、哪些隔离、哪些由 ArkTS 等价实现;
  2. 孪生优于重构,当且仅当配了同断言测试:不编译的代码改一行都是盲改;孪生体 + 上游测试原样移植,等价性有证据链。反过来,KStore 那种"整库真编译"路线测试零改动更彻底——路线不同,"测试即等价性证明"的原则相同;
  3. 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。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐