鸿蒙 PC Markdown 编辑器文件拖放:文档会话与图片资源分流
鸿蒙 PC Markdown 编辑器文件拖放文档会话与图片资源分流桌面用户把文件拖进编辑器时动作看起来完全相同语义却可能相反。拖入 Markdown 文档通常表示“打开它”拖入图片通常表示“把资源放到当前文档并插入链接”拖入不支持文件应该明确拒绝同时拖入文档和图片则不能悄悄打开一部分、复制另一部分。若所有文件都送进同一个“导入附件”函数最常见的结果是把.md当图片读取失败或在失败前已经修改了一半状态。本文讨论 OhMarkdown 在 HarmonyOS PC / 2in1 上完成的原生 UDMF 拖放分流。代码来自 https://gitcode.com/VON-/codex_md_oh对应提交b11519c。图片 Copy、Move、Reference 已在此前模拟器真实文件拖放中通过本轮新增 Markdown/TXT 多文档打开、混合批次整体拒绝和自由窗口入口已经通过 ArkTS 编译与纯函数测试最新系统文件管理器拖入仍等待设备恢复后验收。拖放不是一种文件上传Web 应用经常把dataTransfer.files统一上传服务器但本地优先编辑器没有“上传”这个默认语义。用户文件继续留在原位置Markdown 是文档事实来源图片是否复制或移动由明确设置决定。HarmonyOS 的 UDMF 记录也可能提供Image、File、general.file-uri或具体图片类型不能只检查一个 MIME。因此拖放处理分成四层从 DragEvent 安全提取 URI对 URI 做文档、图片或不支持分类在整批层面决定接受或拒绝最后分别进入文档会话服务或图片资源服务。分类层不读取内容事务层才执行可能失败的 I/O。这个顺序让失败容易解释。系统没有提供 URI时是“无法读取文件 URI”扩展名不支持时是“只接受 Markdown 或受支持图片”文档与图片混合时是“同一批次只能选择一种类型”文档 UTF-8 解码失败则由 DocumentService 给出具体错误。用户不必从“图片导入失败”猜测为什么拖入 README 没有打开。从 UDMF 提取 URI 要兼容多种记录HarmonyOS 系统文件管理器可能把图片表示为unifiedDataChannel.Image普通文件表示为File也可能通过通用记录和值结构给出 URI。实现遍历记录按具体类优先提取再检查受限类型集合最多保留十六个去重 URI防止异常拖放一次制造无界任务。privateextractNativeDroppedFileUris(event:DragEvent):Arraystring{constsourceUris:Arraystring[];constrecordsevent.getData().getRecords();records.forEach((record:unifiedDataChannel.UnifiedRecord){letsourceUri:string;if(recordinstanceofunifiedDataChannel.Image){sourceUrirecord.imageUri;}elseif(recordinstanceofunifiedDataChannel.File){sourceUrirecord.uri;}else{constrecordTyperecord.getType();constvaluerecord.getValue();if(typeofvaluestring(recordTypegeneral.file-uri||recordTypegeneral.file||recordType.startsWith(general.image))){sourceUrivalue;}}if(sourceUri.length0!sourceUris.includes(sourceUri)sourceUris.length16){sourceUris.push(sourceUri);}});returnsourceUris;}日志只记录类型集合不记录文件正文或可恢复的敏感信息。真实 URI可能包含用户目录结构正式报告只保存脱敏路径、文件名、大小和结果。提取异常被捕获后返回空集合外层按可见失败处理不让 DragEvent 异常终止整个工作台。扩展名分类只是第一道门Markdown 文档支持.md、.markdown、.mdown、.mkd和.txt与系统打开选择器一致。URI可能是本地路径也可能是 file URI文件名包含百分号编码和中文。DocumentService 复用已有 URI 解析取得末段解码后再做大小写不敏感扩展判断exportfunctionisSupportedMarkdownDocumentUri(documentUri:string):boolean{return/\.(?:md|markdown|mdown|mkd|txt)$/i.test(getDocumentName(documentUri));}扩展名通过不代表内容可信。真正打开仍进入readUtf8Document执行 20 MiB 上限、UTF-8 fatal 解码、BOM 与换行检测、完整读取和文件指纹建立。分类只是避免错误路由不能替代内容验证。图片分类限制为 PNG、JPEG、GIF 和 WebP与 AssetService 的 MIME 白名单一致。URI 查询和 fragment 不参与扩展判断。最终 AssetService 还会根据实际文件名推导 MIME、检查 10 MiB 大小、处理目标目录和命名冲突所以 WorkspaceShell 的分类同样不是安全终点。为什么混合批次要整体拒绝假设用户一次拖入两个 Markdown 和一张图片。若先打开两个文档再把图片插入“当前文档”图片究竟属于拖放前的文档、最后打开的文档还是鼠标落点对应的标签任何默认答案都可能违背预期。若图片复制成功而第二个文档打开失败用户还要判断哪些动作已经发生。本轮采用确定规则同一批次只能全是受支持文档或全是受支持图片任何不支持项或混合类型使整批在 I/O 前拒绝。DragResult 设置为失败状态栏解释原因文档会话和磁盘都不变化。这不是永久限制而是 Beta 阶段的安全语义。未来若要支持混合批次需要先设计目标文档、执行顺序、部分失败回滚和结果摘要不能在循环中顺手实现。对本地编辑器来说少做一次模糊自动化通常比制造难以察觉的半成功更专业。Markdown 拖放进入多文档会话纯文档批次在操作锁内逐个调用readUtf8Document然后进入已有applyOpenedDocument。这个函数会检查相同 URI 是否已打开已打开时激活原标签不创建重复会话新文档在十二标签上限内增加会话空白且未修改的 Untitled 标签可以被首个文档复用。privateasyncopenNativeDroppedDocuments(sourceUris:Arraystring):Promiseboolean{this.operationInProgresstrue;try{for(constsourceUriofsourceUris){constopenedDocumentawaitreadUtf8Document(sourceUri);awaitthis.applyOpenedDocument(openedDocument);}this.operationStatussourceUris.length1?Dropped document opened:Dropped documents opened;returntrue;}catch(error){this.operationStatusDocument drop failed:${errorinstanceofError?error.message:String(error)};returnfalse;}finally{this.operationInProgressfalse;}}每次切换前会捕获原活动标签的最新 CodeMirror 缓冲区防止用户刚输入但 Bridge 状态尚未刷新时丢失。新文档保存基线、编码、换行、指纹和大文档模式都由标准打开链路建立。拖放只是绕过选择器的入口不创造另一种“临时文档”。当前多文档打开是顺序提交不承诺整批回滚。如果第一份有效、第二份读取失败第一份会保持打开状态会指出失败。这与系统多选打开的常见语义一致但设备报告必须记录具体成功数。混合类型在读取前整体拒绝文件内容错误则属于执行期失败两者边界不同。图片拖放继续遵循资源事务纯图片批次复用 G3-04 已验证的三模式Copy 把源复制到文档资源目录Move 在完整写入、fsync 和链接插入之后删除源Reference 只允许受管理的文档.assets或共享assets内现有资源不把任意外部绝对路径写入 Markdown。工作台在导入前检查当前文档是否有可写父目录、是否存在外部冲突、是否已有其他文件操作。每张图片落盘成功后才调用 Web 的insertNativeDroppedAsset插入标准相对链接。会话 ID在整个导入期间保持一致用户不能在中途切到另一标签让链接进入错误文档。拖入 Markdown 不需要当前文档父目录因为它只是打开文件拖入图片需要资源目标。把这两个前置条件放在分流后非常重要。旧实现先检查父目录会导致用户从文件管理器拖入 README 时收到“请先打开文档文件夹”与实际任务无关。DragResult 与后台异步工作的关系系统拖放回调需要尽快设置DragResult真正读取文件则是异步工作。提取与分类同步完成后应用即可判断是否接受。合法纯文档或纯图片设置DRAG_SUCCESSFUL再启动对应异步函数非法和冲突状态设置DRAG_FAILED。成功 DragResult 表示应用接受了任务不表示磁盘操作最终成功。后续 UTF-8、权限、大小或写入错误会进入状态栏。设备测试要同时观察系统拖放反馈和最终应用状态不能只看鼠标释放时的动画。图片 Move 模式仍然设置 COPY 行为反馈因为系统源删除由应用在事务完成后执行而不是让系统在 DragEvent 返回时自动移动。这样应用能够保证目标落盘和链接插入先成功再删除源若直接声明系统 MOVE可能失去对删除时序的控制。原生标签栏为什么是拖放承载面拖放绑定在 ArkUI 标签栏而不是只绑定 Web 编辑区。系统文件管理器的 UDMF 数据由原生组件直接获得不需要给 ArkWeb 开放文件访问或把绝对 URI发送进 JavaScript。Web 只收到已经落盘的相对图片链接文档打开则由 ArkUI 会话更新。标签栏横跨整个编辑工作区顶部用户在源码、分栏或预览模式都能找到稳定目标。编辑区本身仍保留浏览器内图片粘贴与拖放用于 Web File 对象系统原生文件拖放优先走 ArkUI。两条路径最终汇入相同 AssetService不产生不同命名规则。应用内部证据下图来自 HarmonyOS MateBook Pro 2in1 模拟器中的真实 OhMarkdown 设置侧栏展示图片拖放的复制、移动和仅引用三种持久化模式。它记录了原生拖放资源语义已经进入应用而不是浏览器演示页。这张图属于已完成的图片拖放设备证据。提交b11519c新增的 Markdown 多文件打开和混合批次拒绝尚未在本轮模拟器截图原因是 Mac 锁定且hdc无在线目标。文章不会把旧图描述成新文档拖放已通过最新设备图将在 G3-09 闸门执行后补入本地资产目录。自动化与构建验证ArkTS 普通单元测试新增扩展名识别中文 file URI 的.md、大写.MARKDOWN和.txt返回真PNG 返回假。UnitTestBuild让静态接口、URI解析和正则进入工程门禁。Debug HAP 编译确认 UDMF 类型判断、DragEvent、会话调用和响应式布局都符合 API 24。完整./scripts/verify-local.sh中 Web 回归 43/43 通过说明拖放分流改动没有破坏 Web 图片粘贴、图片拖放、持久预览、多标签或导出。entryohosTestHAP 构建成功但最新包尚未设备执行。真正无法由无头浏览器证明的是系统文件管理器产生的记录类型、URI授权持续时间、多选顺序、DragResult 视觉反馈和物理鼠标落点。因此测试用例把单文档、多文档、重复文档、十二标签上限、图片三模式、混合批次、不支持文件和无权限文件逐项列为模拟器路径。文件安全回归拖放打开不会复制 Markdown不改变源文件不自动保存也不把工作区之外的文件加入目录树。用户编辑后仍通过原 URI安全保存外部修改指纹和三方冲突保护继续生效。重复拖入相同 URI只激活会话避免两个标签同时编辑同一文件产生覆盖竞争。图片 Copy/Move/Reference 继续遵循写入成功后插链接。Move 删除源前已确认目标 fsync 和编辑器插入Reference 只允许受管理资源目录失败不修改正文。新增文档分流没有放宽 AssetService 的路径、大小、MIME 或 Base64 限制。不支持文件不会尝试以 UTF-8 打开也不会作为图片复制。混合批次在任何 I/O 前拒绝减少半成功。最多十六个 URI和十二文档会话上限共同限制一次拖放的资源占用。大文件与性能拖入文档使用 64 KiB 分块读取和 20 MiB 上限UTF-8 decoder 使用 fatal 模式不把无效字节替换成乱码后保存。超过 5 MiB 的正文进入大文档保护模式关闭高成本预览但保留源码编辑。多文件顺序读取避免同时分配多个 20 MiB 缓冲。分类只处理 URI字符串不访问磁盘DragEvent 可以快速返回。图片仍受 10 MiB 单文件与 16 个记录限制。设备性能测试应记录从释放鼠标到标签可输入的耗时、文件大小、文档数量和内存仅记录系统拖放动画没有意义。1000 文件工作区的性能与一次拖入最多十六个文档不同。前者由目录树惰性加载和 TaskPool 搜索负责后者是用户明确选择的多文档打开。实现没有为拖放建立后台数据库或索引保持 G3 的 Level 2 / D2 边界。自由窗口中的拖放反馈在 900 vp 以下侧栏变为覆盖层标签栏仍保持可见用户可以先关闭侧栏再拖入文件。系统拖放目标宽度随窗口变化但不会因为侧栏停靠压缩到低于核心编辑预算。低于 720 vp 时状态栏精简元数据仍保留操作状态用于显示“正在打开拖入文档”或具体失败。窄窗口设备验收要检查拖放高亮是否覆盖正确目标文件释放后标签是否能滚动显示多文档打开是否超出工具栏错误状态是否被截断但可理解。这些视觉问题无法从 ArkTS 编译推断必须由模拟器截图和操作记录收口。可访问性替代路径拖放天生依赖指针不能成为唯一入口。单文件仍可通过CtrlO、工具栏、文件树和命令面板打开图片可以通过粘贴进入同一资源服务工作区文件可通过快速打开搜索。拖放失败不会让键盘路径失效。系统 DragResult 和状态栏提供反馈错误不会只靠颜色。文件类型规则与选择器后缀一致用户可以从错误中理解怎样修正。后续无障碍审查还需确认文件管理器与应用之间的拖放是否有系统辅助技术支持但产品基础能力不依赖它。竞争优势怎样量化拖入 Markdown 打开和图片资源分流是专业编辑器的常见能力优势要通过任务连续性证明。统一任务可以要求用户从文件管理器拖入三份 Markdown、切换其中一份、拖入图片、保存并重新打开记录操作数、路径正确率、失败恢复和总耗时。OhMarkdown 的目标不是“接受更多格式”而是保证文档不复制、图片链接可迁移、混合语义不含糊。图片三模式已有模拟器 3 分证据新的文档拖放仍处于 2 分代码、构建、纯函数和安全边界完成系统多文件记录和窗口行为待测。达到 3 分需要最新 HAP 在 MateBook Pro 2in1 模拟器完成全部拖放用例达到 4 分还需真机与竞品同任务测量。结语文件拖放的难点不在监听一个事件而在为不同文件建立正确语义。OhMarkdown 让 Markdown 进入文档会话让图片进入受管理资源事务让不支持和混合批次在副作用前失败。所有路径继续使用现有文件服务、标签上限、外部冲突和大文档保护没有因追求“拖一下就行”而破坏本地文件事实来源。提交b11519c已把分流实现推送仓库。下一步模拟器验收要真实从系统文件管理器拖入单个、多文件、重复 URI、图片和混合批次并在 640 至 1280 vp 自由窗口观察反馈。完成这些设备证据后才能把“编译可用的拖放”提升为“鸿蒙 PC 上可靠的桌面工作流”。每条设备记录还要保存 UDMF 类型摘要、文件数量、成功打开数量、最终活动标签和失败文案。只有同时核对应用状态与磁盘文件才能确认拖放没有留下部分复制、错误源删除或不可见会话。真实文件管理器验收鼠标框选不是文件拖放本轮模拟器自动化先踩到一个很有代表性的坑。用虚拟鼠标从文件名拖向 OhMarkdown 时文件管理器把动作解释为框选最终选中三项并没有生成 UDMF 文件记录。改用触控长按拖放后系统才进入真正的跨窗口拖放协议。测试记录因此同时保留源文件选中状态、目标标签栏、应用状态和最终标签不能只看指针移动轨迹。单文件路径把Untitled.md拖入标签栏应用打开新标签并显示Dropped document opened。双文件路径先框选Untitled.md和ohmarkdown-1m.md长按拖入后状态变为Dropped documents opened已有同 URI 标签被激活1 MiB 文档创建新会话未保存的unsaved-g3标签仍然存在。图片回归切换到 Copy 模式后从系统文件管理器拖入 JPEG。资源服务先写入assets/ohmarkdown-g3-04.jpg再在当前位置插入标准链接Reference 模式下拖入工作区外图片会明确失败不插入链接切换 Copy 后同一来源成功。随后把 HTML 与 JPEG 组成混合批次应用在任何文档会话或资源写入前整体拒绝并显示use only Markdown documents or only supported images in one drop。正文仍只有拒绝前的一个图片链接这正是“失败不污染”的可观察证据。文件拖放小阶段现在达到模拟器 3 分单文档、多文档、1 MiB 文档、图片 Copy、Reference 安全失败和混合批次整体拒绝全部通过。G3-04 已有 Move 删除源与 Reference 不复制的磁盘证据继续有效。剩余缺口是真机文件管理器版本矩阵、十二标签上限批次、重复 URI 压力和竞品同任务计时。