Chuyển đến nội dung chính

dsh-sidebar-superdoc-docx

Đã xác minh

dsh-sidebar-superdoc-docx · v0.1.0 · MIT · Giao diện web

DSH web plugin: open and edit .docx files in the better-sidebar editor through SuperDoc (superdoc.dev) — a browser-native DOCX editor. Assets are self-hosted from this package's node_modules; edited documents export back to disk atomically.

Cài đặt

dsh plugin add dsh-sidebar-superdoc-docx

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Thẻ

Readme

dsh-sidebar-superdoc-docx

English | 中文

在 DSH Web 界面的 better-sidebar 侧边栏里直接打开并编辑 .docx 文件 —— 由 SuperDoc 驱动的浏览器原生 DOCX 编辑器,直接读写真实 OOXML,无需任何服务端文档服务。

依赖:本插件向 dsh-better-sidebar(>= 0.13.0)注册文件查看器,它是必选 peer 依赖 —— 不安装它,查看器不会出现。请先(或一并)安装。

⚠️ 许可:本插件自身代码为 MIT,但运行时集成了 AGPL-3.0 的 superdoc 与专有许可的 @superdoc/docx-engine,安装即表示接受相应条款。详见文末许可证与 THIRD-PARTY-NOTICES.md。

功能简介

  • 浏览器原生 DOCX 编辑 —— 在侧边栏里查看、编辑 .docx,支持批注与修订痕迹(tracked changes),三种打开模式可在设置页切换:编辑 / 修订 / 查看。
  • 保存写回磁盘 —— 「保存」按钮把编辑后的文档导出为 DOCX,经专用路由原子覆盖原文件(临时文件 + rename),不会产生半写状态;未保存修改以 ● 圆点提示。
  • 跟随外部修改 —— 每 3 秒轮询磁盘:编辑器干净时自动原地换入新版本(replaceFile);有未保存修改时只显示提示条并提供手动「重新加载」,绝不静默丢弃你的编辑。
  • 完全自托管、可离线 —— SuperDoc 编辑器构建与 DOCX 引擎(含其 web worker)从本包 node_modules 经同源路由下发:无 CDN 流量、无第三方文档服务、默认关闭遥测。pnpm install 之后全程可离线。
  • 下载兜底 —— 所有界面(包括全部错误态)都保留普通下载链接。
  • 侧栏自适应 —— 工具栏随面板宽度折叠进「…」溢出菜单,页面按面板宽度自动缩放(fit-to-pane),亮 / 暗主题均可读。

适用场景

  • 人机协同编辑同一份 Word 文档:AI 代理在会话里改了 .docx,你在侧栏几秒内看到新版并继续人工润色;保存后 AI 的下一轮编辑又基于最新版本 —— 双向往返不打断。
  • 内网 / 离线 / 合规环境:不允许出网到 jsdelivr 等 CDN,或不允许接入 SaaS 文档服务的部署;所有资产同源自托管。
  • 不想为编辑 Word 部署服务端:相比 OnlyOffice / Collabora 需要单独的 Document Server,本插件零服务依赖,装上即用。
  • 文档评审流程:以「修订」模式打开,批注与建议以修订痕迹记录,适合审阅-回评的协作。
  • 快速预览:替代内置的代码 / 下载查看器,侧栏点击 .docx 即见排版后的文档,随时可另存下载。

如何安装

前提

  • Node.js >= 20;
  • DSH Web 界面及其 web profile;
  • 同一 profile 内已安装 dsh-better-sidebar >= 0.13.0(见顶部依赖说明)。

从 npm 安装(推荐)

包已发布到 npmjs,包名 dsh-sidebar-superdoc-docx:

dsh plugin --profile web add dsh-better-sidebar dsh-sidebar-superdoc-docx

或手工编辑 profile 的 package.json(如 ~/.dsh/profiles/web/package.json),加入两个 npm 依赖与 bundle 条目,然后在 profile 目录执行 pnpm install:

