夜雨聆风学习资料网

ARTICLE · 1296385

Flyfish Office:统一文档模型、全编程 API,Agent 引用直达原文

Flyfish Office:统一文档模型、全编程 API,Agent 引用直达原文

让 Agent 从一份合同里找出付款条件,已经不算稀奇。

难的是下一步:它说“验收后付款”,你点一下引用,能不能直接看到原文里那一条,以及紧接着的例外条件?

如果打开的是一份上百页的 Word,光有文件名和一段摘录还不够。相同的句子可能出现几次;条款可能藏在表格里;换了一版文件,旧引用也可能已经失效。

最后,核对的人还是得自己翻。

最近,我们完成了 Flyfish Office 的一轮底层整合:DOC 和 DOCX 共用 Word 模型与 Canvas 排版,PPT 和 PPTX 共用演示文稿模型与渲染引擎,表格保留完整 Workbook,WPS 通过适配器进入对应领域。

此前做文档溯源定位时,我们也反复处理过一个问题:后端找到的内容,怎样准确落到浏览器正在显示的原文上。

这两项工作的价值,可以用一个动作说明白:程序读到内容,人点开依据,两边指向同一份文件里的同一处。

统一文档模型,统一的是哪一层?

Word、PPT 和 Excel 都能显示文字、图片、表格,但它们安排内容的方式差别很大。

Word 多插入一段文字,后面的分页可能全变;PPT 有固定画布,还要处理母版、版式和主题继承;Excel 则围绕工作表、单元格、合并区域和公式组织内容。

把它们全部压成一棵只有 text / image / rect 的树,看起来很统一,代价是丢掉了很多原本有用的关系。

我们保留了三个完整领域模型,让同一类文档共用排版和绘制实现:

领域
输入示例
保留下来的核心结构
Word
DOC、DOCX、WPS Writer
节、段落、列表、表格、浮动对象、页眉页脚及兼容属性
演示文稿
PPT、PPTX、WPS Presentation
幻灯片、母版、版式、主题、形状树、图表和媒体
表格
XLS、XLSX、XLSB、ODS、WPS Spreadsheet
Workbook、工作表、单元格、公式、样式和绘图

格式适配器负责把原文件读进来,领域模型保存语义,排版引擎决定内容的位置,绘制层把它呈现在页面上。源格式里尚未完整映射的高级属性,保留来源和诊断,便于继续补齐。

架构示意:统一模型的公共入口,保留三类文档各自的排版规则。

这样做有一个很实际的好处:修复 Word 的公共排版行为时,DOC 和 DOCX 可以复用同一处修复,再分别回归验证。后续接入格式,也有明确的适配边界。

统一接口负责减少接入差异,完整模型负责保住文档本身。

全编程 API,把预览放进自己的业务流程

有些系统只需要点开附件。另一些系统还要读取模型、控制打开时机、接管导航、取消过期任务,并在切换文件时彻底释放资源。

这些操作应该由程序直接调用,不必模拟用户去点工具栏。

统一引擎的基础流程是 open → mount → ready → dispose。下面是当前整合版入口的核心用法;appOptions 沿用应用初始化配置,file 是用户取得的 File,container 是挂载容器。

TYPESCRIPT · 统一文档入口
import { createOffice } from'@flyfish-dev/office';const office = createOffice(appOptions);const session = await office.open(file);const view = await session.mount(container);await view.ready;if (session.document.kind === 'spreadsheet') {const workbook = session.document.model;console.log(workbook.sheets);}

代码里的 model 有实际用途。表格暴露的是完整 Hucre Workbook,视图与模型引用同一份单元格存储;应用可以读取结构化内容,再组织自己的核对、索引或展示逻辑。

资源管理也有明确分工:

  • view.dispose() 释放一个视图,文档模型仍可再次挂载。
  • session.dispose() 先结束视图,再释放模型、媒体、归档与 Worker 等会话资源。
  • office.dispose() 关闭其余会话,并处理仍在进行的导入。

关闭附件、组件卸载和异常分支都要安排清理;复用同一个容器前,应等待旧视图清理完成。否则连续打开几个大文件后,旧请求、旧画布和旧事件监听就可能一起留下来。

