HarmonyOS 7 核心实践:基于 CoreVisionKit 端侧 AI 语义搜图与多模态双链 Vault 架构
一、 引言与业务背景
随着移动端个人知识库(PKM)与“第二大脑”(Second Brain)概念的流行,Obsidian 风格的 Markdown 纯本地双链笔记逐渐成为高效能人士的标配。然而在移动端体验中,纯文本笔记存在一个长期的痛点:多模态图片附件管理极难搜索与关联。用户在笔记中插入了大量的应用截图、设计草图、发票证件或生活照片,事后想要查找某张包含特定视觉内容的图片,传统方法只能依赖死板的文件名或手动 Tag 标记。
借助 HarmonyOS 7 提供的端侧 AI 视觉能力库 CoreVisionKit (@hms.ai.vision.textSearchImage),我们在 AeroPlan 鸿蒙版应用中成功实现了 完全脱离网络、零隐私泄漏、基于 NPU 硬件加速的端侧自然语言语义搜图(例如搜索“猫咪”、“机票”、“发票”或“架构图”)。
效果图展位:端侧 AI 语义搜图与笔记关联实测

本文将深度剖析我们在落地“端侧 AI 语义搜图 + 多模态双链 Vault 架构”过程中遇到的真实技术挑战、架构设计方案及最终解决方案。
二、 系统整体架构设计
整个系统的多模态图片管理与端侧 AI 搜索架构分为:
#mermaid-svg-Pl9wa07p6qbUbFEd{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Pl9wa07p6qbUbFEd .error-icon{fill:#552222;}#mermaid-svg-Pl9wa07p6qbUbFEd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Pl9wa07p6qbUbFEd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Pl9wa07p6qbUbFEd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Pl9wa07p6qbUbFEd .marker.cross{stroke:#333333;}#mermaid-svg-Pl9wa07p6qbUbFEd svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Pl9wa07p6qbUbFEd p{margin:0;}#mermaid-svg-Pl9wa07p6qbUbFEd .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Pl9wa07p6qbUbFEd .cluster-label text{fill:#333;}#mermaid-svg-Pl9wa07p6qbUbFEd .cluster-label span{color:#333;}#mermaid-svg-Pl9wa07p6qbUbFEd .cluster-label span p{background-color:transparent;}#mermaid-svg-Pl9wa07p6qbUbFEd .label text,#mermaid-svg-Pl9wa07p6qbUbFEd span{fill:#333;color:#333;}#mermaid-svg-Pl9wa07p6qbUbFEd .node rect,#mermaid-svg-Pl9wa07p6qbUbFEd .node circle,#mermaid-svg-Pl9wa07p6qbUbFEd .node ellipse,#mermaid-svg-Pl9wa07p6qbUbFEd .node polygon,#mermaid-svg-Pl9wa07p6qbUbFEd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Pl9wa07p6qbUbFEd .rough-node .label text,#mermaid-svg-Pl9wa07p6qbUbFEd .node .label text,#mermaid-svg-Pl9wa07p6qbUbFEd .image-shape .label,#mermaid-svg-Pl9wa07p6qbUbFEd .icon-shape .label{text-anchor:middle;}#mermaid-svg-Pl9wa07p6qbUbFEd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Pl9wa07p6qbUbFEd .rough-node .label,#mermaid-svg-Pl9wa07p6qbUbFEd .node .label,#mermaid-svg-Pl9wa07p6qbUbFEd .image-shape .label,#mermaid-svg-Pl9wa07p6qbUbFEd .icon-shape .label{text-align:center;}#mermaid-svg-Pl9wa07p6qbUbFEd .node.clickable{cursor:pointer;}#mermaid-svg-Pl9wa07p6qbUbFEd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Pl9wa07p6qbUbFEd .arrowheadPath{fill:#333333;}#mermaid-svg-Pl9wa07p6qbUbFEd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Pl9wa07p6qbUbFEd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Pl9wa07p6qbUbFEd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Pl9wa07p6qbUbFEd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Pl9wa07p6qbUbFEd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Pl9wa07p6qbUbFEd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Pl9wa07p6qbUbFEd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Pl9wa07p6qbUbFEd .cluster text{fill:#333;}#mermaid-svg-Pl9wa07p6qbUbFEd .cluster span{color:#333;}#mermaid-svg-Pl9wa07p6qbUbFEd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Pl9wa07p6qbUbFEd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Pl9wa07p6qbUbFEd rect.text{fill:none;stroke-width:0;}#mermaid-svg-Pl9wa07p6qbUbFEd .icon-shape,#mermaid-svg-Pl9wa07p6qbUbFEd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Pl9wa07p6qbUbFEd .icon-shape p,#mermaid-svg-Pl9wa07p6qbUbFEd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Pl9wa07p6qbUbFEd .icon-shape .label rect,#mermaid-svg-Pl9wa07p6qbUbFEd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Pl9wa07p6qbUbFEd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Pl9wa07p6qbUbFEd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Pl9wa07p6qbUbFEd :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
鸿蒙 NEXT 系统能力层 (HarmonyOS NEXT)
核心逻辑与图谱映射层
UI 交互层 (ArkUI)
绝对路径 & 纯英文 Scope
物理拷贝与物理删除
GlobalVaultSearchModal全局 AI 语义搜图弹窗
BrainView文件树 & attachments 附件管理
ObsidianBottomBar右下角单手 👁️预览/✏️编辑 切换
BrainImageSearchManager端侧 AI 引擎调度 & 搜图
VaultManager沙箱读写 & 附件拷贝 & 文本扫描
Reverse Index Graph图片 ➔ Markdown 笔记反向双链图谱
CoreVisionKittextSearchImage 端侧 NPU 语义特征抽取
FileSystem Sandboxel2/base/files/AeroPlanVault/attachments/
三、 CoreVisionKit 端侧 AI 搜图的落地与“避坑”指南
在对接鸿蒙原生 @hms.ai.vision.textSearchImage 接口时,由于端侧 NPU 特征数据库与底层 C++ 引擎存在严格的校验规则,我们总结了以下 4 个必须规避的坑点:
1. Scope 命名限制:必须为纯英文字母与数字(1-32字符)
- 踩坑现象:传入 scope: "knowledge_brain" 会直接引发 NPU 引擎报错 Code: 401, Message: The parameter check failed.。
- 原因分析:CoreVisionKit 底层数据库对 scope 命名做正则校验,严禁包含下划线 _、减号或特殊字符。
- 解决方案:统一收敛为干净的纯英文标识符(例如 'aeroplan'):
private sanitizeScope(scope: string): string {
if (!scope) return 'aeroplan';
// 严格过滤非字母数字字符,限制长度在 1-32 位
const clean = scope.replace(/[^a-zA-Z0-9]/g, '');
return (clean.length > 0 && clean.length <= 32) ? clean : 'aeroplan';
}
2. 传入 NPU 的图片路径:必须剥离 file:// 协议头
- 踩坑现象:直接将 ArkUI 使用的 file:///data/storage/… 路径传给 textSearchImage.insertImage(),会导致报错 Code: 1013100001, Msg: Invalid image path or size.。
- 解决方案:传给 NPU 的物理路径必须是干净的系统绝对路径(如 /data/storage/el2/base/files/AeroPlanVault/attachments/xxx.png);而在 ArkUI Image() 组件渲染缩略图时,又必须拼回 file:// 前缀。
// 传给 CoreVisionKit 引擎
const realAbsPath = '/data/storage/el2/base/files/AeroPlanVault/attachments/Image_1.png';
await textSearchImage.insertImage({ scope: 'aeroplan', imagePath: realAbsPath, noteTitle: '笔记.md' });
// 传给 ArkUI Image 渲染
const uiSrc = `file://${realAbsPath}`;
Image(uiSrc)
3. NPU SQLite 特征库防死锁:全量同步前显式 clearData()
- 踩坑现象:多次重新同步沙箱图片后,搜索接口返回结果集突变为 0。
- 解决方案:在每次执行沙箱 attachments/ 全量图片特征重建时,先调用 textSearchImage.clearData() 重置端侧 SQLite 特征表:
public async syncAttachmentsFromVault(attachmentFiles: string[], attachmentsDir: string): Promise<number> {
// 1. 先重置端侧特征表,防止旧索引冲突
try {
await textSearchImage.clearData();
} catch (e) {
hilog.warn(0x0000, 'BrainImageSearch', `[ClearData Error] ${JSON.stringify(e)}`);
}
// 2. 依次插入新图片绝对路径与关联元数据
// …
}
四、 多模态图片与 Markdown 笔记的反向双链关联图谱
在实际应用中,单纯“搜出图片”是不够的。用户在搜到一张应用截图后,核心诉求是“这张图片是在哪篇笔记里被引用的?点击能否直接切回笔记?”。
反向双链关联与 AI 搜索流程图
#mermaid-svg-QBLaNKoLGrMlOgsJ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QBLaNKoLGrMlOgsJ .error-icon{fill:#552222;}#mermaid-svg-QBLaNKoLGrMlOgsJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QBLaNKoLGrMlOgsJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .marker.cross{stroke:#333333;}#mermaid-svg-QBLaNKoLGrMlOgsJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QBLaNKoLGrMlOgsJ p{margin:0;}#mermaid-svg-QBLaNKoLGrMlOgsJ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .cluster-label text{fill:#333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .cluster-label span{color:#333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .cluster-label span p{background-color:transparent;}#mermaid-svg-QBLaNKoLGrMlOgsJ .label text,#mermaid-svg-QBLaNKoLGrMlOgsJ span{fill:#333;color:#333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .node rect,#mermaid-svg-QBLaNKoLGrMlOgsJ .node circle,#mermaid-svg-QBLaNKoLGrMlOgsJ .node ellipse,#mermaid-svg-QBLaNKoLGrMlOgsJ .node polygon,#mermaid-svg-QBLaNKoLGrMlOgsJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QBLaNKoLGrMlOgsJ .rough-node .label text,#mermaid-svg-QBLaNKoLGrMlOgsJ .node .label text,#mermaid-svg-QBLaNKoLGrMlOgsJ .image-shape .label,#mermaid-svg-QBLaNKoLGrMlOgsJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-QBLaNKoLGrMlOgsJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QBLaNKoLGrMlOgsJ .rough-node .label,#mermaid-svg-QBLaNKoLGrMlOgsJ .node .label,#mermaid-svg-QBLaNKoLGrMlOgsJ .image-shape .label,#mermaid-svg-QBLaNKoLGrMlOgsJ .icon-shape .label{text-align:center;}#mermaid-svg-QBLaNKoLGrMlOgsJ .node.clickable{cursor:pointer;}#mermaid-svg-QBLaNKoLGrMlOgsJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .arrowheadPath{fill:#333333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QBLaNKoLGrMlOgsJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QBLaNKoLGrMlOgsJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QBLaNKoLGrMlOgsJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QBLaNKoLGrMlOgsJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QBLaNKoLGrMlOgsJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QBLaNKoLGrMlOgsJ .cluster text{fill:#333;}#mermaid-svg-QBLaNKoLGrMlOgsJ .cluster span{color:#333;}#mermaid-svg-QBLaNKoLGrMlOgsJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QBLaNKoLGrMlOgsJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QBLaNKoLGrMlOgsJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-QBLaNKoLGrMlOgsJ .icon-shape,#mermaid-svg-QBLaNKoLGrMlOgsJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QBLaNKoLGrMlOgsJ .icon-shape p,#mermaid-svg-QBLaNKoLGrMlOgsJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QBLaNKoLGrMlOgsJ .icon-shape .label rect,#mermaid-svg-QBLaNKoLGrMlOgsJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QBLaNKoLGrMlOgsJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QBLaNKoLGrMlOgsJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QBLaNKoLGrMlOgsJ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Yes
No
Yes
No
用户选择/插入相册图片
VaultManager 拷贝至 attachments/ 沙箱目录
扫描 Vault 所有 .md 笔记正文
正文是否包含图片文件名?
建立图谱映射: Image ➔ 真实笔记标题
标记映射: Image ➔ '未关联笔记'
调用 CoreVisionKit.insertImage 写入 NPU 索引
用户触发文本搜索: 如 '猫咪'
NPU 端侧向量计算返回匹配 Image
IsRealReferencedNote?
展现缩略图 + 📖 打开笔记 → 按钮
展示缩略图, 隐藏打开笔记按钮
点击跳转至底层 Markdown 笔记搜索弹窗常驻保留
为此,我们在 BrainImageSearchManager 中设计了 图片 -> Markdown 笔记的反向双链图谱(Reverse Index Graph):
// 1. 扫描 Vault 下所有 Markdown 笔记正文
const noteToImageMap: Map<string, string> = new Map();
const allNotes = await VaultManager.getInstance().listAllNotes();
for (let i = 0; i < allNotes.length; i++) {
const noteTitle = allNotes[i];
const noteContent = await VaultManager.getInstance().readNote(noteTitle);
if (noteContent) {
const lowerContent = noteContent.toLowerCase();
for (let j = 0; j < attachmentFiles.length; j++) {
const attFile = attachmentFiles[j];
// 检测 Markdown 正文是否包含该图片文件名
if (lowerContent.includes(attFile.toLowerCase())) {
noteToImageMap.set(attFile, noteTitle);
}
}
}
}
// 2. 注册精准的笔记关联关系至 NPU 搜图索引
const realLinkedNote = noteToImageMap.get(fileName) || '未关联笔记';
this.imageToNoteMap.set(realAbsolutePath, realLinkedNote);
await this.insertImage(realAbsolutePath, realLinkedNote);
搜索弹窗 UX 闭环:
五、 解决跨页面/路由生命周期的响应式刷新痛点
在 ArkUI 的单页多路由架构中,用户在 NoteDetailView(笔记编辑页)中插入相册图片后点击返回退出,常遇到“主界面 attachments/ 列表不展示新图片,必须重启 App”的问题。
针对这一问题,我们构建了 “即时 Event 广播 + 路由 Lifecycle 钩子” 的双重保障机制:
双重保障响应式刷新时序图
BrainView (主界面)Index.ets (NavDestination)AppStorage (全局状态)MarkdownEditor (编辑页)BrainView (主界面)Index.ets (NavDestination)AppStorage (全局状态)MarkdownEditor (编辑页)#mermaid-svg-KHNGBwOe959rjJfS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KHNGBwOe959rjJfS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KHNGBwOe959rjJfS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KHNGBwOe959rjJfS .error-icon{fill:#552222;}#mermaid-svg-KHNGBwOe959rjJfS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KHNGBwOe959rjJfS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KHNGBwOe959rjJfS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KHNGBwOe959rjJfS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KHNGBwOe959rjJfS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KHNGBwOe959rjJfS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KHNGBwOe959rjJfS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KHNGBwOe959rjJfS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KHNGBwOe959rjJfS .marker.cross{stroke:#333333;}#mermaid-svg-KHNGBwOe959rjJfS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KHNGBwOe959rjJfS p{margin:0;}#mermaid-svg-KHNGBwOe959rjJfS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KHNGBwOe959rjJfS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-KHNGBwOe959rjJfS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KHNGBwOe959rjJfS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-KHNGBwOe959rjJfS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-KHNGBwOe959rjJfS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-KHNGBwOe959rjJfS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-KHNGBwOe959rjJfS .sequenceNumber{fill:white;}#mermaid-svg-KHNGBwOe959rjJfS #sequencenumber{fill:#333;}#mermaid-svg-KHNGBwOe959rjJfS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-KHNGBwOe959rjJfS .messageText{fill:#333;stroke:none;}#mermaid-svg-KHNGBwOe959rjJfS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KHNGBwOe959rjJfS .labelText,#mermaid-svg-KHNGBwOe959rjJfS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-KHNGBwOe959rjJfS .loopText,#mermaid-svg-KHNGBwOe959rjJfS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-KHNGBwOe959rjJfS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KHNGBwOe959rjJfS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-KHNGBwOe959rjJfS .noteText,#mermaid-svg-KHNGBwOe959rjJfS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-KHNGBwOe959rjJfS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KHNGBwOe959rjJfS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KHNGBwOe959rjJfS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KHNGBwOe959rjJfS .actorPopupMenu{position:absolute;}#mermaid-svg-KHNGBwOe959rjJfS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-KHNGBwOe959rjJfS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KHNGBwOe959rjJfS .actor-man circle,#mermaid-svg-KHNGBwOe959rjJfS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-KHNGBwOe959rjJfS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}机制一:毫秒级状态广播机制二:路由 Lifecycle 闭环兜底用户插入相册图片 Image_xxx.png1AppStorage.setOrCreate('lastInsertedAttachment', filename)2@Watch('onAttachmentInserted') 响应3refreshVaultData() 刷新 […attachments]4点击返回退出编辑页5NavPathStack.pop() 页面退栈6NavDestination.onHidden() 触发7setOrCreate('lastInsertedAttachment', exited_timestamp)8触发强制二次同步9主界面实时展现最新图片列表 (无需重启 App)10用户
1. 插入瞬间触发 AppStorage 广播
// MarkdownEditor.ets 中完成图片物理拷贝后
const permanentUri = await VaultManager.getInstance().copyAttachment(imageUri, filename);
AppStorage.setOrCreate('lastInsertedAttachment', `${filename}_${Date.now()}`);
2. 主视图通过 @Watch 联动更新
// BrainView.ets
@StorageProp('lastInsertedAttachment') @Watch('onAttachmentInserted') lastInsertedAttachment: string = '';
async onAttachmentInserted() {
hilog.info(0x0000, 'BrainImageSearch', `[AppStorage Event] New image inserted: ${this.lastInsertedAttachment}`);
await this.refreshVaultData();
}
async refreshVaultData() {
const attachments = await VaultManager.getInstance().listAttachments();
// 必须重新赋予新解构数组引用 […attachments],确保 ArkUI 响应式列表瞬间重绘
this.attachmentFiles = attachments ? […attachments] : [];
// 自动重新建立反向索引图谱与 NPU 特征同步
const attachmentsDir = VaultManager.getInstance().getAttachmentsDir();
await BrainImageSearchManager.getInstance().syncAttachmentsFromVault(attachments, attachmentsDir);
}
3. 路由 onHidden 闭环兜底
// Index.ets (NavDestination 路由节点)
} else if (name === 'NoteDetail') {
NavDestination() {
NoteDetailView({ filename: param as string })
}
.hideTitleBar(true)
.onHidden(() => {
// 当退出 Markdown 编辑器返回主界面时,强制触发附件列表同步
AppStorage.setOrCreate('lastInsertedAttachment', `exited_${Date.now()}`);
})
}
六、 单手化移动端 UX 改造
为了打造媲美原生 Obsidian 的流畅移动端操作体验,我们对交互界面进行了以下优化:
效果图:单手移动端右下角切态与附件管理


七、 总结与展望
通过充分利用 HarmonyOS NEXT 的端侧 AI 算力与 ArkUI 响应式开发范式,我们在 AeroPlan 中构建了一套高隐私、高效率、多模态的端侧“第二大脑”知识库。
- 完全脱网安全:图片特征提取与语义检索全部在鸿蒙 NPU 端侧完成,不消耗流量,无隐私泄漏风险。
- 无缝多模态双链:打破了传统纯文本 Markdown 与二进制图片附件之间的壁垒。
- 高响应体验:借助状态监听与路由生命周期协同,提供了极速顺滑的即时反馈。
未来,我们将继续探索 CoreVisionKit 的 OCR 文本识别与 CoreSpeechKit 语音输入在个人知识库领域的深度融合,进一步提升鸿蒙原生应用的多模态生产力!


