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

dsh-sidebar-onlyoffice

Đã xác minh

dsh-sidebar-onlyoffice · v0.2.0 · MIT · Giao diện web

DSH web plugin: open .docx/.xlsx/.pptx in the better-sidebar editor through a self-hosted ONLYOFFICE Document Server (JWT-signed config, in-network document/callback routes, save-back to disk; the DS browser UI is always served same-origin through the bui

Cài đặt

dsh plugin add dsh-sidebar-onlyoffice

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-onlyoffice

English · npm · GitHub

DeepSeek Harness(DSH)web 插件:在 dsh-better-sidebar 的文件侧边栏中,用自部署的 ONLYOFFICE Document Server 打开并编辑 .docx / .xlsx / .pptx —— 配置经 JWT 签名、保存原子写回磁盘,并且 AI 修改已打开的文件时编辑器实时刷新。

npm license node CI stars

功能

  • 真正的办公编辑 —— better-sidebar viewer 内嵌完整的 ONLYOFFICE 编辑器(文字/表格/演示),不是静态预览;保存自动写回磁盘文件。
  • 同源反代(常开) —— DS 的整套浏览器界面(api.js、编辑器 iframe、静态资源、协同 socket)由 dsh web 自身在 /sidebar/onlyoffice/ds 反代,浏览器无需访问到 DS 的服务地址,DS 可完全留在内网。
  • 默认签名与围栏 —— 配置密钥后编辑器配置整体 HS256 JWT 签名;文件下载一律凭短时效 HMAC 令牌;浏览器侧路由在 dsh web 信任围栏之内;会话工作目录之外的文件一律拒绝。
  • AI 修改实时可见 —— agent 修改编辑器中打开的文件时,viewer 约 1 秒内经 refreshFile 换到新版本,页面不重载、api.js 不重载。脏编辑器绝不自动刷新(改为横幅提示手动重载)。
  • 原子保存 + 自保存抑制 —— 回调下载保存字节后以临时文件 + rename 原子替换,同 key 串行化;插件自身写入被监听枢纽吸收,用户保存不会回环触发刷新。
  • 内网直连拉取保存文档 —— DS 上报的保存 URL 会改写到 documentServerUrl(容器间直连),避免绕公网反代回环。
  • 与 office 预览插件共存 —— viewer id onlyoffice:docx|xlsx|pptx(priority 10)与 @huanlin/dsh-plugin-better-sidebar-plugin-office 的 docx/xlsx/pptx(priority 0)不冲突;侧边卡片设置页可分别禁用。

工作原理

半 位置 职责
Node(服务端) src/ 在 dsh web 的 webServer 上注册四个路由,实现 ONLYOFFICE 官方集成文档中的 "document storage service" 角色。
浏览器(客户端) src/client/ 注册三个 better-sidebar 文件 viewer;按需加载 DS api.js、挂载 DocsAPI.DocEditor、订阅 SSE watch 流。
better-sidebar viewer (onlyoffice:docx|xlsx|pptx)
   ├─ GET /sidebar/onlyoffice/config    (浏览器端,信任围栏内)
   │     → 编辑器配置 + JWT + api.js 地址 + HMAC 文件令牌
   ├─ GET /sidebar/onlyoffice/file      (Document Server;HMAC 令牌即鉴权)
   ├─ POST /sidebar/onlyoffice/callback (Document Server;status 2/6 → 原子写回)
   ├─ GET /sidebar/onlyoffice/watch     (浏览器端 SSE;inotify 磁盘变更)
   │     → change → 重取 config → docEditor.refreshFile(config)  [不重载]
   └─ /sidebar/onlyoffice/ds/**         (浏览器 ↔ Document Server,同源)
         → api.js + 编辑器 iframe + 静态资源 + socket.io(WS 按文档即时路由,失败降级轮询)
  • Node 半 —— config 路由对会话 cwd 内的绝对路径生成编辑器配置(documentType 映射、内容寻址 key = sha256(host+path+size+mtime)),整体 HS256 JWT 签名,登记 key→文件 映射并签发短时效 HMAC 文件令牌。file 路由向 DS 提供文件字节。callback 路由(JWT 校验)在 status 2/6 时下载保存字节并临时文件+rename 原子写回,status 4 清除映射,应答 {"error":0}。watch 路由以 SSE 推送磁盘变更,由 inotify 监听枢纽供给(fs.watch 同时盯文件与其所在目录,防抖后按同一内容寻址 key 做签名过滤)。
  • 浏览器半 —— 按 URL 仅一次加载 DS api.js,挂载编辑器,卸载时 destroyEditor();失败时展示错误面板 +「下载文件」回退链接(复用 better-sidebar 的 /sidebar/file 路由)。挂载期间订阅 watch 流,发现外部(通常是 AI)修改后重取 config 并调用 docEditor.refreshFile(config)。

AI 修改的实时可见

agent 修改了编辑器中打开的文件时,viewer 约 1 秒内感知:inotify → SSE change 事件 → 重取 config(内容寻址 key 随文件而变)→ refreshFile。编辑器 iframe 本体被复用——页面不重载、api.js 不重载,DS 以全新会话打开新版本(旧 key 以 status 4 关闭)。护栏:

  • 脏编辑器绝不自动刷新 —— refreshFile 无条件丢弃未保存修改(集成方调用路径上没有 isDocumentModified 守卫,已在 DS 源码中核实)。此时改为展示横幅(「文件已被外部修改——重新加载将丢弃未保存的修改」)+ 手动「重新加载」按钮,点击即视为用户确认。
  • DS 自发的 onRequestRefreshFile(断线重连 / 同 key 保存时触发,仅在非脏状态)走同一条刷新路径。
  • 插件自身的回调保存在服务端被抑制(noteSelfSave 记录刚写入的签名),用户保存不会回环刷新、重置光标。
  • 文件被删除推 removed 提示;重新出现推 change。

环境要求

  • DSH web profile(dsh web),Node.js ≥ 20,并已安装 dsh-better-sidebar 插件。
  • 自部署的 ONLYOFFICE Document Server(已在 9.4 社区版实测),网络可达 dsh web 服务器;浏览器无需能访问它(反代挂载点是唯一的浏览器侧 DS 入口)。
  • JWT 启用时(DS 默认),插件的 jwtSecret 必须与 DS 的 JWT_SECRET 一致。
  • document.url / callbackUrl 为私网地址时,DS 需要 ALLOW_PRIVATE_IP_ADDRESS=true(DS 默认拒绝)。

安装

从 npm 源安装(预构建 —— 无需构建权限):

dsh plugin --profile web add dsh-sidebar-onlyoffice

从 GitHub 仓库安装(源码 —— pnpm 会执行 prepare 构建;若 pnpm 拦截构建脚本,请在 profiles/web/pnpm-workspace.yaml 里放行):

dsh plugin --profile web add github:chendefine/dsh-sidebar-onlyoffice

也可以通过 DSH 插件市场(设置 → DSH插件市场)—— 仓库带 dsh-plugin topic,会被自动索引。

bundle 插件加入 profile 层栈后,把配置写进 profile 的 cordis.patch.yml 手编层(见下),重启 dsh web,浏览器硬刷新(Ctrl+Shift+R)。卸载:dsh plugin --profile web remove dsh-sidebar-onlyoffice 后再重启。

配置

所有键均可选;由 profile 的 cordis.patch.yml 层承载:

- id: dsh-sidebar-onlyoffice
  config:
    jwtSecret: "<DS 的 JWT_SECRET>"                # 空 = 不签名(仅 JWT_ENABLED=false 的 DS)
    # documentServerUrl: http://onlyoffice-documentserver  # dsh web 直连的 DS 入口(其自带 nginx)
    # internalBaseUrl: http://172.31.255.4:3080    # DS 回连 dsh web 的基址;空 = 自动探测
    # publicBaseUrl: https://work.example.com      # 网关改写 Host 时的浏览器可见源;空 = 按请求 Origin
    # defaultMode: edit                             # edit | view
    # username: User                                 # 编辑器右上角显示的协作用户名
    # fileLimitMb: 100
    # tokenTtlSec: 600
字段 默认值 说明
jwtSecret '' 与 Document Server 共享的密钥(其 JWT_SECRET)。设置后编辑器配置与回调均 JWT 签名/校验;为空仅适用于 JWT_ENABLED=false 的 DS。
documentServerUrl '' 唯一的 Document Server 地址:dsh web 自身直连的 DS 入口——docker 网络上 DS 容器自带的 nginx(http://onlyoffice-documentserver)或发布到宿主的端口。同时充当反代上游与保存字节拉取基址。
internalBaseUrl (自动探测) DS 回连本 dsh web 服务器的基址(文档下载 + 回调)。空 = 自动探测本机非环回 IPv4 + web 端口。
publicBaseUrl (请求 Origin) 浏览器可见源(协议://主机[:端口]),用于铸造 X-Forwarded-Host/Proto 和匹配 DS 上报的保存 URL。dsh web 前面的网关会改写 Host 时必须设置;空 = 按每个请求的 Origin 推导。
defaultMode edit 新标签页的默认打开模式(edit/view)。
username User 编辑器右上角显示的协作用户名(editorConfig.user.name);留空恢复为 User。修改后重新打开文件生效。
fileLimitMb 100 提供与写回的最大文件体积(MB)。
tokenTtlSec 600 签名 URL 令牌的有效期(秒)。

0.2.0 之前的旧键(proxy、proxyUpstream、internalDocumentServerUrl、proxyPublicBase、documentServerPort)启动时映射一次——proxyUpstream/internalDocumentServerUrl 映射到 documentServerUrl,proxyPublicBase 映射到 publicBaseUrl——并打一条警告;请尽快迁移到新名称。

图形化配置(设置卡片)

组合装载了 DSH settings 服务时(标准 dsh web bundle 均装载),插件会注册 dsh-sidebar-onlyoffice 设置命名空间,Web 客户端的 设置 → 插件 标签页会为它渲染一张配置卡片——八个字段全部可视化,中英文案,暂存式编辑 + 保存/放弃。提交的修改写入部署的用户配置(<DSH_HOME>/settings.yaml),叠加在组合层之上,无需重启即生效:路由闭包与反向代理都按实时配置读取(jwt/mode/username/limit/token/URL 在下一次编辑器配置请求生效;documentServerUrl 变更则立即切换代理上游——已发现的版本化前缀会重置并重新探测,通过卡片首次配置出的地址会即刻认领挂载点)。因此部署可以带空的组合层条目启动,完全通过卡片完成配置。

字段优先级:卡片/用户层 → 组合层(cordis.patch.yml 的 config: 块)→ schema 默认值。在卡片上点"恢复默认"即回落组合层;直接手编 settings.yaml 同样实时生效。

保存 URL 改写(内网直连拉取)

两个方向各自独立配置:DS → 本插件用 internalBaseUrl(文档下载 + 回调);本插件 → DS 拉取回调上报的保存字节。DS 是按反代转发的浏览器可见基址(publicBaseUrl,否则请求 Origin)铸造这些保存 URL 的——反代部署下会绕公网回环(DNS + TLS + 终结器 + 网关)。插件把铸造前缀改写到 documentServerUrl(如 http://onlyoffice-documentserver,docker 网络容器名直连)——路径后缀与查询串原样保留,实现容器间直连。

安全性已实测验证(DS 9.4):/cache/files 的鉴权是 nginx secure_link 的 md5(expires + 请求路径 + 服务端secret),其中"请求路径"是剥掉子路径前缀后的路径,主机名与前缀都不在签名材料内,因此改写主机并剥前缀后仍返回 200。URL 不在公网基址之下时不改写、按原样请求并记一条 warn。

同源反代

本插件是纯反代架构:始终把 DS 的整套浏览器界面通过 dsh web 端口反代到 /sidebar/onlyoffice/ds。config 路由直接下发挂载点的 api.js 地址,编辑器其后加载的一切——iframe、sdkjs/web-apps 静态资源、字体、协同 socket、导出与下载——都经 dsh web 回流。浏览器完全不需要访问到 DS 的服务地址:把 documentServerUrl 指向 DS 容器入口,DS 可以留在内网。顺带消除混合内容坑:https 的 GUI 可透明反代只支持 http 的 DS。

与 DS(9.x,即本插件目标同代)的协作机制:

  • api.js 从自身脚本地址推导 DS 基址,编辑器从 iframe 地址推导 socket 路径——两者都天然落在挂载点之内。
  • 代理按官方「virtual path」形态发送 X-Forwarded-Host: <浏览器主机>/sidebar/onlyoffice/ds,DS 自行铸造的 URL(版本化脚本缓存 302、保存/下载链接)因此指回挂载点;绝对 Location 响应头还会被额外改写为挂载点相对形式。同样的转发头也随 WebSocket 升级腿转发——DS 在该连接上推导 /cache/files 文档数据基址。
  • 编辑器的 socket.io 路径为 <挂载点>/<版本>~<hash>/doc/<key>/c,其中 <key> 正是本插件的内容寻址文档 key。代理按打开的文档即时注册该精确 WebSocket 升级路由(DS status-4 关闭时注销、LRU 上限、DS 升级换版本前缀时自动重发现)。偶发漏注册则平滑降级:编辑器回落到 socket.io 轮询(普通 HTTP,走挂载点前缀路由),编辑不中断。
  • 两条代理腿(HTTP 与升级)都在与其他插件路由相同的浏览器信任围栏之内(同源编辑器放行;跨站页面拒绝)。

注意与边界:

  • 需要 9.x 一代的 DS(子路径 URL 铸造能力)。
  • 每次保存都会更换 key(内容寻址),升级路由随保存而更替——关闭即注销、有上限,实践中无感。
  • 若 dsh web 前面的网关会改写 Host(例如改成 127.0.0.1:3080),DS 响应体内铸造的 URL 会携带该浏览器不可达的主机:此时设置 publicBaseUrl 为浏览器可见源。
  • 安全取舍:DS 界面与 DSH 页面同源运行,DS 被攻破时可脚本化 dsh 源。围栏仍拦截跨站调用;请保持 DS 可信。
  • 静态资源走两跳(浏览器 → dsh web → DS);版本化不可变缓存可穿越该跳,但外层网关若要最佳性能,需把 /sidebar/onlyoffice/ds/** 纳入其缓存策略映射。

部署形态

典型部署:Document Server 容器与 dsh web 同一 docker 网络(容器名可解析,如 onlyoffice-documentserver——即 documentServerUrl),JWT 启用固定密钥,DS 容器加 ALLOW_PRIVATE_IP_ADDRESS=true。DS 完全不需要对外发布浏览器可达的端口。

排错:编辑器报 errorCode:-4「下载失败」

-4 是 DS 下载 document.url 失败。诊断入口是 DS 容器日志里 error downloadFile:url=... 的目标 URL:

  • URL 指向 DS 自己 → internalBaseUrl 配错成了 DS 地址。它必须是 DS 回连 dsh web 的地址(两者同网络时用容器名);改 profile 的 cordis.patch.yml 后 Cordis HMR 自动热生效,无需重启。
  • 404 → 路由不在(配错 host);403 → token 过期/签名不符(重启 dsh web 后重开文件);连接被拒/超时 → 网络不通(确认两个容器同网络,DS 需 ALLOW_PRIVATE_IP_ADDRESS=true)。
  • 快速自测(不开浏览器):带 Host: localhost 请求 config 路由拿到 document.url,再 docker exec onlyoffice-documentserver curl -v <该URL>,应当 200。

已知边界

  • 保存是整文件覆盖:与 agent 并发写同一文件时最后写入者胜。实时刷新收窄了该窗口——干净的编辑器约 1 秒内被推到最新版本——但脏编辑器在 AI 修改后再保存仍会覆盖之;保存完成时横幅随之清除(覆盖已解决分歧)。
  • 自保存抑制存在毫秒级竞态:恰落在插件保存与其 noteSelfSave stat 之间的外部写入可能被误吸收(漏推一次刷新事件;下一次变更即恢复)。实践中可忽略。
  • onRequestRefreshFile 事件需要 ONLYOFFICE Docs ≥ 8.3;refreshFile 本身已在 Document Server 9.4 社区版实测可用。
  • DS 需为 9.x 一代(子路径 URL 铸造);WebSocket 腿依赖按文档即时注册路由,漏注册时静默降级为 socket.io 轮询(老版本 DS 或路由冲突场景)。
  • key→文件 注册表持久化在 <DSH_HOME>/plugins/dsh-sidebar-onlyoffice/registry.json(最新 128 条),dsh web 重启后 DS 重投的保存回调仍能落盘。watch 流会自动重连(EventSource retry: 3000);跨重启保持打开的 viewer 在下次交互时重取配置。

安全

服务端半只会提供与写入会话工作目录之内的文件,浏览器侧路由位于 dsh web 信任围栏之内,DS 的每次下载都凭 config 请求时签发的短时效 HMAC 令牌。Document Server 由用户自行部署与配置——请将其部署在可信网络。完整立场与威胁模型见 SECURITY.md。

开发

pnpm install
pnpm run typecheck   # 类型门禁
pnpm test            # vitest(JWT/key/令牌、路由围栏与回调落盘、保存 URL 内网改写、同源 ds 反代、settings 接线、viewer 描述符、inotify 监听枢纽 + SSE 路由)
pnpm run build       # lib/index.js(Node 半)+ lib/client.js(ModuleLoader 包裹的浏览器半)

仓库结构:

src/
├── index.ts        # 宿主入口:webServer 四路由(含 dsProxy 接线)
├── dsProxy.ts      # 同源 DS 反代:挂载、转发头、即时 WS 路由
├── config.ts       # schemastery schema、基址推导/探测
├── settings.ts     # 设置区接线(图形化配置卡片的宿主半)
├── onlyoffice.ts   # 编辑器配置、JWT 载荷、文件令牌、保存 URL 改写
├── jwt.ts          # 极简 HS256 签名/校验(零依赖)
├── registry.ts     # key→文件 映射、原子写回、持久化
├── watch.ts        # inotify 监听枢纽(防抖、签名过滤、自保存吸收)
├── trust-fence.ts  # 浏览器请求信任校验(host/origin)
├── paths.ts        # 绝对路径 + 包含关系工具
├── wire.ts         # JSON body/错误工具
└── client/         # 浏览器半:viewer、编辑器挂载、i18n、设置卡片

许可

MIT