这里的“全编程”指的是预览、模型读取、生命周期,以及定位适配层的标记控制,都能由 API 驱动。文档编辑、保存回原格式和 Agent 工具编排,仍按各自的能力范围接入。

定位原文,先解决“这到底是哪一处”

最直观的做法,是存下“第 12 页,左上角某个矩形”。

在固定版面的批注中,页坐标很有用。但拿它长期引用会重新排版的 Word 正文,就得面对字体、分页和文件版本的变化。

我们的溯源定位接口采用另一套分工:后端保存结构锚点,前端根据真实排版计算页面几何。

一个引用需要回答三件事:是哪一版文件、哪个结构对象、对象里的哪一段。

对应到接口,是原文件的 documentHash、解析器产生的 sourceKey,以及必要的文本起止偏移和内容摘要。文件哈希用于校验版本,结构键负责定位,摘要帮助检查目标是否匹配。

Word 的目标可以是文本、图片、表格或对象;表格可以定位工作表和单元格区域;演示文稿可以定位幻灯片,PPTX 还支持按原生对象 ID 定位对象。

这里不能让模型凭印象编一个 sourceKey。应先从解析结果里取得真实结构键,再交给标记接口。

定位流程示意:引用保存结构身份,显示位置由渲染器计算。

参考解析服务提供分页内容清单和标记生成接口。比如,从内容清单选中两处依据后,可以向 POST /api/documents/{id}/markers 提交下面这样的业务请求。示例结构键取自演示文件,实际使用时替换为当前文件返回的值。

JSON · 创建结构标记
{"id":"evidence-1","label":"两处依据","mode":"blocks","targets":[{"sourceKey":"docx:body:48:p:44:image:0"},{"sourceKey":"docx:body:63:tbl:3:r:6:c:0:p:0","startOffset":0,"endOffset":5}]}

解析服务补齐结构摘要,返回 office-trace-marker-v2 报文;业务系统保存引用,前端定位适配层消费报文。

有两个细节尤其容易出错。

第一,偏移口径必须一致。 Word 文本先做 NFC 规范化和空白折叠,再使用 UTF-16 的左闭右开区间 [start, end)。不能把 UTF-8 字节数、JavaScript 字符串偏移和肉眼看到的字数混在一起。

第二,分散证据和连续范围是两回事。 第 1、3 页各有一段依据,使用多块模式时,第 2 页不应被涂上颜色。若明确选择从起点到终点的连续范围,中间内容才属于引用范围。跨页时逐页绘制,页面之间的空白不加框。

文件换版后重新索引、生成标记;找不到或无法唯一确认的目标,返回失败。把用户带到“看起来差不多”的位置,会比明确提示引用失效更难发现问题。

Agent 场景:让引用跟着证据走

以“检查交付条件”为例,一条可落地的流程可以这样组织。

  1. 业务服务校验用户权限,取得原文件及版本标识。
  2. 解析并建立内容索引,给文本块、表格和对象保留结构键。
  3. 检索相关内容,把内容与引用 ID 一起交给 Agent。
  4. Agent 输出结论,并选择这次检索返回的证据 ID。
  5. 服务端校验证据所属文件、版本和权限,生成标记。
  6. 用户点击引用,前端打开对应文件并定位;人继续核对完整上下文。

这里有一条很重要的约束:让 Agent 选择证据,让程序校验证据,不让 Agent 猜页面坐标。

在应用层,可以把“查询内容”“取得引用”“打开依据”封装成工具。工具名称由业务定义;文档内容只作为待分析数据,文件里写着“忽略之前的要求”也不能获得执行权限。导出、修改、发送等动作,继续由业务权限和人工确认控制。

统一引擎提供模型与渲染能力;现有定位适配层提供结构锚点、标记和显示能力。把两者用于同一条 Agent 流程时,接入方需要对齐文件身份、索引版本与当前打开会话。定位方法属于定位适配层,不是给 createOffice() 返回值直接追加一个调用就完成集成。

如果使用外部模型,哪些文本会被发送出去,也应由应用明确控制。浏览器本地预览与模型推理的数据流是两件事。

这套做法适合知识库问答、长文档审阅、投标材料核对、报告取数等场景。它让结论更容易复核,但结论是否成立,仍要看证据和推理本身。

真正影响体验的,是等待和切换

