File System Access API 实战:让网页真正读写本地文件
MarkViewhttps://markview.arthttps://github.com/acheding/markview一个纯前端的 Markdown 编辑器最尴尬的地方是它读不到你电脑上的文件——只能「导入一份副本」编辑完再「导出下载」源文件纹丝不动。File System Access API 改变了这件事showOpenFilePicker拿到的是一个文件句柄可以直接createWritable()写回原文件句柄还能存进 IndexedDB下次打开网页时恢复。MarkView 用它实现了CtrlO打开、CtrlS写回以及安装成 PWA 后双击.md文件直接打开。但真正的工作量不在调 API而在它带来的一整套状态问题权限会过期、文件会在别处被改、文件会被删掉、浏览器可能压根不支持。本文讲这些。一、基础三件套封装层是纯逻辑无 Vue、无应用状态// src/core/documents/fileAccess.jsconstMARKDOWN_PICKER_TYPES[{description:Markdown,accept:{text/markdown:[.md,.markdown]}}]// 用户取消AbortError返回空数组/null调用方静默即可其余异常照常抛出。constisPickerCancel(error)error?.nameAbortErrorexportconstpickOpenFilesasync(){try{returnawaitwindow.showOpenFilePicker({multiple:true,types:MARKDOWN_PICKER_TYPES})}catch(error){if(isPickerCancel(error))return[]throwerror}}第一个要处理的就是取消不是错误。用户按 Esc 关掉选择器浏览器抛AbortError如果不拦会弹一个「打开失败」的 toast非常无厘头。这里把它归一化成假值打开返回[]另存返回null调用方直接静默返回。写回是标准的三步但最后一步容易漏// 写回并返回写后的磁盘 mtime作为下次外部修改检测的基准。exportconstwriteFileHandleasync(handle,content){constwritableawaithandle.createWritable()awaitwritable.write(content)awaitwritable.close()constfileawaithandle.getFile()return{lastModified:file.lastModified}}close()之后再取一次lastModified——这个值是后面整套外部修改检测的锚点。少了它你自己写回的操作下一次就会被误判成「别人改了文件」。还有一个轻量版只取 mtime 不读内容// 只取 mtime 不读内容写回前的外部修改快检、focus 时的轻量轮询。exportconststatFileHandleasync(handle){constfileawaithandle.getFile()return{name:file.name,lastModified:file.lastModified}}二、句柄持久化能存但权限不能FileSystemFileHandle可以结构化克隆意味着能直接塞进 IndexedDB。MarkView 为此单开了一张表// v2新增 fileHandles 表持久化文档关联的 FileSystemFileHandle结构化克隆存储// 与文档主表分离——句柄无法参与内容签名比较不能混进增量写入的 documents 表。存取就是普通的put/getAll值直接是句柄对象。但权限不会跟着一起恢复。下次打开网页句柄还在queryPermission()返回prompt——你得重新要一次。而requestPermission()有个硬约束// 必须在用户手势keydown/click 等内调用否则浏览器直接拒绝。exportconstrequestFilePermissionasync(handle){if(typeofhandle?.requestPermission!function)returngrantedreturnhandle.requestPermission({mode:readwrite})}所以启动装载时只能query不能request那时没有用户手势真正要权限的时机有两个CtrlS 的 keydown 里和用户点击头部状态标签时。// CtrlS 的 keydown 是用户手势可直接请求权限跨会话恢复的句柄默认 prompt。if(link.permission!granted){constpermissionawaitrequestFilePermission(handle).catch(()denied)updateLink(docId,{permission})if(permission!granted){toast.show(未获得文件写入权限可用「另存为本地文件」保存,{tone:warning,duration:3000})returndenied}}UI 上「需要重新授权」这个状态被做成了可点击的状态标签——用户看到「重新授权文件访问」点一下就是一次合法的用户手势。这是把 API 约束翻译成交互设计的典型例子。顺带一提句柄不进响应式状态consthandlesnewMap()// docId → FileSystemFileHandle句柄不可比较内容不进响应式状态constlinksref({})// docId → link meta整体替换以触发响应驱动 UI 的是一份可序列化的元信息句柄本身只是一个不参与比较的副本。三、外部修改检测双基线这是全篇最有意思的部分。场景你在 MarkView 里打开了README.md切到 VS Code 改了几行再切回浏览器。这时候应该发生什么答案取决于两边各自改了没有于是需要两个基线// link meta 语义// savedSignature —— 上次与磁盘对齐时文档内容的签名null 基线未知跨会话恢复后尚未与磁盘核对。// lastModified —— 上次读/写时磁盘文件的 mtimenull 尚未核对。两者配合区分「本地脏」与「磁盘变了」。签名用 FNV-1a 加长度前缀够快够短// 内容签名FNV-1a 长度判断「当前内容是否与上次写回磁盘时一致」。// 只用于同一会话内的脏检查不做跨端一致性保证碰撞概率可忽略。exportconstcontentSignature(value){consttextString(value)lethash2166136261for(letindex0;indextext.length;index1){hash^text.charCodeAt(index)hashMath.imul(hash,16777619)}return${text.length}:${(hash0).toString(36)}}检测时机是三个事件没有轮询窗口focus、visibilitychange回到前台、切换文档。判定规则如下磁盘 mtime磁盘内容 vs 当前文档本地是否有未写回的编辑行为未变——直接返回不读内容变了相同—静默对齐基线变了不同没有静默重载磁盘版本 提示变了不同有弹窗让用户决定对应的代码constdiskNormalizednormalizeLineEndings(content)if(diskNormalizeddoc.content){// 内容一致mtime 变化来自外部 touch 或基线核对对齐基线即可。updateLink(docId,{savedSignature:contentSignature(doc.content),lastModified,name,/* … */})return}constlocalCleanlink.savedSignature!nullcontentSignature(doc.content)link.savedSignatureif(localClean){// 本地自上次同步后没动过安全地跟进磁盘版本。reloadFromDisk(docId,content,{name,lastModified})toast.show(已加载「${name}」的最新内容,{tone:success})return}// 双方都有变化或基线未知且内容不一致让用户决策绝不静默丢弃任何一方。constloadDiskawaitconfirm.ask({/* … */tone:danger})几个不那么显然的处理快路径不读内容。mtime 没变就直接返回连text()都不调——每次 focus 都全文读盘对大文件是浪费。「保留当前内容」也要对齐 mtime。// 对齐 mtime这次外部修改已知悉后续 CtrlS 直接覆盖、不再重复弹窗。updateLink(docId,{lastModified,name,missing:false,missingPrompted:false})否则用户选了「保留我的版本」下次 focus 又弹一遍同样的窗非常烦人。基线未知savedSignature null走「让用户决定」分支。跨会话恢复的句柄没有基线——上次会话结束后谁更新过无从判断。与其猜不如把这个场景收敛进已有的冲突分支而不是加一条特殊路径。重载时要丢编辑器缓存。constreloadFromDisk(docId,diskContent,{name,lastModified}){constnormalizednormalizeLineEndings(diskContent)documents.replaceContent(docId,normalized)if(documents.activeFileId.valuedocId)getEditor()?.setDoc(normalized)elsegetEditor()?.forgetDocument(docId)// …}活动文档直接setDoc后台文档则丢掉 CodeMirror 缓存的 EditorState否则切回去还是旧内容加一条污染的撤销栈。行尾归一化是签名一致性的前提。Windows 上的 CRLF 文件如果读进来不归一化每次比对都会「不一致」每次 focus 都误报外部修改// 统一行尾为 LF预设README 磁盘文件/导入文件可能带 CRLF而 marked 在词法分析时// 会把 token.raw 归一化成 LF源行标注data-source-line以 indexOf(token.raw) 回定位// 若 content 仍是 CRLF 则跨行 token 匹配失败滚动同步与搜索定位全部错位。故入库即归一化。写回前还有一次快检——因为 focus 检测和用户按 CtrlS 之间仍有时间窗// 写回前快检磁盘在别处被修改过则先确认避免静默覆盖外部编辑。以及一个容易忽略的时序细节updateLink(docId,{writing:true})// 确认框是异步的内容以落笔瞬间为准。constcontentgetDocById(docId)?.content??确认框弹出期间用户还能继续打字所以内容必须在确认之后才取。四、文件被删了、改名了、移走了这三种情况浏览器统一抛NotFoundError。处置逻辑最值得说的是三态确认框// 文件句柄因磁盘文件被移动、重命名或删除而失效时提示用户处置// 确认 另存到新位置转移关联取消按钮 取消关联、仅保留在 MarkView// Esc / 点遮罩 未做选择——保持丢失标记稍后可点击头部「本地文件已丢失」标签再处理。// 自动检查每次丢失只提示一次避免 focus/visibilitychange 反复打扰显式 CtrlS / 点标签可强制再次提示。confirm.ask返回true/false/null三种值null按 Esc不等于点了取消按钮if(choicetrue){if(documents.activeFileId.value!docId)returnfailedreturnsaveActiveAs()}// 关掉对话框Esc / 点遮罩暂不处置保留失效关联不静默改变文档归属。if(choice!false)returnfailed按 Esc 应该是「我先不处理」而不是「解除关联」。这个区分在测试里被单独钉死了一条用例。另外missingPrompted标志保证自动检查只弹一次窗而显式入口CtrlS、点状态标签传forcePrompt: true绕过它。文件如果「复活」了mtime 快路径命中标记自动清除。整套状态对外收敛成一个五值枚举exportconstFILE_LINK_STATUS{SYNCED:synced,// 内容与上次写回磁盘时一致DIRTY:dirty,// 有未写回磁盘的编辑WRITING:writing,// 正在写入磁盘PERMISSION:permission,// 跨会话恢复的句柄待重新授权MISSING:missing// 磁盘文件已被移动/删除}优先级是writing permission missing synced/dirty而 IndexedDB 保存失败永远优先于文件状态——「IndexedDB 保存失败是数据安全信号始终优先露出」。DOMException 到处置的映射整理成表错误名含义处置AbortError用户取消选择器静默NotAllowedError/SecurityError权限被撤销状态转PERMISSION标签可点重授权NotFoundError文件被删/移动/重命名走丢失处置流程其他未知记日志 error toast五、不支持的浏览器怎么办Firefox 和 Safari 目前都没有这套 API。MarkView 的降级思路是「打开」和「导入」本来就是两个并存的功能不支持时只是「打开」消失。// 「打开」与「导入」是两个并存的入口不做二选一// 打开 保留句柄、CtrlS 写回原文件仅支持 File System Access 时存在不支持的浏览器// 无按钮、不进面板、CtrlO 键位归还「导入」见 shortcuts.js// 导入 拷贝一份进工作区、与磁盘断开始终可用。快捷键的处理很巧妙——CtrlO在不支持的浏览器上「归还」给导入...(isFileSystemAccessSupported()?{open:{key:o},import:{key:o,alt:true}}:{import:[{key:o,alt:true},{key:o}]}),用户不必知道自己的浏览器缺什么CtrlO永远能打开点什么。能力检测分两级这点值得注意// 「打开/另存为」选择器需要 window.show*FilePickerlaunchQueue 句柄的写回只需 createWritable。exportconstisFileSystemAccessSupported()typeofwindow!undefinedtypeofwindow.showOpenFilePickerfunctiontypeofwindow.showSaveFilePickerfunction// launchQueue / 拖拽等外部来源的句柄是否可写回防御非 Chromium 实现给出只读句柄。exportconstisWritableFileHandle(handle)Boolean(handlehandle.kindfiletypeofhandle.createWritablefunction)全局 API 存在不代表每一个拿到的句柄都可写——外部来源launchQueue、其他实现可能给只读句柄所以建立关联前单独检测一次不可写就降级为普通导入副本。CtrlShiftS另存为也有降级不支持时直接走 Blob 下载。而导入始终用动态创建的input typefile// 每次新建实例重复选择同一文件也能触发 change。constopenImportPicker(){constinputdocument.createElement(input)input.typefileinput.accept.md,.markdown,text/markdown,text/plaininput.multipletrueinput.addEventListener(change,()importFiles(input.files),{once:true})input.click()}「每次新建实例」是为了绕开同一个 input 重复选同一文件不触发 change 的老坑。六、双击 .md 文件打开网页装成 PWA 后manifest 里注册文件处理器// 注册为 .md / .markdown 的系统文件处理器安装后双击这类文件即用 MarkView 打开// 应用侧由 launchQueue 消费者接收文件并导入为新文档见 createWorkspace。file_handlers:[{action:base,accept:{text/markdown:[.md,.markdown]}}],// 打开文件时优先复用已有窗口走 launchQueue而非每次新开一个实例。launch_handler:{client_mode:[focus-existing,auto]}应用侧接收constconsumeLaunchFilesasync(launchParams){consthandleslaunchParams?.filesif(!handles?.length)returnawaitwhenDocumentsReady()if(disk?.isSupported){awaitdisk.openViaHandles(handles)return}// …否则解析成 File 导入副本}constsetupFileHandling(){if(typeofwindowundefined||!(launchQueueinwindow))returnwindow.launchQueue.setConsumer(consumeLaunchFiles)}时序有讲究setConsumer必须尽早注册挂在onMounted里同步调用否则 launchParams 会丢但 consumer 内部要await whenDocumentsReady()——「避免导入被随后到达的初始状态覆盖」。注册要早动手要晚。同一个文件重复双击不该开出两份副本去重靠isSameEntry// 两个句柄是否指向磁盘上同一文件同文件去重实现缺失时按不同文件处理。exportconstisSameFileEntryasync(a,b){if(!a||!b||typeofa.isSameEntry!function)returnfalsetry{returnawaita.isSameEntry(b)}catch{returnfalse}}打开流程里还藏着一条顺序约束注释解释得很清楚// 先建关联再命名关联后名字被锁定setDocumentName 直接沿用磁盘文件名// 不参与重名加序号可与普通文档重名只读句柄降级为普通导入副本// 不建关联命名走普通文档的去重老规则。普通文档重名会自动加序号但关联了磁盘文件的文档必须跟磁盘同名——如果先命名后关联锁还没生效README.md会被改成README-1.md刷新后连磁盘文件名都跟着变。测试里专门有一条用例叫「建立关联先于命名」。七、这些浏览器 API 怎么测这套逻辑几乎全是异步 浏览器 API看起来很难测但实际上一个假句柄就够了——关键是让假句柄内建一份「虚拟磁盘」// —— 假句柄内建磁盘状态内容 / mtime / 权限行为与 FileSystemFileHandle 对齐 ——constmakeHandle(name,{content,lastModified1,permissiongranted}{}){constdiskState{name,content,lastModified,permission}consthandle{kind:file,diskState,queryPermission:vi.fn(async()diskState.permission),getFile:vi.fn(async()({name:diskState.name,lastModified:diskState.lastModified,text:async()diskState.content})),createWritable:vi.fn(async(){letpendingreturn{write:async(data){pendingdata},close:async(){diskState.contentpending diskState.lastModified1}}})}handle.isSameEntryvi.fn(async(other)otherhandle)returnhandle}close()才真正提交内容并让 mtime 自增——语义和真实 API 一致。于是模拟「别的程序改了文件」handle.diskState.content # 外部编辑; handle.diskState.lastModified 99模拟「文件被删了」让getFile抛NotFoundError模拟「权限被撤销」改diskState.permissionlaunchQueue也一样捕获 consumer 就能直接驱动// 捕获 launchQueue consumer模拟系统双击 .md 文件把句柄交给应用。conststubLaunchQueue(){letconsumernullvi.stubGlobal(window,{launchQueue:{setConsumer:(fn)(consumerfn)}})return()consumer}这里有个前提条件fileAccess.js里所有调用都写成window.showOpenFilePicker(...)而不是解构出来用才能整体vi.stubGlobal(window, …)替换掉。写法上多一个window.前缀换来的是整个模块可测。结语三条经验API 的约束会渗透到交互设计里——requestPermission必须在用户手势内于是「待授权」这个状态就得做成一个可点击的按钮这不是妥协是把技术约束翻译成了合理的 UI。冲突检测要双基线——只看 mtime 分不清「谁改的」只看内容签名不知道「磁盘动没动」两个基线加上一个「未知」态null四种场景就都收敛了。给假对象一份内部状态——测这类 API与其一个个 mock 返回值不如让假句柄自带虚拟磁盘close()提交内容、mtime 自增测试用例读起来就跟真实操作一样。

