鸿蒙 PC Markdown 编辑器外部修改检测:从文件指纹到冲突决策

发布时间:2026/7/23 13:50:38
鸿蒙 PC Markdown 编辑器外部修改检测:从文件指纹到冲突决策 鸿蒙 PC Markdown 编辑器外部修改检测从文件指纹到冲突决策桌面 Markdown 文件很少只被一个程序触碰。用户可能在终端执行格式化脚本、在 Git 客户端切换分支、让生成器更新文档或者同时打开另一款编辑器。应用如果假设“打开之后磁盘永远不变”下一次保存就可能无提示覆盖外部内容反过来如果任何时间戳变化都弹出冲突又会把自己的保存、元数据更新和无内容变化误判成风险。OhMarkdown 在鸿蒙 PC 版本中建立了活动文档外部修改检测。公开仓库是 https://gitcode.com/VON-/codex_md_oh基础实现进入提交57aea97完整冲突比较进入5cb4aed。本文讨论文件指纹、轮询、正文复核、干净重载和脏缓冲区保护不把尚未完成的全工作区文件监听或云端同步描述成现有能力。外部修改检测首先是数据所有权问题打开文件后系统同时存在至少三份相关事实磁盘当前版本、编辑器当前缓冲区、应用上次成功保存或打开时的持久化基线。只比较磁盘与缓冲区无法判断是谁改变了什么只记录时间戳又无法解释内容是否真的变化。三方模型是可靠冲突处理的基础。持久化基线代表应用最后确认的磁盘内容。缓冲区可能在基线上产生本地修改磁盘也可能被外部程序改写。若只有磁盘变化而缓冲区干净应用可以自动重载若两边都变化就必须暂停自动保存并让用户决策若指纹变化但正文与格式仍等于基线只需更新指纹不应打扰用户。这个模型也说明检测不是保存逻辑的附加弹窗。它直接影响 dirty、自动保存、标签关闭和恢复快照。外部变化判断必须属于文档可靠性状态机而不是一个每隔几秒显示 Toast 的独立功能。指纹用于筛选而不是证明内容相同文档服务读取文件后记录DocumentFingerprint主要包含字节长度与纳秒级修改时间。它的作用是廉价判断“是否值得重新读取”而不是充当内容哈希。真实接口类似exportinterfaceDocumentFingerprint{size:number;mtimeNs:number;}exportfunctionisSameDocumentFingerprint(left:DocumentFingerprint|undefined,right:DocumentFingerprint):boolean{returnleft!undefinedleft.sizeright.sizeleft.mtimeNsright.mtimeNs;}文件大小相同不代表正文相同修改时间也可能因复制、恢复或文件系统粒度产生碰撞。因此指纹相同时可以跳过大部分轮询读取指纹不同时则重读正文、BOM 和换行格式。最终用户数据决策基于实际内容与格式而不是只看元数据。没有在每次轮询计算整文件 SHA-256是性能和用途上的取舍。哈希仍需读取全部文件与直接读取并解析内容的 I/O 成本相近而且应用最终还要获得磁盘正文用于重载或比较。指纹做第一层过滤正文做第二层确认更符合活动文档场景。轮询只覆盖活动文档当前实现通过组件生命周期启动定时器只检查活动文档privateinitializeDocumentReliability():void{if(this.externalChangeTimerId0){this.externalChangeTimerIdsetInterval((){this.checkActiveDocumentForExternalChanges();},EXTERNAL_CHANGE_POLL_MILLISECONDS);}}aboutToDisappear():void{this.cancelScheduledAutoSave();if(this.externalChangeTimerId0){clearInterval(this.externalChangeTimerId);this.externalChangeTimerId-1;}}只看活动文档并非完整文件监听但它有明确预算。用户当前正在编辑的文件风险最高所有后台标签同时轮询会随着标签数量线性增加 I/O。切换标签时应用读取目标会话和指纹后续轮询转到新活动文档。组件消失时必须清理定时器否则页面重建后会出现多个轮询并发旧实例还可能更新已经失效的状态。生命周期清理与启动条件同等重要。定时器只触发检查检查函数自己还有重入保护避免上一次慢 I/O 未结束时启动下一轮。检查入口先拒绝不合适的时机核心方法的第一段不是读取而是条件过滤privateasynccheckActiveDocumentForExternalChanges():Promisevoid{if(this.externalChangeCheckInProgress||this.operationInProgress||this.documentUri.length0||this.externalConflictVisible){return;}constsessionIdthis.activeDocumentSessionId;constdocumentUrithis.documentUri;this.externalChangeCheckInProgresstrue;try{constfingerprintawaitreadDocumentFingerprint(documentUri);if(sessionId!this.activeDocumentSessionId||documentUri!this.documentUri||isSameDocumentFingerprint(this.documentFingerprint,fingerprint)){return;}// 指纹变化后再读取正文。}finally{this.externalChangeCheckInProgressfalse;}}文件操作进行中时跳过是为了避免把应用自己的原子保存中间状态识别为外部变化。未命名文档没有磁盘目标不需要检查。冲突已经显示时暂停继续轮询防止磁盘再次变化不断覆盖待比较版本。externalChangeCheckInProgress阻止定时器重入。更关键的是捕获sessionId和documentUri。异步stat等待期间用户可能切换标签恢复后必须确认仍在同一会话和 URI。只依赖组件当前字段会把旧文件结果应用到新标签这是多文档编辑器里非常隐蔽的竞态。指纹变化后读取正文与格式当元数据发生变化服务调用readUtf8Document。该函数不是普通文本读取它同时检测 UTF-8 BOM、换行类型、内容和新指纹。检查逻辑随后区分三种情况constdiskDocumentawaitreadUtf8Document(documentUri);if(sessionId!this.activeDocumentSessionId||documentUri!this.documentUri){return;}if(this.persistedDocumentContentdiskDocument.contentthis.isSameDocumentFormat(this.documentFormat,diskDocument.format)){this.documentFingerprintdiskDocument.fingerprint;this.syncActiveDocumentSession();}elseif(this.documentDirty){this.registerExternalConflict(diskDocument);}else{this.applyExternalDiskDocument(diskDocument);}第一种情况说明正文和格式没有变化只是元数据不同。应用更新指纹不提示。第二种情况是本地 dirty磁盘也变化进入冲突。第三种是本地干净安全应用磁盘版本。格式必须参与比较。磁盘可能只从 LF 改成 CRLF或增加 UTF-8 BOMJavaScript 字符串正文看起来相同但字节事实已经改变。若忽略格式下一次保存会静默恢复旧格式。无损编辑器不能把换行和 BOM 当成无关元数据。干净缓冲区可以自动重载当本地没有修改外部变化可直接应用。实现会更新 URI、名称、正文、持久化基线、格式、指纹、修订、dirty 和状态再调用 Web 编辑器设置文档privateapplyExternalDiskDocument(diskDocument:OpenedDocument):void{this.clearExternalConflict();this.documentUridiskDocument.uri;this.documentNamediskDocument.name;this.documentContentdiskDocument.content;this.persistedDocumentContentdiskDocument.content;this.documentFormatdiskDocument.format;this.documentFingerprintdiskDocument.fingerprint;this.documentRevision0;this.documentDirtyfalse;this.operationStatusReloaded after external change;this.setEditorDocument(diskDocument.content);this.clearRecoveryDraft();this.syncActiveDocumentSession(diskDocument.content);}重置修订和 dirty 很重要因为外部磁盘版本此刻成为新的基线。恢复草稿也应清理否则下次启动可能把旧干净内容当作未保存恢复。所有字段一起更新避免标签标题、状态栏和 Web 正文各自处于不同版本。自动重载仍有体验取舍光标和滚动位置可能变化。当前实现优先保证正文事实正确后续可在内容差异较小且偏移有效时尝试恢复选区。不能为了保留光标而继续展示过期内容更不能在没有提示的情况下把旧缓冲区重新写回磁盘。脏缓冲区必须进入显式冲突本地 dirty 表示缓冲区包含尚未写入的用户工作。外部磁盘版本也可能包含其他工具的有效修改。实现将磁盘文档按活动会话存入externalConflicts显示冲突栏取消待执行自动保存privateregisterExternalConflict(diskDocument:OpenedDocument):void{this.externalConflicts.set(this.activeDocumentSessionId,diskDocument);this.externalConflictVisibletrue;this.cancelScheduledAutoSave();this.operationStatusExternal changes need attention;}冲突按会话存储而不是单个全局对象。用户可能在一个标签处理冲突时切换另一个标签后者的磁盘版本不能覆盖前者。活动会话切换时原生层根据映射恢复对应冲突可见状态。自动保存被立即暂停因为它最可能造成无提示覆盖。手动保存也会在写入前检查指纹不能借按钮绕过。冲突栏提供 Compare、Keep Local、Use Disk 和 Save As用户可以看到选择后果而不是只收到“文件已变化”的死路提示。文件暂时不可用时宁可暂停文件可能被移动、删除、卸载或权限失效。检查捕获异常后如果会话仍匹配会取消定时自动保存并显示File unavailable; auto save paused。应用不会把读取失败解释为“文件不存在可以新建覆盖”也不会无限重试写入。这种错误是可恢复的。用户可以恢复挂载、重新授权或 Save As 到新位置编辑缓冲区与恢复快照仍保留。真正危险的是错误降级为普通保存如果 URI 指向被重新创建的不同文件自动写入可能覆盖不相关内容。根目录权限和单文件 URI 的生命周期需要在鸿蒙 PC 真机上继续验证。模拟器能覆盖文件删除和修改但无法代表所有外接存储、网络盘或企业策略。当前报告因此只确认本地授权文件路径。原子保存与检测必须协同应用自己的保存会改变 mtime 和大小。如果轮询恰好在 AtomicFile 提交前后运行可能看到临时状态。operationInProgress让检查避开显式文件事务保存完成后更新新指纹使下一轮不误报。不过操作锁不能成为唯一依据。系统调度可能让检查先开始随后用户触发保存。捕获会话和 URI、正文复核以及保存路径自身的冲突检查共同降低竞态风险。未来若采用文件观察 API也仍需处理自写事件和外部事件去重。保存前检查与后台轮询解决不同窗口。轮询尽早提示保存前复核守住最终写入时刻。只做轮询会在两个周期之间漏掉变化只在保存时检查则让用户长时间编辑过期基线。两者结合才符合桌面编辑器预期。三方比较建立在真实基线上进入 Compare 后应用向 Web 传递持久化基线、本地缓冲区和磁盘正文。三列并不是“旧、新、另一个旧”的装饰而是分别回答共同起点是什么、本地写了什么、磁盘被改了什么。格式信息也随元数据传递。小中型文档显示完整三方逐行差异总字符数超过上限时降级为有界摘要避免构造巨大 DOM 冻结界面。外部检测本身不承担差异算法但必须保存正确磁盘快照否则用户打开比较时磁盘再次变化会让决策对象漂移。Keep Local 使用冲突时捕获的磁盘版本作为新基线再允许用户保存Use Disk 应用捕获版本前需要二次确认Save As 将本地缓冲区写到新 URI。每个动作都围绕三份事实更新而不是简单切换一个布尔值。真实冲突界面下图来自 MateBook Pro 2in1 模拟器。文档本地已有修改同时外部程序改写文件后应用没有自动覆盖而是显示显式冲突栏选择比较后可以查看基线、本地和磁盘三方内容截图对应仓库中的设备报告docs/test/ohmarkdown/2026-07-18-g3-03-document-reliability/。它证明本地模拟器路径与界面状态成立不代表远程文件系统和所有第三方编辑器已经兼容。自动化测试应该制造真实磁盘变化仅调用registerExternalConflict不能证明检测有效。设备测试需要先通过应用打开文件再从测试进程改写字节等待轮询确认干净文档重载或脏文档出现冲突。格式测试还应只改变 BOM 或换行验证正文相同时仍识别字节变化。纯函数测试适合覆盖指纹比较、格式解析和三方摘要。Playwright 适合覆盖三方视图和关闭行为。ohosTest 适合验证 CoreFileKit 读取和字节保存。模拟器手工路径验证工作台、系统文件和焦点。当前统一基线的 Playwright 为29/29、ohosTest 为7/7外部检测主体在57aea97与5cb4aed完成。时间相关测试应使用可控等待或轮询条件避免硬编码“睡眠两秒必然触发”。真机 I/O 调度可能有抖动。更稳的断言是等待状态达到目标并设置总体超时同时记录实际耗时。性能与扩展边界活动文档指纹检查通常只做一次stat开销较低只有变化时才读取正文。对单个活动文件这一策略能在响应性与资源之间取得平衡。对于数百标签或整个工作区它不适用因此当前没有扩展到全量轮询。大文件变化时重读会占用内存和 I/O。检查函数不会在 UI 线程进行正文比较之外的复杂渲染三方视图还有字符上限。G3-10 需要在鸿蒙 PC 真机测量 10 MiB 级文档的轮询、重读、冲突显示和输入响应不能用小文件模拟器截图推断性能领先。如果平台提供可靠的文件观察 API可以用事件唤醒替代固定轮询但事件仍需指纹去重和正文复核。观察 API 可能丢事件、合并事件或只提供目录级通知不能删除当前一致性模型。实现替换的是触发方式不是三方事实。安全与隐私边界检测只访问用户通过系统选择器授权的活动文档 URI不扫描任意目录不上传哈希或正文也不申请网络。磁盘快照保存在内存中的活动会话冲突映射恢复记录位于应用沙箱。冲突界面通过textContent或受控编辑器展示不把 Markdown 当可执行 HTML。错误消息不应泄漏完整系统路径到不必要的遥测。当前项目没有远程遥测状态栏使用通用描述。未来若加入诊断日志应默认脱敏 URI仅在用户主动导出诊断包时提供可审查信息。符号链接和工作区搜索有额外NOFOLLOW规则单文件授权 URI 的外部修改检测遵循系统授权结果。应用不尝试绕过权限读取原路径这使“无法访问”成为正常错误状态而不是使用更高权限解决。没有采用的方案没有仅靠 mtime 自动重载因为同大小修改、时间戳复制和格式变化会产生误判。没有每轮计算哈希因为仍需读全文且哈希不能替代格式和正文快照。没有默认自动合并因为 Markdown 行级合并可能把语义冲突伪装成成功当前没有成熟合并引擎和用户验收。没有为所有打开标签创建独立定时器。那会让资源消耗随标签增长并制造多个并发 Bridge 状态。当前只保证活动文档后台标签在激活时复核。这个边界明确记录在项目状态中。没有在文件消失时立即创建新文件。删除可能是用户或版本控制的意图自动重建会对抗外部操作。应用保留缓冲区并暂停保存把 Save As 作为显式恢复路径。验收清单回归外部修改检测时需要覆盖元数据未变不重读只改变 mtime 不弹冲突干净正文变化自动重载dirty 与磁盘同时变化显示冲突只改变 BOM 或换行也被识别应用自己的保存不误报切换标签后旧检查结果不落到新会话文件删除暂停自动保存冲突期间不继续轮询覆盖快照Keep Local、Use Disk、Save As 更新正确基线。设备矩阵还应包括MateBook 模拟器、鸿蒙 PC 真机 Debug 与 Release、本地文档目录、外接存储、版本控制切换、输入法激活、多窗口切换和大文件。每项都需要记录文件字节、UI 状态和操作耗时而不是只保留一张提示截图。结论外部修改检测的核心不是轮询而是三方事实与不可逆操作的约束。文件指纹负责低成本发现变化正文和格式负责确认活动会话令牌阻止异步串线自动保存暂停保护本地与磁盘两份工作三方比较帮助用户做最终决策。OhMarkdown 当前已完成本地活动文档纵切并通过 MateBook Pro 2in1 模拟器验证。全工作区事件监听、自动合并、远程存储和真机长期稳定性仍是后续项。明确这些边界并不会削弱产品反而确保每一次“已完成”都能对应真实代码、文件字节和设备证据。