dsh-better-sidebar-onlyoffice
Đã xác minhdsh-better-sidebar-onlyoffice · v0.1.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).
Cài đặt
dsh plugin add dsh-better-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ẻ
Tác giả
Readme
dsh-better-sidebar-onlyoffice
DeepSeek Harness(DSH)web 插件:在 dsh-better-sidebar 的文件侧边栏中,用自部署的 ONLYOFFICE Document Server 打开并编辑 .docx / .xlsx / .pptx —— 配置经 JWT 签名、保存原子写回磁盘,并且 AI 修改已打开的文件时编辑器实时刷新。
功能
- 真正的办公编辑 —— better-sidebar viewer 内嵌完整的 ONLYOFFICE 编辑器(文字/表格/演示),不是静态预览;保存自动写回磁盘文件。
- 默认签名与围栏 —— 配置密钥后编辑器配置整体 HS256 JWT 签名;文件下载一律凭短时效 HMAC 令牌;浏览器侧路由在 dsh web 信任围栏之内;会话工作目录之外的文件一律拒绝。
- AI 修改实时可见 —— agent 修改编辑器中打开的文件时,viewer 约 1 秒内经
refreshFile换到新版本,页面不重载、api.js 不重载。脏编辑器绝不自动刷新(改为横幅提示手动重载)。 - 原子保存 + 自保存抑制 —— 回调下载保存字节后以临时文件 + rename 原子替换,同 key 串行化;插件自身写入被监听枢纽吸收,用户保存不会回环触发刷新。
- 内网直连拉取保存文档 —— 可选
internalDocumentServerUrl把 DS 保存 URL 改写到 docker 网络基址(容器间直连),避免绕公网反代回环。 - 与 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) [不重载]
- 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 服务器,浏览器也能直接访问它。
- JWT 启用时(DS 默认),插件的
jwtSecret必须与 DS 的JWT_SECRET一致。 document.url/callbackUrl为私网地址时,DS 需要ALLOW_PRIVATE_IP_ADDRESS=true(DS 默认拒绝)。
安装
从 npm 源安装(预构建 —— 无需构建权限):
dsh plugin --profile web add dsh-better-sidebar-onlyoffice
从 GitHub 仓库安装(源码 —— pnpm 会执行 prepare 构建;若 pnpm 拦截构建脚本,请在 profiles/web/pnpm-workspace.yaml 里放行):
dsh plugin --profile web add github:chendefine/dsh-better-sidebar-onlyoffice
也可以通过 DSH 插件市场(设置 → DSH插件市场)—— 仓库带 dsh-plugin topic,会被自动索引。
bundle 插件加入 profile 层栈后,把配置写进 profile 的 cordis.patch.yml 手编层(见下),重启 dsh web,浏览器硬刷新(Ctrl+Shift+R)。卸载:dsh plugin --profile web remove dsh-better-sidebar-onlyoffice 后再重启。
配置
所有键均可选;由 profile 的 cordis.patch.yml 层承载:
- id: dsh-better-sidebar-onlyoffice
config:
jwtSecret: "<DS 的 JWT_SECRET>" # 空 = 不签名(仅 JWT_ENABLED=false 的 DS)
# documentServerUrl: http://192.168.1.10:3082 # 浏览器加载 api.js 的基址;空 = 按访问 Origin 推导
# internalDocumentServerUrl: http://onlyoffice-documentserver # 本服务端拉取保存文档的内网基址(见下)
# internalBaseUrl: http://172.31.255.4:3080 # DS 回连 dsh web 的基址;空 = 自动探测
# documentServerPort: 3082
# defaultMode: edit # edit | view
# fileLimitMb: 100
# tokenTtlSec: 600
| 字段 | 默认值 | 说明 |
|---|---|---|
jwtSecret |
'' |
与 Document Server 共享的密钥(其 JWT_SECRET)。设置后编辑器配置与回调均 JWT 签名/校验;为空仅适用于 JWT_ENABLED=false 的 DS。 |
documentServerUrl |
(推导) | 浏览器侧 DS 基址(如 http://192.168.1.10:3082)。空 = 按每个请求的 Origin 主机 + documentServerPort 推导。 |
documentServerPort |
3082 |
按 Origin 推导 DS 地址时使用的端口。 |
internalDocumentServerUrl |
'' |
本服务端拉取 DS 上报保存 URL 的基址(docker 网络直连,如 http://onlyoffice-documentserver)。空 = 按上报地址原样请求(走公网入口)。 |
internalBaseUrl |
'' |
DS 回连本 dsh web 服务器的基址(文档下载 + 回调)。空 = 自动探测本机非环回 IPv4 + web 端口。 |
defaultMode |
edit |
默认打开模式;viewer 齿轮设置可按用户覆盖。 |
fileLimitMb |
100 |
提供与写回的最大文件体积(MB)。 |
tokenTtlSec |
600 |
签名 URL 令牌的有效期(秒)。 |
每个 viewer 的齿轮设置里还有两个共享开关:打开模式(edit/view)与文档服务地址(浏览器侧覆盖,任一卡片设置对三个 viewer 同时生效)。
内网直连拉取保存文档(internalDocumentServerUrl)
三个方向的地址各自独立:浏览器 → DS 用 documentServerUrl;DS → 本插件用 internalBaseUrl;本插件 → DS(下载保存字节)默认用 DS 在回调里自报的地址——反代部署下那是指向公网入口的 URL,保存流量会绕公网回环(DNS + TLS + 终结器 + 网关)。设置 internalDocumentServerUrl(如 http://onlyoffice-documentserver,同 docker 网络容器名直连)后,回调里的保存 URL 会把浏览器侧前缀(协议/主机/子路径前缀)替换为该基址,路径后缀与查询串原样保留,实现容器间直连。
安全性已实测验证(DS 9.4):/cache/files 的鉴权是 nginx secure_link 的 md5(expires + 请求路径 + 服务端secret),其中"请求路径"是剥掉子路径前缀后的路径,主机名与前缀都不在签名材料内,因此改写主机并剥前缀后仍返回 200。URL 不在浏览器侧基址之下时不改写、按原样请求并记一条 warn。
部署形态
典型部署:Document Server 容器与 dsh web 同一 docker 网络(容器名可解析,如 onlyoffice-documentserver),DS 发布在宿主机 3082 端口,JWT 启用固定密钥,DS 容器加 ALLOW_PRIVATE_IP_ADDRESS=true。
排错:编辑器报 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 修改后再保存仍会覆盖之;保存完成时横幅随之清除(覆盖已解决分歧)。
- 自保存抑制存在毫秒级竞态:恰落在插件保存与其
noteSelfSavestat 之间的外部写入可能被误吸收(漏推一次刷新事件;下一次变更即恢复)。实践中可忽略。 onRequestRefreshFile事件需要 ONLYOFFICE Docs ≥ 8.3;refreshFile本身已在 Document Server 9.4 社区版实测可用。- 浏览器必须能直接访问 Document Server(混合内容限制:https GUI 不能加载 http api.js——需要反代或
documentServerUrl指向 https 入口)。 - dsh web 重启会丢失 key→文件 内存映射,DS 侧已打开的编辑器在下次保存回调时会收到 error 1(DS 重试后失败),重新打开文件即可。watch 流会自动重连(EventSource
retry: 3000)。
安全
服务端半只会提供与写入会话工作目录之内的文件,浏览器侧路由位于 dsh web 信任围栏之内,DS 的每次下载都凭 config 请求时签发的短时效 HMAC 令牌。Document Server 由用户自行部署与配置——请将其部署在可信网络。完整立场与威胁模型见 SECURITY.md。
开发
pnpm install
pnpm run typecheck # 类型门禁
pnpm test # vitest(69 个用例:JWT/key/令牌、路由围栏与回调落盘、保存 URL 内网改写、viewer 描述符、inotify 监听枢纽 + SSE 路由)
pnpm run build # lib/index.js(Node 半)+ lib/client.js(ModuleLoader 包裹的浏览器半)
仓库结构:
src/
├── index.ts # 宿主入口:webServer 四路由
├── config.ts # schemastery schema、基址推导/探测
├── 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、设置