社区引导:框架源码与版本发布见 https://atomgit.com/CPF-Flutter/flutter_flutter/tree/oh-3.41.9-release
CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
| 项目 | 版本 |
|---|---|
| 开发工具 | DevEco Studio 26.0(内置 SDK:DevEco Studio/sdk) |
| 系统 API | HarmonyOS 7.0.0 / API 26(compatibleSdkVersion: 26.0.0) |
| Flutter | 3.41.9 |
| 实测设备 | 华为(nova 14) |
全文基于
flutter_ohos_ai_agent三方库 +随声记示例 App,包括最关键的:Flutter 页面一键调起小艺智能体对话面板。
一、痛点:跨端框架对接鸿蒙系统 AI 能力:痛点不在框架,而在系统 Kit 碎片化与环境隔离
跨端框架接鸿蒙系统 AI 能力,痛点与框架无关:
- 能力是碎片化的。语音识别在 CoreSpeechKit,语音合成在 textToSpeech,智能体在 AgentFrameworkKit,意图在 Intents Kit / InsightIntent,技能声明在 SkillSkill 体系。一个「语音指令 → 操作」闭环要横跨四五个 Kit。
- 能力是有条件的。AgentFrameworkKit 依赖
agentKitHsp,模拟器上没有;小艺 bundle 对三方是不可读的(GET_BUNDLE_INFO_PRIVILEGED);意图回传要 API 23+。能力探测本身就是一个需要设计的功能。 - 跨端框架只管渲染,不管系统能力。Flutter 引擎跑在 ArkUI 之上,但
FunctionComponent(智能体官方组件)只能活在 ArkUI 页面里;Flutter 渲染树里塞不进它。 - 真机与模拟器是两个世界。模拟器缺 agentKitHsp、没麦克风;真机有全量能力,但安装需要华为签发的调试证书。两端的行为差异必须在库里抹平。
跨端框架接入鸿蒙系统 AI,瓶颈不在框架本身:鸿蒙 AI 相关能力分散在多个独立 Kit 中,一条语音指令链路就要串联多个套件;各类 AI 能力还有版本、依赖、权限限制,需要专门做能力探测;跨端引擎只负责渲染,原生智能组件无法嵌入 Flutter/RN 渲染树;同时模拟器和真机环境差异巨大,依赖包、硬件、证书条件不一致,适配层需要抹平两边行为。
Flutter 鸿蒙(flutter_ohos)的三方插件体系给一个干净的落点:把系统能力封装成 plugin,Dart 侧拿到的就是一个纯 Dart API。应用开发者不需要知道 AgentFrameworkKit 是懒加载的、不需要知道小艺深链怎么拼——
AIAgent.openAgent()一行搞定。
二、效果先行:两端实测
模拟器:能力探测 + 文本指令闭环
模拟器没有 agentKitHsp 和麦克风,库的能力探测如实降级:

点示例句 chip,走「文本兜底 → 匹配 → 执行 → 状态回写」:
add-expense · 1.00 · 2ms,「午餐花了三十五块」→ 餐饮 ¥35,中文数字正确解析成 35;- 「今天花了多少」→
query-today,TTS 朗读「今天一共花了 61 元……」; - 「撤销上一笔」→
undo-last,账单回滚。
真机:全绿能力面板
HUAWEI TLR-AL00(API 26)上,能力面板全绿:

逐项含义:
| 探测项 | 真机结果 | 依据 |
|---|---|---|
| API | 26 · TLR-AL00 | deviceInfo.sdkApiVersion |
| AgentKit | ✓ AgentFrameworkKit | canIUse('SystemCapability.AI.Agent.AgentKit') |
| 语音识别 | ✓ CoreSpeechKit | SystemCapability.AI.SpeechRecognizer |
| 语音合成 | ✓ textToSpeech | SystemCapability.AI.TextToSpeech |
| 结果回传 | ✓ API 23+ | insightIntentProvider 可用 |
| 小艺 | ✓ 链路可达 | canOpenLink('vassistant://voice/main') |
| 已声明 Skill | ✓ add-expense@1.0.0 | skillManager.getSkillInfosForSelf() |
真机:语音闭环实测
文本 chip 命中 add-expense · 1.00 · 9ms;「按住说话」后 CoreSpeechKit 真实起会话,事件日志出现两次「识别开始,请说指令」;TTS 朗读账单汇总。整条链路的延迟在个位数毫秒级(本地匹配),语音部分的耗时取决于说话本身。