相关新闻

【Chroma】安装和运行 Chroma 向量数据库

【Chroma】安装和运行 Chroma 向量数据库

安装和运行 Chroma 向量数据库一、Windows 平台安装和运行 Chroma1. 安装2. 运行3. 测试二、Linux 平台安装和运行 Chroma1. 安装2. 运行3. 测试三、LuckReport 项目推荐1、项目简介2、在线体验四、学习交流最近在做 Agent 相关的项目,免不了要跟向量数据库打交道。…

2026/8/15 21:36:51 阅读更多 →
Linux动态链接器环境变量:LD_PRELOAD、LD_LIBRARY_PATH与LD_DEBUG详解

Linux动态链接器环境变量:LD_PRELOAD、LD_LIBRARY_PATH与LD_DEBUG详解

1. 项目概述:动态链接器的“后门”与“探照灯”在Linux这片广袤的天地里,我们每天都在和各种程序打交道。编译、运行、调试,看似顺理成章,但你是否想过,一个程序从磁盘上的二进制文件,到在内存中活蹦乱跳地…

2026/8/15 21:35:51 阅读更多 →
状态模式与策略模式深度辨析:从线上故障到实战应用

状态模式与策略模式深度辨析:从线上故障到实战应用

1. 从一次线上故障说起:为什么“策略”救不了“状态”那天晚上十一点,报警电话响了。线上一个核心的订单处理服务,在处理“待支付”转“已支付”的订单时,突然卡住了。日志里疯狂刷着“非法状态转换”的错误,但诡异的是…

