dsh-sidebar-superdoc-docx
v0.1.0
Published
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.
Maintainers
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 界面及其
webprofile; - 同一 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 购买授权 |
致谢
- SuperDoc by Harbour Enterprises —— 编辑器本体;
- dsh-sidebar-onlyoffice —— 本包遵循的插件形态(运行时脚本注入、信任围栏、宿主路由)。
