开发调试踩坑记录
记录项目开发过程中遇到的典型问题和调试经验,帮助后续高效定位。
1. ArkUI @Prop 深拷贝丢失 Record 属性
现象
卡片渲染时组件参数不完整,下划线前缀的属性(如 _componentPath、__ascfInstanceId__)丢失。
根因
ArkUI @Prop 装饰器会对传入的数据做深拷贝。Record<string, Object> 类型的字段在深拷贝过程中,非字面量赋值的属性(通过 obj[key] = value 动态设置的)可能被丢弃或变为不可枚举。
解决方案
遵循已有的 static singleton 模式(参考 commit e8abf71)。在 @Prop 边界之前(aboutToAppear 中)将数据存入 static Map,build() 时通过 key 取回:
// 错误:@Prop 深拷贝会丢失属性
@Prop cardData: CardRenderData = { params: {} }
// 正确:static store 绕过 @Prop
const staticStore: Map<number, Record<string, Object>> = new Map()
// aboutToAppear: staticStore.set(key, realParams)
// build: staticStore.get(key) ?? fallback
相关文件
ascf-agent/agent-runtime/src/main/ets/ui/FlexCardView.ets
2. BinaryWriter 嵌套 Map 序列化损坏
现象
LoadInstance 反序列化 buffer 数据与实际 Map 结构不匹配,C++ Deserializer 读取 key-value 时错位,导致 params 丢失或损坏。日志:
ReadObject(VALUE) FAILED at number=2 key=drink_recommended-drinks_1
根因(多层嵌套)
当 jsParams 结构为 Map { "params": Map { "params": "<JSON_STRING>" } }(两层嵌套 Map),BinaryWriter 序列化时产生错误的二进制结构。C++ Deserializer 在解析时 key-value 错位——将 value(如 drink_recommended-drinks_1)误读为 key。
详细原因:Serializer.writeJSObject() 递归调用 writeValue() 处理嵌套 Map,但 BinaryWriter 的 tag-length-value 格式在处理嵌套结构时存在 offset 计算 bug。
诊断方法
- 在 C++
Deserializer::ReadObjectProperties中打印每个 key 和对应的 value 类型 - 查看 LoadInstance 的 hex buffer:
hdc shell "hilog -x" | grep "DEBUG-LoadInstance.*buf" - 对比 ArkTS 侧构造的 Map 结构与 buffer hex 内容,定位错位点
解决方案(两条互补路径)
路径 A:扁平化 params(推荐)
在 FlexUIEngine.loadCard() 中,将每个 key-value 直接设为 jsParams 的 entry,不对 params 做二次包装:
// 错误:嵌套 Map
jsParams.set('params', JSON.stringify(cleanParams)); // Map { params: String }
// 正确:扁平化,每个 key 独立
for (const key of keys) {
if (key !== '_componentPath') {
const val = params[key];
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
jsParams.set(key, val as HippyValue);
} else {
jsParams.set(key, JSON.stringify(val) as HippyValue);
}
}
}
路径 B:Bootstrap 直接读取扁平 params(JS 侧兜底) 无论 ArkTS 侧用哪种格式,bootstrap 兼容两种结构:
// 兼容扁平格式 params = { __ascfInstanceId__, structuredContent, ... }
// 兼容嵌套格式 params = { name, id, params: { ... } }
var cardParams = (params && params.params) ? params.params : (params || {});
相关文件
flexui-engine/.../serialization/Serializer.ets— 序列化逻辑(不建议修改)flexui-engine/.../flexui/FlexUIEngine.ets— 参数转换层(路径 A)toolkit/ascf-toolkit/lib/aimode-compiler/harmony-entry-template.js— JS bootstrap(路径 B)
3. ParseJson 对 PendingException 的 FATAL 崩溃
现象
FOOTSTONE_DCHECK(status == JSVM_OK) 在 debug build 中触发 FATAL 终止进程。
根因
JSHVM::ParseJson 只处理了 JSVM_GENERIC_FAILURE,未处理 JSVM_PENDING_EXCEPTION。当前序操作在 JS VM 中抛出了未捕获异常(如 LoadInstance param error),后续 OH_JSVM_JsonParse 返回非 OK 状态,触发 FOOTSTONE_DCHECK fatal。
解决方案
将 FOOTSTONE_DCHECK 改为正常的错误处理逻辑:
auto status = OH_JSVM_JsonParse(env_, jsh_string_value->GetValue(), &result);
if (status == JSVM_GENERIC_FAILURE) {
FOOTSTONE_LOG(ERROR) << "JSHVM::ParseJson error, json: " << json;
return nullptr;
}
// 新增:处理其他非 OK 状态(如 PENDING_EXCEPTION)
if (status != JSVM_OK) {
FOOTSTONE_LOG(ERROR) << "JSHVM::ParseJson status=" << (int)status;
return nullptr;
}
return std::make_shared<JSHCtxValue>(env_, result);
相关文件
flexui-engine/driver/js/src/vm/jsh/jsh_vm.cc
4. callJsModule 事件与 FlexView 组件不在同一 JS Scope
现象
callJsModule("__ascfCallbacks", "onCardEvent", ...) 发送了正确的数据(日志可见 structuredContent 含 3 个 items),但组件侧的 NotificationType.Result 回调从未触发。组件日志显示 addListener on unknown instance ""。
根因
Hippy 引擎为每个 FlexView 创建独立的 JS 执行上下文(Scope)。callJsModule 调用通过 ArkTS → C++ → 主 Scope 的 hippyBridge 函数派发,目标 globalThis.__ascfCallbacks 属于主 Scope。但 FlexView 的 .pack.js 运行在子 Scope 中,其 __ascfCallbacks 是不同的对象。
时序图:
ArkTS: allocate() → callJS('onCardEvent', [id, 'result', dataJson])
→ C++: CallJs(MAIN_SCOPE) → hippyBridge → globalThis.__ascfCallbacks.onCardEvent(...)
↑
主 Scope 的 __ascfCallbacks
✗ 子 Scope 的 instanceEventBus 不在这个 Scope
诊断方法
- 确认
__ascfCallbacks存在:bootstrap 添加console.log(typeof globalThis.__ascfCallbacks) - 对比
callJsModule时间戳与bootstrap RUN时间戳,确认到达顺序 - 检查组件日志中是否出现
[modelContext] onInstanceCreate/registerInstance/onCardEvent
解决方案
不依赖 callJsModule 传递初始数据。改为两条路径:
- 通过 LoadInstance params 传递(扁平化,参考 Pitfall #2)——数据随组件创建一起到达
- Bootstrap 直接注入全局变量——在
run(params)中提取 params 并设置globalThis.__ascfResultData__,组件created()中直接读取
// bootstrap: run(params)
globalThis.__ascfResultData__ = parsedParams;
globalThis.__ascfInstanceId__ = instId;
// component: created()
var data = globalThis.__ascfResultData__;
if (data) { this.setData({ items: data._meta.viewItems.slice(0, 3) }); }
相关文件
flexui-engine/driver/js/src/js_driver_utils.cc— CallJs / LoadInstance scope 管理flexui-engine/driver/js/src/scope.cc— Scope::LoadInstanceascf-agent/agent-runtime/src/main/ets/modelcontext/InstanceManager.ets— 事件发送toolkit/ascf-toolkit/lib/aimode-compiler/harmony-entry-template.js— bootstrap 全局注入
5. callJsModule 事件在 .pack.js 加载之前到达
现象
callJsModule("onInstanceCreate", ...) 日志时间戳早于 runScriptFromUri(.pack.js 加载完成)700ms+。调用到达时 __ascfCallbacks 尚未定义。
根因
ArkTS 侧 InstanceManager.allocate() → callJS('onInstanceCreate', ...) 是同步调用,立即通过 C++ bridge 投递到 JS 线程。但 FlexView 的 JS bundle 加载(runScriptFromUri)是异步的(需要从 rawfile 读取并解析 500KB+ 的 .pack.js)。
时序:
t=0: allocate() → callJS('onInstanceCreate', ...) → 投递到 JS task queue
t=0: FlexView 创建 → loadCard → loadModule → 异步加载 .pack.js
t=700ms: runScriptFromUri 完成 → bootstrap 执行 → __ascfCallbacks 定义
t=700ms: JS task queue 处理 callJS → __ascfCallbacks 已存在 ← 但 Scope 不对!
解决方案
采用 InstanceManager 的 pending events 机制,延迟到组件加载完成后再 dispatch:
// allocate() 和 dispatchResult() 将事件存入 pendingEvents
this.pendingEvents.set(instanceId, [
['onInstanceCreate', [instanceId]],
['onCardEvent', [instanceId, 'result', resultJson]],
]);
// FlexCardView.onViewReady 时 flush
AscfAIModule.sInstanceManager.flushEvents(instanceId);
相关文件
ascf-agent/agent-runtime/src/main/ets/modelcontext/InstanceManager.ets— flushEventsascf-agent/agent-runtime/src/main/ets/ui/FlexCardView.ets— onViewReady 触发 flush
6. McpToolExecutor 未调用 Skill API,数据来源错误
现象
卡片渲染显示"没有匹配的饮品"(空状态)。onCardEvent('result', data) 中的 data 只有:
{"tool":"getRecommendedDrinks","arguments":{},"_componentPath":"..."}
缺少 structuredContent、_meta、items 等业务数据。
根因
McpToolExecutor.execute() 目前返回硬编码 mock 数据,而不是调用真正的 Skill API 实现。正确的流程应该是:
LLM calls mcp__getRecommendedDrinks
→ ToolExecutor (source='mcp')
→ McpToolExecutor.execute()
→ SkillApiBridge.executeApi(skillPath, apiName, args) ← 当前缺失!
→ callJS('executeSkillApi', [...])
→ JS: skillApiRegistry.execute(skillPath, apiName, args)
→ 执行 apis/getRecommendedDrinks.js(真实业务逻辑)
→ read data/seed.js → filter → return { structuredContent, _meta }
Skill API 的注册与执行流程:
activate_skilltool →SkillActivator.activate(skillPath)→ 在 JS 引擎执行index.jsindex.js调用skill.registerAPI('getRecommendedDrinks', getRecommendedDrinks)注册 handler- Handler 存储在
modelContext.ts的skillApiRegistry(JS 侧) - 后续 tool 调用应通过
SkillApiBridge.executeApi()→callJS('executeSkillApi', ...)触发 handler - Handler 执行结果通过
callNative('onSkillApiResult', ...)返回 ArkTS 侧
解决方案
McpToolExecutor 需要持有 SkillApiBridge 的引用和 skillPath,在 execute() 中调用真正的 API:
class McpToolExecutor implements ToolExecutor {
private entry: McpApiEntry
private skillPath: string
private apiBridge: SkillApiBridge
async execute(args: string): Promise<string> {
const parsedArgs = JSON.parse(args)
// 调用真正的 Skill API(JS 侧执行)
const apiResult = await this.apiBridge.executeApi(this.skillPath, this.entry.name, parsedArgs)
const resultObj = JSON.parse(apiResult)
// 注入 _componentPath(CardRenderer 需要)
if (compPath) { resultObj._componentPath = compPath }
return JSON.stringify(resultObj)
}
}
注册时传入依赖:修改 McpConfigLoader.registerToolEntry() 接收 skillPath 和 apiBridge:
McpConfigLoader.registerToolEntry(entry, toolRegistry, mcpConfigDir, apiBridge)
// mcpConfigDir 如 'skills/drink-skill'(从 mcp.json 路径推导)
注意:skillPath 需要与 index.js 中 createSkill('skills/drink-skill') 的路径一致。
相关文件
ascf-agent/agent-runtime/src/main/ets/mcp/McpConfigLoader.ets— McpToolExecutor + registerToolEntryascf-agent/agent-runtime/src/main/ets/modelcontext/SkillApiBridge.ets— executeApi / handleResultascf-agent/agent-runtime/src/main/ets/AgentRuntime.ets— loadConfig 传入 apiBridgeflexui-engine/driver/js/packages/hippy-ascf/src/modelContext.ts— skillApiRegistry.executedemos/ascf-ai-demo/ascf/ascf_src/skills/drink-skill/index.js— API 注册 (createSkill + registerAPI)demos/ascf-ai-demo/ascf/ascf_src/skills/drink-skill/apis/getRecommendedDrinks.js— API 实现
⚠ 注意 skillPath 不一致:loadConfig 从 configPath(如 ascf-ai-demo/skills/drink/mcp.json)推导 skillPathFromConfig = ascf-ai-demo/skills/drink。但 index.js 中 createSkill('skills/drink-skill') 使用的是不同路径。当前推导可能与实际注册路径不匹配,导致 SkillApiBridge.executeApi() 找不到 handler。需要后续统一或通过 SkillDiscovery 映射。
7. has 全局变量在 FlexView Scope 中不存在
现象
组件 created() 中抛出 ReferenceError: has is not defined。日志:
[recommended-drinks] has is not defined
根因
组件代码直接引用 has.modelContext.getContext(this)(全局变量),但 has 来自 @hippy/ascf 的 ES module export,不会自动暴露为 FlexView Scope 的全局变量。主 Scope 中 Hippy 引擎注入了 has,但子 Scope 没有。
解决方案
在 bootstrap 模板中显式将 has 设为全局变量:
var _hippyAscf = require('@hippy/ascf');
if (_hippyAscf.has) { globalThis.has = _hippyAscf.has; }
注意:@hippy/ascf 的 webpack resolve alias 必须指向 flexui fork(flexui-engine/driver/js/packages/hippy-ascf/src),因为上游 Hippy repo 的 has.ts 不包含 modelContext 导出。
相关文件
toolkit/ascf-toolkit/lib/aimode-compiler/harmony-entry-template.js— bootstrap 全局注入toolkit/ascf-toolkit/lib/aimode-compiler/webpack-config-factory.js— resolve aliasdemos/ascf-demo/webpack.config.js— 主 bundle 的 resolve alias
8. HAR 增量构建缓存导致的"改了代码不生效"
现象
修改了 FlexUIEngine.ets(位于 flexui-engine/framework/ohos),重新编译 hippy HAR 成功,但 agent HAP 部署后行为不变。
根因
hvigorw增量构建可能跳过已编译的模块(显示UP-TO-DATE)ohpm install不会自动更新已缓存的 HAR(需要删除oh_modules/.ohpm重装)hvigorw不检测 rawfile 文件变化,需clean后重新assembleHap
解决方案
完整的强制重编流程:
# 1. 清理 + 重编 hippy HAR
rm -rf flexui-engine/framework/ohos/build
hvigorw --mode module -p module=hippy@default assembleHar
# 2. 清理 agent 缓存 + 重装依赖
rm -rf ascf-agent/oh_modules/.ohpm ascf-agent/agent-runtime/build ascf-agent/entry/build
ohpm install
# 3. 重编 agent HAP
hvigorw --mode module -p module=entry@default assembleHap
验证 HAR 是否包含修改:
unzip -p entry-default-signed.hap "resources/rawfile/..." | grep "关键代码片段"
相关文件
flexui-engine/framework/ohos/build/— HAR 输出ascf-agent/oh_modules/.ohpm/— ohpm 缓存
9. ArkTS 语法限制
9a. build() 中只能写 UI 组件语法
错误:
build() {
const storeParams = this.paramsFromStore() // ❌ 不允许变量声明
Logger.info(TAG, '...') // ❌ 不允许函数调用
FlexView({ ... })
}
正确:所有非 UI 逻辑移到 aboutToAppear() 或回调中。如需日志,使用 .onAppear() 回调。
9b. 禁止无类型对象字面量 (arkts-no-untyped-obj-literals)
错误:
return { drinkId: 1, name: '拿铁', price: 28 } // ❌
正确:通过 JSON.stringify/parse 绕过类型检查:
const jsonStr = '{"drinkId":1,"name":"拿铁","price":28}'
return JSON.parse(jsonStr) as Record<string, Object>
9c. 类型转换需要经过 unknown
错误:
const merged: Record<string, Object> = result as Record<string, Object> // ❌
正确:
// ToolExecResult → Object → JSON.stringify → JSON.parse → Record<string, Object>
const mergedStr = JSON.stringify(result as Object)
const merged: Record<string, Object> = JSON.parse(mergedStr) as Record<string, Object>
9d. typeof 禁止用于 any/unknown 类型
错误:
const val = params[key] // val 类型为 ESObject | undefined ≈ any
if (typeof val === 'string') { ... } // ❌
正确:先显式转换类型:
const val: Object = params[key] as Object
const valType: string = typeof val
if (valType === 'string') { ... }
10. 端到端渲染数据流(完整版)
Tool Call (LLM)
│
▼
McpToolExecutor.execute() → 返回 { structuredContent, _meta, _componentPath } ← 必须有业务数据!
│
▼
AgentRuntime.run().TOOL_EXECUTING
├── CardRenderer.resolve() → 查找 componentPath → CardRegistryEntry
├── InstanceManager.allocate() → 创建 instanceId + 事件入队 (pendingEvents)
├── InstanceManager.dispatchResult() → result 事件入队
│
▼
FlexCardView
├── @Prop 边界 ← static store 绕过深拷贝
├── FlexView → HippyEngine.loadModule()
│
▼
FlexUIEngine.loadCard() → jsParams 扁平化 ← 关键转换点!
│
▼
HippyBridgeManagerImpl.loadInstance()
├── BinaryWriter 序列化 {
│ name: String,
│ id: Number,
│ params: { ← 单层 Map,无嵌套!
│ __ascfInstanceId__: String, ← 必须在此
│ structuredContent: String (JSON),
│ _meta: String (JSON),
│ __instanceName__: String,
│ __instanceId__: Number,
│ }
│ }
│
▼
C++ JsDriverUtils::LoadInstance
├── Hermes: buffer → UTF-16 string → ParseHippyValue → createFromJsonUtf8
│ ⚠ 注意:Hermes 路径期望 JSON,不是 binary 格式!
│ ⚠ getSerializerBufferBinary() vs getSerializerBufferJson() 不匹配
│
▼
JS: appRegister[name].run(params)
│
▼
Harmony Entry Bootstrap
├── require('@hippy/ascf') → globalThis.has = _hippyAscf.has ← 必须设置!
├── cardParams = params.params || params ← 兼容 flat/nested
├── globalThis.__ascfResultData__ = parsedParams ← 直接数据注入
├── globalThis.__ascfInstanceId__ = instId
│
▼
Component created()
├── globalThis.__ascfResultData__ → setData(items) ← 主要数据路径
├── has.modelContext.getContext(this).on(Result, ...) → 备用事件路径
│
▼
Glass-easel setData → template re-render → NativeBackend.flush → HippyBridge updateNode → C++ 渲染
关键检查点(按顺序):
- McpToolExecutor — 是否返回了
structuredContent+_meta @Prop边界 — 数据是否通过 static store 绕过FlexUIEngine.loadCard— Object → HippyMap 转换是否扁平- BinaryWriter 序列化 — buffer hex 是否与构造的 Map 一致
- C++ Hermes 路径 — buffer 是 binary 还是 JSON 格式
- Bootstrap —
globalThis.has、__ascfResultData__是否设置 - Component —
created()中数据来源是否正确
11. 调试工具链速查
构建与部署
# Agent 完整 dev 循环
./build.sh dev-agent
# 仅构建 hippy HAR(FlexUIEngine 修改后)
rm -rf flexui-engine/framework/ohos/build
hvigorw --mode module -p module=hippy@default -p product=default -p buildMode=debug assembleHar
# Agent 强制重编(HAR 修改后)
rm -rf ascf-agent/oh_modules/.ohpm ascf-agent/agent-runtime/build ascf-agent/entry/build
ohpm install && hvigorw --mode module -p module=entry@default assembleHap
# 重编 .pack.js 组件 bundles(bootstrap/组件修改后)
node -e "var w=require('webpack');var {createAiModeCompilerConfig}=require('./toolkit/ascf-toolkit/lib/aimode-compiler/webpack-config-factory');w(createAiModeCompilerConfig('demos/ascf-ai-demo/ascf/ascf_src','demos/ascf-ai-demo',{mode:'development'})[1],function(e,s){if(!s.hasErrors())console.log('OK')})"
cp -r demos/ascf-ai-demo/entry/src/main/resources/rawfile/skills ascf-agent/entry/src/main/resources/rawfile/ascf-ai-demo/
# 安装 + 启动
hdc install -r ascf-agent/entry/build/default/outputs/default/entry-default-signed.hap
hdc shell "aa force-stop com.atomicservice.5765880207854768701 && sleep 1 && aa start -a EntryAbility -b com.atomicservice.5765880207854768701"
日志查看
# JS console 日志(经过 ConsoleModule → hilog)
hdc shell "hilog -x -D 0x0001" | grep 'console_module' | grep 'recommended-drinks'
# C++ 日志(domain 0x0001, tag: hippy)
hdc shell "hilog -x -D 0x0001" | grep 'A00001.*hippy'
# LoadInstance buffer hex dump
hdc shell "hilog -x -D 0x0001" | grep 'DEBUG-LoadInstance.*buf'
# Bridge 调用日志(onInstanceCreate, onCardEvent)
hdc shell "hilog -x -D 0x0001" | grep 'callJsModule.*ascfCallbacks'
# 查看崩溃
hdc shell "hilog -x -D 0x0001" | grep -E 'FATAL|CHECK FAILED|SIGSEGV|LoadInstance param error'
添加诊断日志
ArkTS (Logger):
import { Logger } from '../utils/Logger'
const TAG = 'ComponentName'
Logger.info(TAG, 'msg=%{public}s count=%{public}d', strVal, numVal)
JS (console,经 ConsoleModule → hilog):
console.log('[prefix] message')
console.warn('[prefix] warning')
console.error('[prefix] error')
C++ (footstone):
#include "footstone/logging.h"
FOOTSTONE_LOG(INFO) << "message"; // release + debug
FOOTSTONE_DLOG(INFO) << "message"; // debug only
FOOTSTONE_LOG(ERROR) << "message"; // 错误
验证 HAP 内容
# 提取 pack.js 查看是否包含修改
unzip -p entry-default-signed.hap "resources/rawfile/ascf-ai-demo/skills/drink/components/recommended-drinks/index.pack.js" | grep "关键代码"
# 列出 HAP 中所有 rawfile
unzip -l entry-default-signed.hap | grep rawfile
常见构建陷阱
- HAR 修改不生效 →
rm -rf oh_modules/.ohpm && ohpm install - .pack.js 修改不生效 → 重编 webpack + 重编 HAP(hvigorw 不检测 rawfile 变化)
- ArkTS 编译错误 10605038 → 对象字面量需要显式类型,用
JSON.parse(jsonStr)绕过 - ArkTS 编译错误 10905204 →
build()中不能写非 UI 语句,移到aboutToAppear()或回调 - hvigorw 找不到 hvigor-config.json5 → 检查 CWD,必须在项目根目录或模块目录执行
- 设备 ID 变化 →
hdc list targets查看,更新build.sh中的 DEVICE 变量
12. AGP 7.2.2 + Compose BOM 版本兼容性
现象
Demo APK 构建失败:checkDebugAarMetadata FAILED — requires compile against version 34。
根因
Compose BOM 2024.02.00 传递依赖了 activity:1.8.2、lifecycle:2.7.0、emoji2:1.4.0 等库,这些库的 AAR metadata 声明需要 compileSdkVersion >= 34,但项目使用 COMPILE_VERSION=33。
关键认知
compileSdkVersion不需要 AGP 升级 —— AGP 只是构建工具,不会限制你能用哪个 SDK 平台编译- 限制条件只有一个:SDK platform 必须已安装(
$ANDROID_HOME/platforms/android-34) - AGP 7.2.2 可以编译 SDK 34,只要平台已安装
解决方案
路径 A(推荐):升级 COMPILE_VERSION 到 34
# flexui-engine/gradle.properties
COMPILE_VERSION=34
不需要升级 AGP,不需要升级 Kotlin,不需要改 NDK。
路径 B(备用):降级 Compose 依赖到与 SDK 33 兼容的版本
// 不用 BOM,手动指定版本
implementation 'androidx.compose.ui:ui:1.5.0'
implementation 'androidx.compose.material3:material3:1.1.0'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
相关文件
flexui-engine/gradle.propertiesflexui-engine/android-modules/agent-runtime/build.gradle
13. Compose API 版本差异
现象
编译错误:Unresolved reference: HorizontalDivider、Unresolved reference: VerticalDivider、Unresolved reference: TextAlign.Unspecified。
根因
HorizontalDivider / VerticalDivider 在 Material3 1.2.0(BOM 2024.x)中引入。降级到 BOM 2023.x 后这些 API 不存在。
解决方案
| 旧 API | 替换 |
|---|---|
HorizontalDivider(...) |
Divider(...) |
VerticalDivider(thickness, color, modifier) |
Box(Modifier.fillMaxHeight().width(thickness).background(color)) |
TextAlign.Unspecified |
TextAlign.Start(作为默认 fallback) |
相关文件
agent-runtime/src/main/java/org/flexui/agent/ui/markdown/MarkdownRenderer.ktagent-runtime/src/main/java/org/flexui/agent/ui/markdown/CodeFenceBlock.kt
14. Kotlin 属性命名与 JVM 方法签名冲突
现象
编译错误:Platform declaration clash: getServers()Ljava/util/List;
根因
Kotlin 的 val servers: List<T> 属性编译为 JVM 的 getServers() 方法。如果同文件中手动定义了 fun getServers() 方法,两者 JVM 签名相同,冲突。
解决方案
将内部属性重命名为 _servers(下划线前缀),保留公开方法的命名:
// 错误
private val servers = mutableListOf<McpServerConfig>()
fun getServers(): List<McpServerConfig> = servers // JVM 签名冲突
// 正确
private val _servers = mutableListOf<McpServerConfig>()
fun getServers(): List<McpServerConfig> = _servers // OK
相关文件
agent-runtime/src/main/java/org/flexui/agent/mcp/McpServerManager.kt
15. kotlinx.serialization 编译器插件不可用
现象
Kai 参考代码大量使用 @Serializable 注解和 kotlinx.serialization.json.*,直接移植到本项目的 Android 模块会编译失败。
根因
kotlinx.serialization 需要 Kotlin 编译器插件 (kotlin("plugin.serialization")),但本项目未配置该插件。添加插件需要和项目现有的 Kotlin 版本严格匹配。
解决方案
全部替换为 org.json.JSONObject / JSONArray(Android SDK 内置,零依赖):
// Kai 风格(需要 serialization 插件)
@Serializable
data class MemoryEntry(val key: String, val content: String)
val json = Json { ignoreUnknownKeys = true }
json.decodeFromString<List<MemoryEntry>>(raw)
// Android 风格(零依赖)
data class MemoryEntry(val key: String, val content: String)
val obj = JSONObject(raw)
val entry = MemoryEntry(key = obj.getString("key"), content = obj.getString("content"))
影响范围
全部 data/、mcp/、email/、sms/ 模块的序列化代码。
16. ChatViewModel 绕过 DataRepository 直接调用 AgentRuntime
现象
ChatViewModel.sendMessage() 直接调用 agentRuntime.run(),跳过 DataRepository.ask()。
影响
- 会话不持久化(ConversationStorage 未使用)
- Memory tools 未注册
- System prompt 缺少技能信息
- 丢失 Kai 的 fallback chain 机制
根因
AgentChatView 创建 ChatViewModel 时传入的是 AgentRuntime 而非 DataRepository。原始设计意图是 ChatViewModel 通过 DataRepository 间接访问 AgentRuntime。
解决方案
修改 AgentChatView 创建 DataRepository 并传入 ChatViewModel:
// AgentChatView.kt
val repo = remember { DataRepository(context) }
val viewModel = ChatViewModel(repo)
当前状态
ChatViewModel 直接使用 AgentRuntime,功能可用但缺少持久化层。后续需重构为通过 DataRepository 访问。
相关文件
agent-runtime/src/main/java/org/flexui/agent/ui/AgentChatView.ktagent-runtime/src/main/java/org/flexui/agent/ui/ChatViewModel.kt
17. 分阶段实现中的依赖顺序
问题
Phase 2(Skills)的 BuiltinTools 依赖 Phase 2.4 的 SkillActivator,但任务被安排在 Phase 2.1。Phase 1.5 的 ToolExecutor 需要 Phase 1.6 的 LlmTypes.ToolCall。
解决方案
- 提前定义关键接口:在 Phase 1 先定义所有跨模块依赖的接口(
ToolCall、StreamCallbacks) - Stub 先行:先创建空实现(如
McpConfigLoaderstub),后完成功能 - 重构式完成:Phase 1.5 的
ToolCall先在ToolExecutor.kt内联定义,Phase 1.6 再移到LlmTypes.kt
经验教训
写实现计划时,应该先做依赖分析,按依赖图的拓扑顺序排列任务。
18. 主线程运行 AgentRuntime.initialize() 导致 ANR 风险
现象
AgentRuntime.initialize() 包含网络请求(RemoteConfig)、文件 I/O(SkillDiscovery.scan()),在 Dispatchers.IO 上运行。
解决方案
始终在 IO 协程中初始化:
CoroutineScope(Dispatchers.IO).launch {
agentRuntime?.initialize()
}
相关文件
demos/android-demo/.../MainActivity.kt
19. LazyColumn 中动态 item 类型的处理
现象
需要在消息列表中穿插渲染卡片(FlexCardView),但 LazyColumn.items() 期望单一数据类型。
解决方案
将消息和卡片展平为 List<Any>,在 items 中按类型分发:
val flatMessages = messages.flatMap { msg ->
val items = mutableListOf<Any>(msg)
msg.card?.let { items.add(it) }
items
}
items(flatMessages.size) { index ->
when (val item = flatMessages[index]) {
is ChatMessage -> MessageBubble(message = item)
is CardRenderData -> FlexCardView(card = item)
}
}
相关文件
agent-runtime/src/main/java/org/flexui/agent/ui/ChatMessageList.kt
20. 包重命名影响范围
操作
com.openhippy.example → org.flexui.agentdemo
影响文件
| 文件 | 修改内容 |
|---|---|
build.gradle |
namespace + applicationId |
AndroidManifest.xml |
package 属性 |
MainActivity.kt |
package 声明 + 目录移动 |
settings.gradle |
无需修改(Gradle module 名 :example 不变) |
scripts/android/dev.sh |
adb am start 包名 |
scripts/android/start.sh |
adb am start 包名 |
易遗漏项
adb am start命令中硬编码的包名(不在 Gradle 中管理)- 如果
AndroidManifest.xml的<activity android:name=".MainActivity">使用的是相对路径(.前缀),它自动匹配当前 package,无需修改
21. 使用 Logger 而非 Log 进行格式化日志
现象
Log.i(TAG, "msg=%s count=%d", str, count) 在 API < 24 的设备上编译为 Log.i(String, String) 而非 Log.i(String, String, Object...)。
解决方案
统一使用项目的 Logger 工具类(内部调用 String.format):
// 正确
Logger.info(TAG, "msg=%s count=%d", str, count)
// 错误(在 minSdk 21 上格式化失败)
Log.i(TAG, "msg=%s count=%d", str, count)
相关文件
agent-runtime/src/main/java/org/flexui/agent/utils/Logger.kt
22. FlexUIView 在 Compose 中的 AndroidView 集成
关键实现
FlexCardView 使用 AndroidView 将 FlexUIView(FrameLayout 子类)嵌入 Compose 树:
AndroidView(
factory = { ctx ->
FlexUIView(ctx).apply {
url = card.bundleUrl
componentName = card.componentName
params = card.params
client = object : FlexUIClient { ... }
}
},
update = { flexView ->
if (flexView.url != card.bundleUrl) {
flexView.url = card.bundleUrl
flexView.componentName = card.componentName
}
},
)
关键点
FlexUIView.loadCardInternal()在onAttachedToWindow()中自动触发 —— 不需要手动调用updatelambda 只在参数变化时调用;首次渲染只执行factoryFlexUIView需要FlexUIEngine.ensureInit()已调用且引擎就绪
相关文件
agent-runtime/src/main/java/org/flexui/agent/ui/FlexCardView.ktflexui-engine/framework/android/.../FlexUIView.kt
23. Android @HippyMethod 参数类型:HippyMap vs HippyArray
现象
callJavaScriptModule("__ascfCallbacks", "executeSkillApi", ...) 成功将调用发送到 JS 侧,JS 代码正常执行并通过 callNative('AscfAIModule', 'onSkillApiResult', {callbackId, result}) 返回结果,但 Native 侧 crash:
java.lang.reflect.InvocationTargetException
Caused by: java.lang.NullPointerException: Parameter specified as non-null is null:
method org.flexui.agent.modelcontext.AscfAIModuleModule.onSkillApiResult, parameter params
根因
Hippy 框架中 @HippyMethod 的参数类型映射规则:
| JS 传给 callNative 的 params | Java 反序列化类型 | @HippyMethod 参数类型 |
|---|---|---|
{key: value} (对象) |
HippyMap |
必须用 HippyMap |
[item1, item2] (数组) |
HippyArray |
必须用 HippyArray |
"string" (字符串) |
String |
String |
42 (数字) |
int/double |
int/double |
JS 侧 callNative('AscfAIModule', 'onSkillApiResult', {callbackId, result}) 传的是对象,反序列化为 HippyMap。但方法签名声明的 params: HippyArray → ArgumentUtils.parseArgument(HippyArray.class, ...) 无法转换 → 返回 null → Kotlin 非空检查抛 NullPointerException。
HippyNativeModuleInfo.invoke() 通过 Method.invoke() 反射调用,异常被包装为 InvocationTargetException。如果 exception handler 只打印 e.message(不打印 e.cause),根因完全不可见。
参考
Hippy 框架原生用例确认了此规则:
// WebSocketModule.java — JS 传对象 → HippyMap
@HippyMethod(name = "connect")
public void connect(HippyMap request, Promise promise) { ... }
// UtilsModule.java — JS 传数组 → HippyArray
@HippyMethod(name = "vibrate")
public void vibrate(HippyArray patternHippy, int repeat) { ... }
// NetworkModule.java — JS 传字符串 → String
@HippyMethod(name = "getCookie")
public void getCookie(String url, Promise promise) { ... }
解决方案
将所有 @HippyMethod 的 params 类型从 HippyArray 改为 HippyMap(因为 JS 侧统一传对象 {key: value}):
// 错误
fun onSkillApiResult(params: HippyArray, promise: Promise) = ...
// 正确 — JS 传 {callbackId, result},反序列化为 HippyMap
fun onSkillApiResult(params: HippyMap, promise: Promise) = ...
相关文件
android-modules/agent-runtime/src/main/java/org/flexui/agent/modelcontext/AscfAIModuleModule.ktframework/android/.../HippyNativeModuleInfo.java:177—Method.invoke()反射调用点framework/android/.../HippyModuleManagerImpl.java:383—doCallNatives()
24. InvocationTargetException 隐藏根因 — Exception Handler 必须打印 cause chain
现象
logcat 只能看到 handleNativeException: java.lang.reflect.InvocationTargetException,没有堆栈,没有 cause。只能猜测是哪个模块崩溃。
根因
Hippy 框架通过 Method.invoke() 反射调用 @HippyMethod 方法。如果方法内部抛异常,Method.invoke() 包装为 InvocationTargetException。InvocationTargetException.getMessage() 返回 null(无 detail message),根因存在 getCause() 中。
默认的 handleNativeException 只打印 e.message:
// 错误 — 丢失根因
override fun handleNativeException(e: Exception, haveCaught: Boolean) {
LogUtils.e(TAG, "[Engine] handleNativeException: ${e.message}") // null!
}
解决方案
始终打印 cause chain:
override fun handleNativeException(e: Exception, haveCaught: Boolean) {
LogUtils.e(TAG, "[Engine] handleNativeException: ${e.message}")
var cause: Throwable? = e.cause
while (cause != null) {
LogUtils.e(TAG, "[Engine] cause: ${cause.javaClass.name}: ${cause.message}")
LogUtils.e(TAG, "[Engine] cause stack: ${cause.stackTraceToString()}")
cause = cause.cause
}
if (e.cause == null) {
LogUtils.e(TAG, "[Engine] stack: ${e.stackTraceToString()}")
}
}
相关文件
framework/android/.../FlexUIEngine.kt— exception handlerframework/android/.../HippyBridgeManagerImpl.java:374—handleMessagecatch block →reportException(e)
25. Android callJavaScriptModule Native→JS→Native 双向链路调试
完整链路
Native: SkillApiBridge.executeApi()
→ callJS("executeSkillApi", [skillPath, apiName, argsJson, callbackId])
→ HippyBridgeManager.callJavaScriptModule("__ascfCallbacks", "executeSkillApi", params)
→ Handler.sendMessage(MSG_CODE_CALL_FUNCTION, FUNCTION_ACTION_CALL_JS_MODULE=6)
→ handleCallFunction(msg) — V8 序列化 + callFunction(6, ...)
→ JNI callFunction → V8 deserialize → global.hippyBridge("callJsModule", callObj)
JS: native2js.js
→ case 'callJsModule':
→ __GLOBAL__.jsModuleList[callObj.moduleName][callObj.methodName](callObj.params)
→ __ascfCallbacks.executeSkillApi(params)
→ skillApiRegistry.execute(skillPath, apiName, args)
→ API handler → result
JS→Native (回调):
→ bridge.callNative('AscfAIModule', 'onSkillApiResult', {callbackId, result})
→ V8 serialize → JNI → HippyModuleManagerImpl.doCallNatives()
→ HippyNativeModuleInfo.invoke() → Method.invoke()
→ AscfAIModuleModule.onSkillApiResult(params: HippyMap, promise)
Native: AscfAIModule.handleSkillApiResult()
→ SkillApiBridge.handleResult() → Promise resolved → API 完成
关键断点 & 日志
| # | 位置 | 确认内容 | 日志 TAG |
|---|---|---|---|
| 1 | callJavaScriptModule |
参数是否正确 | FlexUI: callJS -> __ascfCallbacks.$method |
| 2 | handleCallFunction |
序列化成功?id=6? | HippyBridgeManager: handleCallFunction: functionId=6 (callJsModule) |
| 3 | HippyBridgeImpl.callFunction |
mInit=true? buffer 长度? |
HippyBridgeImpl: callFunction: fn=callJsModule fnId=6 bufLen=... |
| 4 | V8 JNI | scope 存在? | logcat scope can not found (C++ FOOTSTONE_LOG) |
| 5 | native2js.js |
action 到达? | JS console.log 或 ConsoleModule |
| 6 | __ascfCallbacks |
executeSkillApi 执行? | [INFO:console_module.cc] '[DEBUG] ascfCallbacks executeSkillApi RECV' |
| 7 | callNative 回调 |
onSkillApiResult 参数类型? | AscfAIModuleModule: call: method=onSkillApiResult |
| 8 | handleResult |
callbackId 匹配? | SkillApiBridge handleResult: callbackId=... |
常见误判
- "callJavaScriptModule 不工作" → 检查方向:很可能 Native→JS 正常,是 JS→Native 的
callNative回调失败。用 ConsoleModule.log 测试可区分。 - "InvocationTargetException" → 不是
callJavaScriptModule的问题,是@HippyMethod反射调用的方法内部抛异常。 - "scope can not found" →
JsDriver.mInstanceId无效。检查引擎初始化是否完成(STATUS_OK)。
诊断技巧
- 用
ConsoleModule.log测试callJavaScriptModule是否可达(如果 ConsoleModule 也没有输出,问题在 V8 序列化/反序列化层) - 看 logcat 中
CallFunction: putVarint日志 — 只要出现,说明 V8 序列化在进行 - 对比
handleNativeException时间戳与callJS dispatched时间戳 — 如果立即出现(<50ms),说明在 Handler 分发阶段出错
相关文件
framework/android/.../HippyBridgeManagerImpl.java— handleCallFunction, handleMessageframework/android/.../HippyBridgeImpl.java— callFunctiondriver/js/lib/bridge/android/native2js.js— JS 侧派发driver/js/packages/hippy-ascf/src/ascfCallbacks.ts— executeSkillApiandroid-modules/agent-runtime/.../AscfAIModuleModule.kt— JS→Native 回调
26. Android debugMode=true 导致 Hippy 从 Dev Server 拉空 Bundle
现象
Hippy 引擎初始化 STATUS_ERR_BRIDGE: run script error,V8 crash。即使 jsfwk.js 已正确打包到 assets。
根因
FlexUIEngineConfig.debugMode = true 时,Hippy 不会从 assets 加载 core bundle,而是从 localhost:38989 的 dev server 拉取。如果 dev server 未运行,获取到空字符串,V8 执行空脚本 crash。
解决方案
生产/测试设备上设置 debugMode = false:
FlexUIEngine.ensureInit(FlexUIEngineConfig(
debugMode = false, // 从 assets 加载,不要连 dev server
...
))
相关文件
framework/android/.../FlexUIEngine.ktdemos/android-demo/.../MainActivity.kt
27. Webpack 缓存导致修改不生效
现象
修改了 ascfCallbacks.ts,重编 jsfwk.js 成功(webpack 无错误),但部署后 JS 行为不变。日志仍显示旧代码的错误。
根因
Webpack module cache 保留了旧的编译输出。当文件修改时间戳未触发重新编译时,webpack 使用缓存中的旧模块。
解决方案
清除 webpack 缓存后重编:
rm -rf flexui-engine/driver/js/node_modules/.cache
bash scripts/build-jsfwk.sh
相关文件
driver/js/scripts/build-jsfwk/index.js
28. Android build.gradle afterEvaluate 删除 Assets 目录
现象
Gradle 构建时 src/main/assets/ 目录被清空,导致 jsfwk.js 和 skill assets 丢失。
根因
旧的 build.gradle 中有:
project.afterEvaluate {
delete 'src/main/assets'
copy { from "res" into "src/main/assets" }
}
这会将整个 assets/ 目录删除再重建,导致构建脚本之前拷入的文件被清除。
解决方案
删除 afterEvaluate 块,改为在构建脚本中直接将文件拷贝到 src/main/assets/,Gradle 不做任何清理。
相关文件
demos/android-demo/build.gradlescripts/build-demo.shscripts/build-jsfwk.sh
29. Android McpConfigLoader 未注入 _componentPath 导致卡片不渲染
现象
Tool 执行成功返回数据,但 chat UI 中没有卡片显示。日志中缺少 CardRenderer.resolve、Card rendered、onCardReady 等卡片渲染相关输出。
根因
卡片渲染由 AgentRuntime.run() 的 TOOL_EXECUTING 阶段触发,关键逻辑:
val componentPath = extractComponentPath(trContent)
if (componentPath.isNotEmpty()) {
val cardData = cardRenderer.resolve(trContent)
...
}
extractComponentPath() 从 tool result JSON 中提取 _componentPath 字段。如果这个字段不存在,卡片渲染被跳过。
OHOS vs Android 差异:
| OHOS | Android(修复前) | |
|---|---|---|
execute() 返回 |
{tool, arguments, _componentPath} + merge API result |
直接返回 API result(无 _componentPath) |
_componentPath 来源 |
entry._meta.ui.componentPath → 注入到 base result → 与 API result merge |
未注入,API result 没有此字段 |
OHOS McpConfigLoader.ets:77-85:
const result: ToolExecResult = {
tool: this.entry.name,
arguments: parsedArgs,
}
const compPath = entryMetaUi?.componentPath
if (compPath !== undefined && compPath !== '') {
result._componentPath = compPath // ← 关键注入!
}
// 执行 API → merge API result 与 base result
Android McpConfigLoader.kt(修复前)直接返回 apiResult,没有构建 base result 和注入 _componentPath。
解决方案
对齐 OHOS 逻辑,在 McpConfigToolExecutor.execute() 中:
- 构建 base result JSON
{tool, arguments, _componentPath}(含 entry metadata 中的componentPath) - 执行 Skill API 获取业务结果
- Merge API result 到 base result(API 字段覆盖 base 字段)
- 返回合并后的 JSON
override suspend fun execute(args: Map<String, Any>): Any {
val baseResult = JSONObject().apply {
put("tool", entryName)
put("arguments", JSONObject(args.toMap()))
if (!componentPath.isNullOrEmpty()) {
put("_componentPath", componentPath) // ← 注入!
}
}
val apiResult = apiBridge.executeApi(skillPath, entryName, args)
// Merge: API result keys overwrite base result keys (matching OHOS)
val apiObj = JSONObject(apiResult)
val apiKeys = apiObj.keys()
while (apiKeys.hasNext()) {
val key = apiKeys.next()
baseResult.put(key, apiObj.get(key))
}
return baseResult.toString()
}
注意:Merge 时 API result 覆盖 base result。例如 API result 中有 arguments 字段会覆盖 base result 中的 arguments,这是预期行为。
验证日志
[McpConfigLoader] Skill API result: ... len=1027
[ToolExecutor] execute: tool=searchDrinks resultLen=1159 ← len 增加了 132 bytes(base metadata)
[CardRenderer] Resolved card: components/recommended-drinks/index -> /skills/.../index.pack.js/drink_recommended-drinks
[AgentRuntime] Card rendered: components/recommended-drinks/index -> instanceId=drink_recommended-drinks_1
[ChatViewModel] onCardReady: component=drink_recommended-drinks ...
数据流
mcp.json entry._meta.ui.componentPath
→ McpConfigToolExecutor.componentPath
→ execute() → baseResult._componentPath
→ merge with API result → finalResult._componentPath
→ AgentRuntime.extractComponentPath() → componentPath
→ CardRenderer.resolve() → CardRenderData
→ InstanceManager.allocate() → dispatchResult()
→ ChatViewModel.onCardReady() → FlexCardView 渲染
相关文件
android-modules/agent-runtime/.../McpConfigLoader.kt:405-455— McpConfigToolExecutor.execute()android-modules/agent-runtime/.../AgentRuntime.kt:263-316— TOOL_EXECUTING 卡片渲染ascf-agent/agent-runtime/.../McpConfigLoader.ets:70-99— OHOS 参考实现