长文档往往先出现可读首屏,再继续排版。此时用户点击一个靠后的引用,目标可能还没有准确坐标。

为了让接口看起来快,先猜一个位置跳过去,后续排版再把它挪走,用户就会看到画面反复跳动。

定位接口因此区分三种结果:

状态
业务应该怎样理解
accepted
至少一个目标跨度已经可见
pending
标记已登记,目标还在准备或尚未可见
rejected
校验或定位失败,读取原因并提示

收到 onReady 后,说明当前会话可以接受定位请求,不代表全文已经排版完成。下面这段 JavaScript 在现有定位适配层中调用,marker 是业务包装后的标记,signal 使用当前会话提供的取消信号。

JAVASCRIPT · 异步定位
asyncfunctionrevealEvidence(handle, marker, signal) {if (signal.aborted) returnfalse;const result = await handle.locateMarker(marker, {reveal: true,replace: true,    signal,  });if (result.status === 'rejected') {thrownewError(result.reason || '定位失败');  }if (signal.aborted) returnfalse;return result.status === 'pending'    ? handle.revealMarker(marker.id)    : true;}

宿主先检查句柄是否具有定位能力,再调用这段逻辑。取消操作还应接上 cancelMarkerReveal();异步失败通过 subscribeMarkers() 的状态订阅处理。

目标未就绪时,优先处理它依赖的排版任务,其他内容继续后台处理。遇到连续正文、跨页表格等必须先完成的依赖,就继续等待真实结果。

用户取消、切换文件或选了另一个引用后,旧请求不能晚一点又把页面拉回去。布局或字体版本改变时,旧几何也要失效重算。

这些处理不显眼,却决定了用户是否敢连续点开十几个引用。

不开 Agent,日常工作也能用

审批里的“请看这一条”。 审批意见绑定条款或表格范围,下一位处理人点击意见就能看到上下文。遇到文件换版,系统要求重新确认引用,避免拿旧批注套新正文。

表格里的“这个数从哪来”。 核对结果保留工作表和单元格位置。展示异常项时,能够回到相应区域继续检查公式和相邻数据。检查规则来自业务,不必把每次核对都交给模型。

培训材料里的“看这一页这个图”。 问题单或说明文字绑定幻灯片;需要更细的位置时,PPTX 可使用对象级定位。跳转之外,页面本身仍保留图表和文字之间的关系。

附件中心里的“一套打开流程”。 不同格式使用共同会话协议,应用统一管理加载、关闭和错误提示。Word 使用渐进布局与可见页预算,演示文稿按需加载并只挂载当前页,表格维持虚拟行列窗口。

纯前端渲染可以减少预览转码服务的负担;检索索引、文件存储和 Agent 推理仍各自消耗资源。若业务要求固定输出版面,或已有成熟的服务端转换流程,也可以继续使用相应方案,不必为了统一而改掉适合自己的部分。

专业性,最后要落到可验证的行为

这次统一模型整合已经完成源码收尾。对应提交的 CI 中,跨格式集成回归 87/87、Excel 原有回归 198/198 通过;14 个包还在独立消费项目里进行了实际 tarball 安装与严格浏览器 TypeScript 检查。

这些检查回答的是接口、依赖和生命周期能否一起工作。具体文档的排版效果,继续通过样例和原生参考逐项验证,不能用“测试全绿”替代每一类文档的保真验收。

统一引擎的新入口按整合版组件交付,整组 npm 发布仍有独立流程;定位接口沿用相应适配层。现有演示站用于体验预览效果,Agent 工具与业务工作流由接入方组织。

如果你也在做类似系统,可以把下面五件事放进验收:

  • 同一句话出现两次,引用能否区分正确的那一次?
  • 同一段内容跨页,高亮是否分开绘制,且不多标下一页?
  • 文件换版,旧引用是否被识别,而非继续套用?
  • 用户切换文件,旧任务是否停止跳转并释放资源?
  • 回答引用了表格或图片,用户能否看到原对象及上下文?

我们想把这些基础能力做扎实:让开发者能直接调用,让日常使用的人少翻几页,让 Agent 给出的每一条依据都有地方可查。

点击底部「阅读原文」,可以用自己的样例体验 Office 预览效果。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料