把商业表格引擎接进 Agent 工作台:四次取舍

dsh-spreadjs-editor 把原生 SpreadJS Designer 接进了 DSH 的右侧栏,让工作区里的工作簿能直接打开、编辑、保存回原文件。

在 DSH 的插件里接一套重型引擎,功能本身往往不是最难的部分。真正要回答的是四件事:引擎放不放进发布包、入口从哪来、写入由谁负责、界面该画多少。这四件事上每个选择都有代价,而且通常要等到装到用户机器上才显现。

下面把这四次取舍摊开,每次都说清判断和代价,也看看换一种选法会怎样。

一、引擎放进发布包,还是让用户安装时自己构建

先看一个数字:这个插件现在的发布包约 53MB,绝大部分是内置的 SpreadJS 与配套 Designer。

这里有一条硬限制,它决定了这一节的取舍:DSH 的插件通过 pnpm 安装,而 SpreadJS 走的是 npm 这条分发路径,插件安装的环节没法把引擎一起装进来。引擎因此只能在发布侧打进包里。代价随之固定下来:内置的 SpreadJS 什么时候升、升到哪一版,从依赖解析变成了仓库里的一件事,要由我们自己跟。

如果这条限制不存在,最优解不是自己打包:引擎跟着依赖走,版本升级就不用我们操心。所以这一节真正的取舍不在「打不打包」,而在打包之后,构建放在谁的机器上。

我们最早把构建放在使用者那边。0.1.0 的包只有 56KB,引擎留作依赖、构建产物不进 tarball,用户安装时用 postinstall 现场构建。包在 npm 页面上很好看,代价全在使用者那边:每个装插件的人都要跑一遍构建,还要多带一套构建工具链。构建失败的环境各有各的失败方式,而失败发生在别人的机器上,我们只能等对方把日志发过来。

0.1.2 换成了现在这一种:引擎和相关产物全部进包,dependencies 清空,装完即用,包体涨到约 48MB。

两个方向都能用,区别在代价落在谁身上。我们最后选的判断只有一条:包体是发布方的问题,构建失败是使用者的问题。前者我们能自己解决,后者只能等别人配合。

这条判断反过来也成立。内网仓库、离线交付这类环境对体积敏感,48MB 会直接变成使用方的负担,那种场景下安装时构建更合适。要避免的是第三种情况:分发环境不可控,又拿不到对方的构建日志。这时候一次构建失败就足以让安装卡住,而失败原因我们根本看不到。

二、DSH 没提供的位置,要不要自己造

DSH 的早期版本没有右侧栏。它能给插件的位置是零散的:侧边栏底部的一个动作位(sidebar.footer.action),加上覆盖整个界面的浮层(shell.overlay)。第一版就挂在这两个席位上:底部一个入口,点开在浮层里显示 SpreadJS 查看器。

这条路能跑通,但这两个位置不是为文档设计的。工作簿只能「弹出来看」,不能像文件那样被打开、切换、关闭;插件还得自己管查看器状态、自己去读文件字节,当时仓库里对应的就是 store.ts 和 fs-bridge.ts 两个模块。

后来 DSH 提供了更贴近文档的接口,插件重构成 view-first 的形状。中间有一段时间,插件同时维护两套适配器:一套对接新接口,一套对接第三方侧边栏。两套注册路径、两套测试,这是那段时期真实的成本。再往后砍掉一套,只留第三方侧边栏。

直到 0.2.0,DSH 自己长出了右侧栏和文档注册表,插件顺着迁过去,写法收敛成两个注册:

ts

复制代码
// 登记这个实现认领哪些后缀ctx.documentPreviews.register({  id: SPREADSHEET_DOCUMENT_ID,  extensions: ['xlsx', 'xlsm', 'csv', 'sjs', 'ssjson'],  title: () => 'SpreadJS',  loading: 'bytes-complete',  wrap: false,})// 同一个 id 下提供正文ctx.slots.inject('sidebar.right.tab.document', () => ctx.slots.register({  name: 'sidebar.right.tab.document',  key: SPREADSHEET_DOCUMENT_ID,}, SpreadsheetDocument))

第三方依赖随之整个删掉。更省事的是文件读取也不用自己做了:字节由 DSH 的文档实现读好后传进来,插件里原来那个 /sidebar/file 路由连同它的桥接模块一起删掉了。客户端里省下来的东西同样能说明问题:DSH 的 text 标签类型已经认领了所有会话文件地址,插件不需要注册标签类型、面板、store 或任何布局。

所以这类位置怎么选,关键不在当下哪条省事,而在这个能力将来会不会由 DSH 提供。DSH 正在快速补齐的能力,自己造一遍大概率只在维护成本上留下记录。

三、DSH 能读字节,但写不了工作簿

先看 DSH 给了什么。文件相关的接口都在 ctx.fs 上:

要做什么 调什么
寻址 ctx.fs.resolve(path, { cwd })
拿到可打开的绝对路径 ctx.fs.processPath(target)
判断是否在工作区内 ctx.fs.contains(rootTarget, target)
取元信息 ctx.fs.stat(target)
读字节 ctx.fs.readBytes(target, signal, maxBytes)