2026/8/15 21:35:51 阅读更多 →

最新新闻

Python seek:一针见血定位文件,别再傻傻从头读了

Python seek:一针见血定位文件,别再傻傻从头读了

于文件操作之际, seek()方法乃是一个能高效定位文件指针的核心工具, 它可让开发者精准把控读写位置, 特别适用于大文件处理、日志分析等这类需要随机访问的场景。本文会从基础语法、参数详解、典型应用场景以及注意事项这四个方面予以展开说明。一、基础语法与参数解析seek()方…

2026/8/15 23:06:39 阅读更多 →
Hermes Agent 的系统要求是什么?

Hermes Agent 的系统要求是什么?

关键前提**:Hermes Agent 的硬件要求取决于你用哪种模型接入方式——是调用云端 API(轻量)还是在本地跑大模型(重量级),两者差距巨大。一、两种部署模式的硬件要求 模式 A:调用云端 API&#xf…

2026/8/15 23:06:39 阅读更多 →
Hermes Agent 与 CrewAI 的区别是什么?

Hermes Agent 与 CrewAI 的区别是什么?

Hermes Agent vs CrewAI 详细对比 核心结论(一句话) CrewAI 是"多 Agent 协作编排框架"——你定义一组角色,分配任务,它们协同完成;Hermes Agent 是"单 Agent 自进化系统"——一个持久运行的 Agen…