真机:Flutter 一键调起小艺智能体
这是本次最重要的一张图。点「调起智能体」后:
- 插件
openAgent走第一优先级路径:startAbility到宿主的AgentHostAbility承载页(hilog:openAgent via=host ability=AgentHostAbility agentId=com.example.flutter_ohos_ai_agent_example.LedgerAgent); - 承载页渲染 AgentFrameworkKit 官方
FunctionComponent; - 点击「小艺」入口,小艺语音面板从页面底部弹出(「点击或按住说话」);
- 事件流回抛
agentDialogOpened,Dart 侧事件日志记录「智能体弹层 opened」。

完整的调用链是:
Flutter(Dart) → MethodChannel → ArkTS 插件 → startAbility(AgentHostAbility)
→ ArkUI 承载页 → FunctionComponent → 小艺语音面板
↑ agentDialogOpened 事件原路回抛 ↑
这就是「三方库 ↔ 小艺」双向互通的形态:出向由库拉起小艺,入向小艺侧的对话事件通过 EventChannel 回到 Flutter。对话框内加载具体智能体卡片,需要 agentId 在小艺开放平台完成注册(见第六节)。

三、原理:小艺能力全景与 Flutter 的桥接姿势
3.1 四块能力的分工
| 能力 | Kit | 我们的用法 |
|---|---|---|
| 端侧语音识别 | CoreSpeechKit(speechRecognizer) | 「按住说话」,实时回抛中间/最终文本 |
| 端侧语音合成 | CoreSpeechKit(textToSpeech) | 朗读执行结果(「记下了,餐饮 35 元」) |
| 智能体调起 | AgentFrameworkKit(FunctionComponent) | 承载页 + 官方组件,弹出小艺面板 |
| 意图接入 | Skill 体系 + InsightIntent | 小艺入口能「找到」应用;系统意图路由回 App |
外加一个纯 Dart 的本地意图匹配器(三路加权 + 中文数字槽位),让「不经过云端」的语音闭环也能成立——这是示例 App 真正执行操作的地方。
3.2 两条硬边界
- AgentFrameworkKit 没有「命令式打开智能体」的 API。唯一的拉起入口是 ArkUI 组件
FunctionComponent,而 Flutter 渲染不了 ArkUI 组件——所以必须有一个 ArkUI 承载页做跳板。这是 Flutter 与 RN 在这一环节的本质差异:RN 版可以用 TurboModule 起一个ArkUI 页面,Flutter 版一样要起,只是发起方从 JS bridge 换成了 MethodChannel。 - agentId 是云侧资产。
FunctionComponent只负责渲染与对话通道,智能体卡片来自小艺开放平台。没注册的 agentId 不会报错,但面板里是空的(本文示例即如此)。
3.3 Flutter OHOS 插件的桥接链路
RN 版的桥是「TS spec + 手写 C++ 方法表(免 codegen)」;Flutter 版标准姿势简单得多:
- MethodChannel(
flutter_ohos_ai_agent/methods):请求-响应,13 个方法一个 switch 搞定; - EventChannel(
flutter_ohos_ai_agent/events):单通道 +kind区分(voice/intent/agentDialog),Dart 侧只广播一个Stream<AIAgentEvent>; - 编解码由
StandardMethodCodec约定,没有任何生成步骤。
一个必须建立的心智模型:ArkTS 侧的普通对象经 StandardMethodCodec 到 Dart 后是 Map<Object?, Object?>。所有模型类都从 Map 还原、字段级兜底,绝不让一个字段打死整张卡片:
factory AgentCapability.fromMap(Object? raw) {
final m = raw is Map ? raw : const {};
bool b(String k) => m[k] == true;
return AgentCapability(
agentKitAvailable: b('agentKitAvailable'),
speechRecognizerAvailable: b('speechRecognizerAvailable'),
// …
skills: (m['skills'] as List? ?? const [])
.whereType<Map>()
.map(SkillDescriptor.fromMap)
.toList(),
);
}
四、核心实现:
4.1 库结构总览
flutter_ohos_ai_agent/
├── lib/
│ ├── flutter_ohos_ai_agent.dart # 出口:export + 常量
│ └── src/
│ ├── models.dart # 通道数据模型(ArkTS ↔ Dart 一一对应)
│ ├── skill_registry.dart # 本地意图匹配:三路加权 + 中文数字
│ ├── ai_agent.dart # 门面:MethodChannel + EventChannel
│ └── ai_agent_controller.dart # ChangeNotifier 状态机(Flutter 版 useAIAgent)
├── ohos/
│ └── src/main/ets/components/plugin/
│ ├── FlutterOhosAiAgentPlugin.ets # 主插件:通道 + 探测 + ASR/TTS + openAgent
│ └── AgentFeature.ets # AgentFrameworkKit 薄封装(动态 import 懒加载)
├── example/
│ ├── lib/main.dart # 随声记:语音记账示例
│ └── ohos/entry/
│ ├── skills/add-expense/ # 小艺 Skill 声明(SKILL.md + 脚本)
│ └── src/main/
│ ├── module.json5 # 权限/querySchemes/意图 metadata/承载页
│ ├── ets/pages/AgentHost.ets # 智能体承载页(FunctionComponent)
│ ├── ets/entryability/AgentHostAbility.ets
│ └── ets/intents/AddExpenseIntentExecutor.ets
└── scripts/oh_run.sh # 一键构建 + 签名 + 安装 + 启动
4.2 通道契约:
ArkTS 侧 onMethodCall 就是一张明牌:
async onMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
try {
switch (call.method) {
case 'detect': result.success(await this.detect()); break;
case 'listSkills': result.success(await this.listSkills()); break;
case 'getAgentCard': result.success(await this.getAgentCard()); break;
case 'startListening': await this.startListening(this.parseListenOptions(call)); result.success(null); break;
case 'stopListening': await this.stopListening(); result.success(null); break;
case 'cancelListening': await this.cancelListening(); result.success(null); break;
case 'isAgentSupported': result.success(await this.isAgentSupported(call.argument('agentId') as string ?? '')); break;
case 'openAgent': result.success(await this.openAgent(call.argument('optionsJson') as string ?? '{}')); break;
case 'speak': await this.speak(call.argument('text') as string ?? ''); result.success(null); break;
case 'shareIntent': await this.shareIntent(call.argument('intentJson') as string ?? '{}'); break;
case 'completeIntent': await this.completeIntent(call.argument('instanceId') as number ?? 0,
call.argument('resultJson') as string ?? '{}'); break;
default: result.notImplemented();
}
} catch (e) {
// 业务错误统一按 PlatformException(code=aiAgent) 回给 Dart,不撕裂成原生异常
result.error('aiAgent', message, null);
}
}
事件统一走一条 EventChannel,kind 区分:语音事件(start/result/vadBegin/vadEnd/error/complete)、系统意图(intent)、智能体弹层(agentDialog)。Dart 侧:
Stream<AIAgentEvent> get events {
_events ??= _eventsChannel.receiveBroadcastStream(null)
.map(AIAgentEvent.fromMap);
return _events!;
}
4.3 能力探测:把「能不能用」做成一个方法
private async detect(): Promise<AgentCapability> {
const apiVersion = deviceInfo.sdkApiVersion;
const skills = await this.listSkills();
const xiaoyiInstalled = await this.hasXiaoyi();
const cap: AgentCapability = {
agentKitAvailable: this.syscapOk('SystemCapability.AI.Agent.AgentKit'),
speechRecognizerAvailable: this.syscapOk('SystemCapability.AI.SpeechRecognizer'),
ttsAvailable: this.syscapOk('SystemCapability.AI.TextToSpeech'),
intentProviderAvailable: apiVersion >= 23 && this.syscapOk('SystemCapability.AI.InsightIntent'),
xiaoyiInstalled: xiaoyiInstalled,
apiVersion: apiVersion,
deviceModel: deviceInfo.productModel,
skills: skills,
};
return cap;
}
「小艺可达」判定是三档兜底,与 RN 版同一套结论:
getBundleInfo(小艺)—— 真机上必失败(见踩坑 #2);canOpenLink('vassistant://voice/main')—— 需要宿主声明querySchemes;getSkillInfosForSelf()—— 自家 Skill 被技能中心收录即代表 AI 入口可达。
4.4 AgentFrameworkKit 懒加载:别让缺失的 hsp 杀死进程
这是本次最大的坑,值一整节。AgentFeature.ets 顶层 import 了 @kit.AgentFrameworkKit;模拟器没有 agentKitHsp,静态 import 会让进程启动时直接 abort——不是 try/catch 能救的那种,是压根跑不起来。现象是:App 点开即闪退,faultlog 里只有一条 cppcrash。
解法:动态 import 懒加载,把失败窗口收窄到真正用到它的那一次调用:
private async isAgentSupported(agentId: string): Promise<boolean> {
if (!this.syscapOk(SYSCAP_AGENT_KIT) || agentId === '' || this.ability === null) {
return false;
}
// AgentFrameworkKit 由 agentKitHsp 提供,模拟器等环境可能没有:
// 顶层静态 import 会让进程在启动时直接 abort,必须动态 import 懒加载
try {
const feature = await import('./AgentFeature');
return await feature.isAgentSupport(this.ability.context, agentId);
} catch (e) {
const err = e as BusinessError;
// hsp 缺失 / 隐私协议未同意(1022400011) / 未登录(1022400012) → 归为「不可用」
console.warn(`${TAG} isAgentSupport(${agentId}) failed: ${err.code} ${err.message}`);
return false;
}
}
同理,承载页(AgentHost.ets)也只在 canIUse('SystemCapability.AI.Agent.AgentKit') 成立时才渲染 FunctionComponent,否则给出文案兜底。
4.5 端侧语音识别与权限
speechRecognizer 是 user_grant 麦克风权限,顺序必须是:先申请权限,再建引擎。不申请权限直接建引擎,只会收到 1002200002,报错文案不含「缺权限」三个字——排查半天是常态。插件里先 requestPermissionsFromUser,再 createEngine,然后把五类回调原样翻译成事件:
engine.setListener({
onStart: (sid, msg) => this.emitVoice({ kind: 'start', sessionId: sid }),
onEvent: (sid, code, msg) => this.emitVoice({ kind: code === 1 ? 'vadBegin' : 'vadEnd', sessionId: sid }),
onResult: (sid, result) => this.emitVoice({
kind: 'result', sessionId: sid,
result: { sessionId: sid, text: result.result, isFinal: result.isFinal, isLast: result.isLast },
}),
onComplete: (sid, msg) => this.emitVoice({ kind: 'complete', sessionId: sid }),
onError: (sid, code, msg) => this.emitVoice({ kind: 'error', sessionId: sid, errorCode: code, errorMessage: msg }),
});
Dart 侧 AIAgentController 把 result(isLast=true) 直接喂给 dispatch(utterance)——语音到操作的最后一步是纯 Dart 逻辑,与 RN 版共用同一套匹配器设计。
4.6 本地意图匹配:三路加权 + 中文数字
云端大模型当然能做意图理解,但记账这种高频低复杂度场景,端侧规则匹配是 1ms 级、零成本、可解释的。matchCommand 三路加权:
for (final skill in _registry.values) {
double score;
if (skill.examples.any((e) => e == u)) {
score = 1.0; // ① 示例句精确命中
} else {
double bestSim = 0;
for (final e in skill.examples) { // ② 编辑距离相似度
bestSim = max(bestSim, similarity(u, e));
}
score = bestSim * 0.6;
final hits = skill.keywords.where((k) => u.contains(k)).length;
score += (hits * 0.18).clamp(0.0, 0.9); // ③ 关键词命中
}
// …取最高分,低于阈值返回 null
}
槽位抽取含一个手写中文数字解析器(三十五→35、两百零五→205、三千五百→3500、一万二→12000),并处理口语省略。这里藏过一个真 bug,见踩坑 #9。
4.7 调起智能体:承载页 + 官方组件
AgentFrameworkKit 没有命令式 API,唯一入口是 ArkUI 组件。所以 openAgent 的优先级设计是:
- 第 1 条路(真路径):
startAbility到宿主的AgentHostAbility,由承载页渲染官方组件; - 第 0 条兜底:
openLink('vassistant://voice/main')走系统链接解析; - 深链三连:
VoiceAbility+ viewData / qiankun action / 纯 ability。
private async openAgent(optionsJson: string): Promise<OpenAgentResult> {
const opts = JSON.parse(optionsJson) as OpenAgentOptions;
// …第 1 条路:
const hostWant: Want = {
bundleName: selfBundle,
abilityName: hostAbility, // 'AgentHostAbility'
parameters: { 'agentId': agentId, 'queryText': queryText },
};
await ability.context.startAbility(hostWant);
return { opened: true, via: 'host', message: `已打开智能体承载页 ${hostAbility}` };
// …失败则依次降级 openLink / 深链三连,全部失败时区分 16000001 给出人话
}
承载页只有一件事——把官方组件挂出来:
@Entry
@Component
struct AgentHost {
@StorageLink('agentHostAgentId') agentId: string = '';
@StorageLink('agentHostQuery') queryText: string = '';
private controller: FunctionController = new FunctionController();
aboutToAppear(): void {
this.controller.on('agentDialogOpened',
() => console.info('[AgentHost] dialog opened'));
this.controller.on('agentDialogClosed',
() => console.info('[AgentHost] dialog closed'));
}
build() {
Column() {
Text('随声记 · 智能体') /* … */
if (canIUse('SystemCapability.AI.Agent.AgentKit') && this.agentId !== '') {
FunctionComponent({
agentId: this.agentId,
options: { queryText: this.queryText, title: '小艺' } as FunctionOptions,
controller: this.controller,
onError: (err: BusinessError) =>
console.error(`[AgentHost] agent error ${err.code} ${err.message}`),
})
} else { /* 文案兜底 */ }
}
}
}
agentDialogOpened/Closed 事件经 AgentHostAbility 写回 AppStorage/事件流,最终在 Flutter 侧事件日志里显示「智能体弹层 opened」——双向互通的证据链闭合。
4.8 反向桥:系统意图怎么「渡」进 Dart
小艺/技能中心通过 InsightIntent 调起应用时,入口是宿主的意图执行器(UIAbility external ability)。执行器本身在宿主工程里(意图声明必须挂在宿主),但它可以通过插件的静态单例把意图转交回 Dart:
// AddExpenseIntentExecutor.ets(宿主侧,示意)
import FlutterOhosAiAgentPlugin from 'flutter_ohos_ai_agent';
export default class AddExpenseIntentExecutor {
onExecute(params: Record<string, Object>, callback: AsyncCallback<insightIntentProvider.ExecuteResult>): void {
FlutterOhosAiAgentPlugin.routeIntent('AddExpense', params)
.then(() => callback({ code: 0 })) // Dart 侧稍后用 completeIntent 回填真实结果
.catch((e: BusinessError) => callback({ code: e.code }));
}
}
Dart 侧注册处理器:
AIAgent.registerIntentHandler('AddExpense', (invocation, params) async {
final amount = (params['amount'] as num?)?.toDouble() ?? 0;
ledger.add(amount);
await AIAgent.speak('记下了,餐饮 $amount 元');
return SkillResult.ok({'amount': amount});
});
意图注册的关键在 module.json5:意图 profile 必须以 metadata 挂在 UIAbility 上,否则 profile 就是一份没人读的 JSON:
"metadata": [
{ "name": "ohos.insightIntent.config", "resource": "$profile:insight_intent" }
]
4.9 Skill 声明:让小艺「找得到」应用
skills/add-expense/SKILL.md 声明技能的语义(输入/输出/示例句),打包进 hap 后由系统收录到技能中心。真机上 getSkillInfosForSelf 返回 1 条:add-expense@1.0.0 · system · EntryAbility——这正是能力面板「已声明 Skill ✓」的来源,也是「小艺可达」第三档判定的依据。
4.10 状态机:Flutter 版的 useAIAgent
RN 版暴露 useAIAgent() hook;Flutter 的对等物是 AIAgentController extends ChangeNotifier,内置状态机:
idle → listening(ASR)→ recognizing(拿最终文本)→ matching → executing
→ speaking(TTS)→ idle;任何一步出错 1.2s 后自动回 idle
UI 只需要 AnimatedBuilder(animation: controller) 就能拿到全部状态与事件日志,示例 App 因此很薄。
4.11 一键脚本:模拟器/真机双模式
scripts/oh_run.sh 自动识别签名方式:
- 工程
build-profile.json5里配置了signingConfigs.material(DevEco 自动签名产物)→ 直接用 hvigor 已签名 hap——这是真机唯一可行路径; - 否则回退 OpenHarmony SDK 调试证书链本地签名——仅适用模拟器/开发板。
# 模拟器
./scripts/oh_run.sh example debug
# 真机(UDID 自动读取,多设备时用 HDC_TARGET 指定)
HDC_TARGET=<你的设备序列号> ./scripts/oh_run.sh example debug
五、我踩过的坑(每一条都有真实报错)
坑一:静态 import AgentFrameworkKit,模拟器启动即闪退。 模拟器缺 agentKitHsp,顶层 import { … } from '@kit.AgentFrameworkKit' 让进程在启动时直接 abort。faultlog 里只有一条 cppcrash-com.example.flutter_ohos_ai_agent_example-*.log,hilog 里连插件第一行日志都打不出来。修复是动态 import 懒加载(5.2 节)。教训:凡是系统 Kit,先 canIUse 再 import,且 import 必须懒。
坑二:真机查询小艺包信息被拒。 真机日志原文:
[AIAgentFlutter] getBundleInfo(com.huawei.hmos.vassistant) failed: 201 BusinessError 201:
Permission denied. An attempt was made to GetBundleInfo forbidden by permission:
ohos.permission.GET_BUNDLE_INFO_PRIVILEGED.
GET_BUNDLE_INFO_PRIVILEGED 是系统应用专属权限。别把它当「小艺没装」——三档判定因此必需(5.1 节)。
坑三:真机拒收 OpenHarmony 调试签名。 真机 hdc install 原文:
error: failed to install bundle. code:9568257 error: fail to verify pkcs7 file.
HarmonyOS 商用系统只信任华为 AGC 签发的证书链,OpenHarmony 链只对模拟器/开发板有效。解法:DevEco Studio → Project Structure → Signing Configs → Automatically generate signature,一次点击生成 .p12/.cer/.p7b 并写入工程 signingConfigs;profile 与 bundleName、设备 UDID 绑定。验证 profile 覆盖设备:
openssl cms -verify -noverify -inform DER -in <p7b> -out -
# 看输出里 debug-info.device-ids 与 bundle-info.bundle-name
坑四:canOpenLink 报 17700056。 scheme 未在宿主 module.json5 的 querySchemes 声明,深链解析直接拒绝。补上 "querySchemes": ["vassistant"] 才可能返回 true。这个权限模型和 Android 的 <queries> 一个思路:能查谁的 scheme,要自己先声明。
坑五:未配置签名时 flutter build hap 退出码为 1,但产物已生成。 终端提示 请通过DevEco Studio打开ohos工程后配置调试签名…,退出码 1,但 entry-default-unsigned.hap 已经落盘。构建脚本对退出码做了容忍,只认 hap 文件是否存在。教训:Flutter OHOS 工具链的「失败」不总是失败,以产物为准。
坑六:ArkTS 侧对象到 Dart 是 Map<Object?, Object?>。 StandardMethodCodec 把 ArkTS 普通对象解码成 Dart Map,字段类型不匹配就变 null。所有模型类做字段级兜底、列表元素 whereType<Map>() 过滤——绝不让一个字段打死整张卡片。
坑七:ArkTS 严格类型的两处收敛。 agentCards: ESObject[] 在 lint 下过不了,改 Object[];插件字段声明 common.UIAbility 会与注入实例的类型不兼容,直接 import UIAbility from '@ohos.app.ability.UIAbility' 用具体类型。ArkTS 的「严格」不是烦人,是把运行时类型坑提前到编译期。
坑八:意图执行器 import 插件要用默认导出。 import { FlutterOhosAiAgentPlugin } from 'flutter_ohos_ai_agent' 在 ohpm 产物下解析不到,改 import FlutterOhosAiAgentPlugin from 'flutter_ohos_ai_agent' 立刻通过。ohpm 对命名导出的解析规则和 npm 不完全一致,跨包引 ArkTS 类优先默认导出。
坑九:中文数字口语省略。 初版把「一百二」解析成 1(应为 120)、「一万二」解析成 2(应为 12000)。修复是补两条缺省规则:位值后缺省 ×10、万段后缺省 ×10000。排查时的坑外坑:dart run 直跑与 flutter test 环境行为不一致,验证必须放到 flutter test 里做,并用 三十五/一百二/一万二/两百零五/三千五百 五个用例固化。
坑十:麦克风权限不申请,ASR 只报 1002200002,文案不提「权限」二字。先 requestPermissionsFromUser 再 createEngine,顺序焊死在同一个入口里(5.3 节)。
坑十一:热重启时 EventChannel 先 onCancel 再 onListen。 onCancel 里若销毁 ASR/TTS 引擎,二次 listen 后事件全丢。只清引用、不销毁,真正的清理放在 onDetachedFromEngine 之后的引擎销毁里。
坑十二:双设备在线,hdc install 会装错地方。 本机同时在线真机和模拟器,不带 -t 的 install 走默认目标,应用静悄悄装错了设备。所有命令带 -t 是纪律;排查方法也简单:装完 hdc -t <目标> shell bm dump -n <包名> 核对版本,别靠猜。
坑十三:FunctionComponent 只能活在 ArkUI 页面。 Flutter 渲染树塞不进它,这是 Flutter 与纯 ArkUI 应用的本质差异,承载页方案是唯一解(5.5 节)。RN 版可以用 TurboModule 起 ArkUI 页面,Flutter 版一样要起,只是发起方不同。
坑十四:agentId 未注册时,FunctionComponent 不报错但面板无卡片。 onError 安静、面板可对话,很容易误判成「代码写错了」。排查时先确认 agentId 是否已在小艺开放平台注册,再怀疑代码。真机验证时我们的面板就是这种状态:链路全通、卡片为空——这是预期行为,不是 bug。
七、现在怎么用:从 三方库 到依赖
这个工程现在是两层结构:flutter_ohos_ai_agent 是插件本体(Dart API + ArkTS 实现),example(随声记)演示从 Skill 声明到智能体调起的完整宿主配置。
接入自己的 Flutter 鸿蒙工程,四步:
- 引入插件:
dependencies:
flutter_ohos_ai_agent:
path: /path/to/flutter_ohos_ai_agent
- 宿主
module.json5补四样(示例工程可直接抄):ohos.permission.MICROPHONE权限、querySchemes: ["vassistant"]、UIAbility 的ohos.insightIntent.configmetadata、AgentHostAbility承载页。 - Dart 侧注册技能(示例句 + 关键词 + 槽位 + handler),UI 挂
AIAgentController。 - 需要智能体调起时,在小艺开放平台注册智能体,把 agentId 填进
openAgent()——库与示例都做成了配置项,注册完成后填入即可,代码零改动。
运行示例:
# 真机:先在 DevEco 里给 example/ohos 配一次自动签名,之后全自动
HDC_TARGET=<设备序列号> bash scripts/oh_run.sh example debug
总结:
本文基于 Flutter OHOS 3.41.9、鸿蒙 API 26,落地了
flutter_ohos_ai_agent三方插件,以「随声记」语音记账 Demo 完整跑通从 Skill 声明、语音交互、意图解析到小艺智能体面板唤起的全链路闭环。
鸿蒙原生AI能力分散在多个独立Kit,同时存在依赖、权限、版本、环境等前置约束,并且官方 FunctionComponent 智能体组件无法嵌入 Flutter 渲染树,这是跨端接入的核心卡点,和 Flutter 框架本身无关。插件的核心思路就是搭建一层 ArkTS 桥接适配层,通过 MethodChannel 处理请求调用、EventChannel 流式回抛语音、意图、智能体弹窗事件,对外收敛成简洁统一的 Dart API,业务侧无需感知底层各个Kit的差异。
为解决模拟器缺少 agentKitHsp 导致启动闪退的致命问题,插件对 AgentFrameworkKit 采用动态懒加载导入;同时实现一套多判据的能力探测逻辑,自动识别当前设备支持的语音识别、TTS、智能体、意图能力,区分模拟器与真机环境做降级兜底。语音链路严格遵循先申请麦克风权限再初始化识别引擎的顺序;本地意图匹配器采用加权策略+中文数字槽位解析,适合记账这类轻量端侧指令场景,延迟低、不依赖云端。
智能体唤起是整套方案的关键:依靠独立的 ArkUI 承载 Ability 承载 FunctionComponent 官方组件,再通过事件回传把弹窗状态同步给 Flutter,打通了 Flutter 页面和小艺面板的双向通信;同时支持 InsightIntent 系统意图路由,小艺技能中心下发的指令可以转发到 Dart 层执行业务逻辑。
15



被折叠的 条评论
为什么被折叠?