{
  "dependencies": {
    "dsh-better-sidebar": ">=0.13.0",
    "dsh-sidebar-superdoc-docx": "^0.1.0"
  },
  "dsh": { "profile": { "bundles": ["…", "dsh-better-sidebar", "dsh-sidebar-superdoc-docx"] } }
}

最后重启 dsh web(host 半需重新加载),浏览器硬刷新(Ctrl/Cmd+Shift+R)。

从 GitHub 源码安装(开发用)

dsh plugin --profile web add github:chendefine/dsh-sidebar-superdoc-docx

本地开发:克隆、构建、link:

git clone https://github.com/chendefine/dsh-sidebar-superdoc-docx
cd dsh-sidebar-superdoc-docx
pnpm install
pnpm build        # → lib/index.js + lib/client.js + lib/types

然后在 profile 的 package.json 里把依赖指向克隆目录,并在 profile 目录执行 pnpm install:

{
  "dependencies": {
    "dsh-sidebar-superdoc-docx": "link:/绝对路径/dsh-sidebar-superdoc-docx"
  }
}

插件配置(profile 的 cordis.patch.yml)

- id: dsh-sidebar-superdoc-docx
  config:
    fileLimitMb: 100            # 保存路由的大小上限(MB),默认 100
    allowOutsideWorkspace: false # 允许保存解析后会话工作目录之外的文件,默认 false

如何使用

打开文档

侧栏文件树点击任意 .docx —— 将以 DOCX(SuperDoc 编辑) 查看器打开(而不是内置的代码 / 下载查看器)。

切换打开模式

设置 → 侧边卡片 → 文件预览 → DOCX(SuperDoc 编辑) 的齿轮里,把「打开模式」设为 编辑 / 修订 / 查看。选择持久化在 pluginSettings['superdoc:docx'].mode,切换后编辑器以新模式重新挂载,即时生效;「查看」模式同时隐藏保存按钮。

编辑与保存

  • 顶部工具栏为 SuperDoc 原生工具栏(加粗、列表、批注等),随面板宽度自动折叠;
  • 修改后标题行出现 ● 有未保存修改,点击 保存 导出并原子写回原路径;
  • 状态机:保存中… → 已保存 / 保存失败(失败会带原因,可重试);保存进行中再做的编辑会继续保持「未保存」提示,可再次保存。

跟随外部修改(例如 AI 代理编辑了该文件)

  • 编辑器干净时:3 秒轮询发现磁盘变化 → 自动重新拉取并以 replaceFile 原地换到新版本,并重新适配缩放;
  • 编辑器有未保存修改时:仅显示「文件已在磁盘上被修改」提示条,由你决定是否点「重新加载」(重载会丢弃当前未保存编辑)。

与其他查看器共存

查看器 id priority
内置代码查看器 code -100
内置下载查看器 binary-download -50
office 预览插件 docx 0
OnlyOffice 插件 onlyoffice:docx 10
本插件 superdoc:docx 10

同优先级(如与 OnlyOffice)按注册顺序取胜。每个查看器都可在「设置 → 侧边卡片 → 文件预览」单独停用,互不影响。

技术架构

双端架构

浏览器(client 半,极小 CJS bundle,经 window.__ModuleLoader__ 注册)
  └─ ctx.betterSidebar.registerFileViewer('superdoc:docx', exts:['docx'], priority:10, fetchStrategy:'mediaUrl')
      └─ SuperDocView: <script src="/sidebar/superdoc/assets/superdoc.min.js">(暴露全局 `SuperDoc`)
          读:fetch(/sidebar/file?sessionId=&path=)        → Blob → new SuperDoc({ document: blob, contained: true })
          存:superdoc.export({triggerDownload:false}) → Blob → PUT /sidebar/superdoc/save?sessionId=&path=