2026/8/15 23:06:39 阅读更多 →
2030年算力能耗预测:8000亿度电背后的技术挑战与绿色算力实践

2030年算力能耗预测:8000亿度电背后的技术挑战与绿色算力实践

这次我们来看一个关于未来算力与能源消耗的预测:到2030年,全国算力用电量预计将达到8000亿千瓦时。这个数字背后,是近6万亿度的绿电需求将涌入电网。这不仅仅是能源消耗的预测,更是对数据中心、AI算力、云计算基础设施乃至整个数字…

2026/8/15 23:06:39 阅读更多 →
电子商务专业毕业论文一键生成⚡这6个AI工具实测很好用

电子商务专业毕业论文一键生成⚡这6个AI工具实测很好用

电子商务专业的毕业论文,卡住的人比想象的要多。你说它偏管理吧,导师让你跑结构方程模型;你说它偏技术吧,又得大量引用消费者行为理论和营销模型。TAM模型怎么套、UTAUT量表从哪来、SPSS跑出来不显著怎么办、直播电商的数据怎么收…

2026/8/15 23:06:39 阅读更多 →
Python自动化监控网页更新并发送提醒

Python自动化监控网页更新并发送提醒

在跟进行业动态之际, 是不是老是忧心会错过关键的网页更新, 像竞争对手官网呈现新品发布的页面, 合作方政策调整所涉及的通知页面, 每日通过手动方式去刷新并查看着实耗费时间, 倘若有所遗漏便极有可能对业务决策造成影响。千万别让信息滞后致使工作节奏被拖缓, 它能够助力你自…