解析会话的工作区根也全是 DSH 的服务:ctx.sessions.get(sessionId).header.cwd 优先,会话不在线上时问 ctx.sessionPersistence.stat(id),最后回落到 ctx.sandboxPolicy.workspaceRoot。

没有给的是写字节。 ctx.fs 改文件只有 writeText 和 editText,连原子写入 writeFileAtomic 也只接受字符串。工作簿是二进制,DSH 写不了,Agent 也写不了。

于是插件要自己补的动作只剩一个:在同一目录下用随机后缀建临时文件(writeFile(..., { flag: 'wx' })),再 rename 覆盖目标。形状和 DSH 自己的原子写入一致,只是收的是字节。

保存一个工作簿要按顺序过七道判断:

  1. 请求的 Host 头指向回环地址,或者是部署里显式列出的地址
  2. 参数齐全:sessionId、path、body 哈希
  3. 能解析出这个会话的工作区根
  4. ctx.fs 在场;服务缺席就拒绝,没有兜底路径
  5. fs.contains(rootTarget, target),目标不能越出工作区
  6. 上传字节的 sha256 与请求声明的 body 哈希一致
  7. 文件已存在时,fs.stat 加 fs.readBytes 读出当前内容,它的 sha256 必须等于打开时的 base 哈希

源码注释把这段的分工说得很直接:所有判断都已委托给 DSH 的服务,本地只剩最后一次字节替换。

这也是写回这一段仍然算「临时」的原因。等 DSH 提供二进制写入,它就能整体交还,插件回到只做格式适配和界面集成的位置。

顺序上这里有个教训:先读 DSH 的写入接口,再选引擎。等集成完才发现形状不对就晚了。只要 DSH 只提供文本写入,任何二进制格式都要自己实现,工作簿、图片、压缩包、PDF 都一样。写入一旦由插件负责,来源校验、路径包含、冲突检测、原子替换这些安全判断也一并转移过来,DSH 不会再兜底。

四、界面元素由谁来画

面板上的东西,哪些由插件自己画,哪些交给 DSH?两条路都能把功能做出来。插件完全可以自己画一条状态栏显示文件名和版本,DSH 的标签页同样能显示文件名。区别还是在代价。

插件画,样式、位置、行为都由自己控制。代价是这些都要自己维护,DSH 改一次外观就得跟一次;而且信息源不在插件手里,显示的未必和 DSH 的实际状态一致。

DSH 画,自动跟着 DSH 的外观和交互走,插件不用管。代价是位置和样式由 DSH 的接口决定,接口没提供的信息,我们想显示也没有地方放。

判断标准可以归结到一点:这份信息属于谁。文件名、文件身份、文件状态是 DSH 的事实,插件再画一遍等于复制一份会漂移的数据。0.2.0 因此删掉了面板顶部的文件名标题栏,文件名交回 DSH 的标签页。反过来,引擎的工具栏、菜单、主题在 DSH 那边根本不存在,只能由插件提供;插件自己的版本信息没有常驻的必要,搬进了「关于」弹窗,保存与冲突提示改成右下角一个会消失的浮层,处理完就不再占位置。

主题的坑跟 DSH 无关,它是引擎内部的问题。Designer 有两套绘制路径:工具栏和面板是 DOM 控件,工作表的单元格区域画在 canvas 上。setTheme() 只重新着色前者。我们早期只接了控件那一半,结果就是深色工具栏配一张雪白的表格,看起来像两个产品拼在一起。

引擎自带的外观系统要整块接。 判断接全了没有,不能看哪一层更显眼,要看引擎对外暴露出几条绘制路径。

小结

这四件事里,包体和界面是自己能算清的账,入口和写入取决于 DSH 给什么。真正容易吃亏的是后两件,因为它们需要我们在动手之前就去读接口、读 DSH 的实现。

这几轮改动始终停在集成层。SpreadJS 的电子表格功能没有被插件动过,插件补的一直是 DSH 里的安装、打开、保存、主题和配置体验。

如果只能留一条建议:先查 DSH 的接口形状,再选引擎。 功能可以慢慢补,接口形状不匹配,补的代价会成倍增加。

参考链接

相关推荐
howdoyoudo2026065 小时前
AI审计手记 #28:从“暂停训练”到“切断联网”——AI安全控制手段的极限升级
大数据·人工智能·安全·ai·语言模型
·云扬·10 小时前
Prompt 工程与安全:从 System 设计到注入防御
java·人工智能·安全·ai·prompt
九卷10 小时前
企业级Agent系统整体技术架构介绍
ai·架构·大模型·llm·agent·智能体
俊哥V14 小时前
每日 AI 研究简报 · 2026-10-12
人工智能·ai
空堂与归16 小时前
REA登顶GitHub:AI如何逆向一切软件
ai·大模型·agent
效率工作实验室16 小时前
2026年企业级 AI 智能体平台选型指南:关键评估维度
ai·agent·企业级·智能体平台·智能体平台选型
Am-Chestnuts18 小时前
DeepSeek 内容转 Word 总翻车?用 DS随心转
ai