Node(host 半,4 条带围栏的路由)
  ├─ GET  /sidebar/superdoc/info                    版本 / 健康检查 / 缓存种子
  ├─ GET  /sidebar/superdoc/assets/<file>           superdoc/dist-cdn(封闭白名单)
  ├─ GET  /sidebar/superdoc/engine/dist-cdn/<path>  @superdoc/docx-engine/dist-cdn 镜像(引擎 + worker)
  └─ PUT  /sidebar/superdoc/save                    原始 DOCX 字节 → 会话 cwd 内原子写回
  • client 半只做三件事:注册查看器、经 better-sidebar 的 media 路由取文件字节、把 SuperDoc 实例挂进侧栏面板;
  • host 半不跑任何文档逻辑,只负责同源资产下发与带围栏的保存;
  • 挂载版本:[email protected] + @superdoc/[email protected](以 package.json 依赖为准,info 路由上报实际版本并兼作缓存种子)。

为什么需要引擎镜像

client 在脚本加载前设置 globalThis.SUPERDOC_ENGINE_CDN_BASE_URL = '/sidebar/superdoc/engine',把 SuperDoc 的引擎解析指到本插件路由;引擎随后动态 import …/dist-cdn/docx-engine.es.js,其 web worker 也相对这个同源 URL 解析 —— 浏览器不允许跨源创建 worker,否则 jsdelivr 会成为运行时依赖。这正是 host 半镜像整个 dist-cdn 目录的全部理由。

安全边界

  • 信任围栏(src/trust-fence.ts,与 better-sidebar 行为一致):Host 头必须是 loopback 或 webRuntime.trustedHosts 中的可信授权,sec-fetch-site: cross-site 与 Origin 不匹配一律拒绝 —— 防 DNS rebinding / 跨站请求,不是身份认证。
  • 工作区围栏(src/paths.ts + 保存路由):仅接受绝对路径;isWithin 做段级包含比较(/a/bc 不算在 /a/b 内);对父目录做 realpath 封闭符号链接逃逸;仅允许 .docx;请求体超过 fileLimitMb 返回 413;写入走 tmp + rename 原子替换。
  • 资产白名单(src/assets.ts):superdoc 构建只暴露 3 个文件的封闭白名单;引擎子路径做形状校验、拒绝 ./.. 段、realpath 必须落在 dist-cdn 内且为普通文件。
  • 无外泄:遥测默认关闭(telemetry: { enabled: false }),插件自身不在磁盘保存任何状态。

开发细节和规范

目录结构

src/
  index.ts            宿主半:构建并注册 4 条路由(buildRoutes 纯函数,便于测试)
  assets.ts           node_modules 资产定位 / 白名单 / realpath 包含检查 / 内容类型
  config.ts           配置解析(fileLimitMb、allowOutsideWorkspace;纯 TS,零依赖)
  paths.ts            绝对路径要求 + 段级包含 + symlink 安全的父目录 realpath
  trust-fence.ts      浏览器信任围栏(复制而非 import 上游,插件不得依赖其内部)
  wire.ts             {ok,...} / {ok:false,error:{code,message}} JSON 形状 + 限长原始字节读取
  client/
    index.ts          客户端半:注册 superdoc:docx 查看器 + 挂载词典
    SuperDocView.tsx  编辑器组件(挂载 / 保存状态机 / 磁盘轮询 / fit-to-pane 缩放)
    loader.ts         运行时加载器(script/stylesheet 单例、引擎基址、contained 布局 CSS)
    settings.ts       读取打开模式(带校验,回退 editing)
    urls.ts           /sidebar/file 与保存路由的 URL 构造(对齐 better-sidebar 请求契约)
    i18n.ts / locales.ts / icons.tsx   zh/en 词典、注册与图标
tests/                vitest:routes / save-flow / viewers / trust-fence / locales