2026/8/15 23:05:38 阅读更多 →

日新闻

内景 空间站内部 中国空间站 太空 内仓

内景 空间站内部 中国空间站 太空 内仓

本项目为前几天收费帮学妹做的一个项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。 一、项目描述 空间站内部 中国空间站 太空 内仓 地址:本地PC端运行(或Web…

2026/8/15 0:00:30 阅读更多 →
重新定义数据接口:3个突破性场景让通达信数据读取更智能

重新定义数据接口:3个突破性场景让通达信数据读取更智能

重新定义数据接口:3个突破性场景让通达信数据读取更智能 【免费下载链接】mootdx 通达信数据读取的一个简便使用封装 项目地址: https://gitcode.com/GitHub_Trending/mo/mootdx 当我们面对海量金融数据时,传统的数据获取方式往往让我们陷入困境—…

2026/8/15 0:00:30 阅读更多 →
一文读懂快消WMS怎么选?2026年国内外10大主流WMS品牌盘点

一文读懂快消WMS怎么选?2026年国内外10大主流WMS品牌盘点

快消品(FMCG)是流通速度较快、竞争较为激烈的行业之一。一瓶饮料从出厂到消费者手中,往往只有几十天甚至几天的周转窗口。这决定了快消行业的仓储管理系统(WMS)与制造业、电商行业存在明显区别:它不仅需要管…

2026/8/15 0:02:30 阅读更多 →

周新闻

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁 【免费下载链接】baidupankey 在线查询网盘提取码(维护中 rm repo) 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 你是否曾经在深夜寻找一份重要资料&#x…

2026/8/13 2:38:34 阅读更多 →
如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/15 12:59:14 阅读更多 →
收藏!小白程序员轻松入门大模型,从Harness工程开始实践

收藏!小白程序员轻松入门大模型,从Harness工程开始实践

文章强调学习大模型不应只关注模型本身,而应重视模型外的系统搭建,即Harness。提出AgentModelHarness的实用公式,详细介绍Harness的四个层次:持久化层、执行层、控制层和观察与验证层。文章还探讨了上下文工程、工具设计、AGENTS.…

2026/8/13 10:41:51 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/14 13:40:53 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/14 14:06:45 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/15 2:35:29 阅读更多 →