构建产物

  • host:lib/index.js,ESM(es2023),运行时零第三方依赖;
  • client:lib/client.js —— window.__ModuleLoader__.load({ id, factory }) 注册的 CJS bundle,与 dsh-sidebar-onlyoffice、dsh-web-search-aggregation 相同的官方外置客户端投递形态;
  • SuperDoc 编辑器本体不打包进 bundle:由 host 路由在运行时以经典 <script> 注入(与 onlyoffice 加载 api.js 的方式同构)。

客户端纯度门禁

tsdown.config.ts 内置 rolldown 插件,构建期直接报错:client bundle 不得 import 任何 Node 内建模块、不得值导入 @deepseek-ai/*;React / react-dom / cordis 作为 external 由宿主模块表提供。浏览器半必须自包含。

代码约定

  • 不 import monorepo 内部类型:宿主与客户端都定义结构化的 context faces(RouteContext、ClientContextFace),外部插件不得触达 monorepo 的 Context augmentation 图;
  • 浏览器 JSON 一律 {ok:...} / {ok:false,error:{code,message}}(对齐 better-sidebar 的 wire 格式),错误码:forbidden / method-error / bad-request / not-found / fs-error / internal;
  • viewer id 命名空间化(superdoc:docx),避免与内置及 onlyoffice:docx 冲突;priority 10 > 内置;fetchStrategy: 'mediaUrl';
  • 词典 zh / en 的 key 集合必须完全一致(locales 测试强制),注册在插件唯一的 dshSidebarSuperdoc 命名空间;
  • 所有页面级注入幂等(stylesheet、布局 CSS、editor script 均为单例,重挂载安全);
  • 保存路由是唯一的 fs 写入面;better-sidebar 自带 fs.write 仅支持 UTF-8 文本,二进制导出必须走本路由。

测试

pnpm test(vitest run)覆盖:

文件 覆盖
routes.test.ts 4 条路由:白名单命中 / traversal 与符号链接拒绝 / 工作区围栏开与关 / 413 / 405 / 403
save-flow.test.ts 保存状态机:成功后清除未保存提示、保存中编辑保持未保存、头部按钮位置稳定
viewers.test.ts 查看器契约:id / exts / priority / fetchStrategy / 设置行,与既有 viewer 无 id 冲突
trust-fence.test.ts loopback 与可信授权通过;未知 Host / 跨站标记 / Origin 不匹配拒绝
locales.test.ts zh / en key 一致、值非空、命名空间唯一

常用命令

pnpm typecheck   # tsc --noEmit
pnpm test        # vitest run
pnpm build       # host ESM + client ModuleLoader bundle(纯度门禁强制)

已知局限

  • 只支持 .docx(SuperDoc 不打开旧版 .doc);
  • 有未保存修改时直接关 tab 无法拦截 —— 请留意 ● 未保存圆点;
  • better-sidebar 的 workspaceFence 关闭时可以打开工作区外的文件,但保存它们仍需本插件 allowOutsideWorkspace: true;
  • 字体:SuperDoc 核心不带字体,文档以系统字体渲染(如需一致排版可后续接入 @superdoc-dev/fonts,本插件未含)。

许可证

本插件代码为 MIT;整合(未修改、随 pnpm install 安装并由路由原样下发)两个 SuperDoc 组件:

包 许可证 说明
superdoc AGPL-3.0 未修改的 npm 产物;以网络服务形式提供时触发 AGPL 源码提供义务
@superdoc/docx-engine 专有许可(DOCX Engine Proprietary License) 无商业协议时,仅可作为 SuperDoc 的依赖用于 AGPL 允许的用途(评估 / 开发 / 测试);商业使用需向 SuperDoc 购买授权

详见 THIRD-PARTY-NOTICES.md。

致谢

  • SuperDoc by Harbour Enterprises —— 编辑器本体;
  • dsh-sidebar-onlyoffice —— 本包遵循的插件形态(运行时脚本注入、信任围栏、宿主路由)。