跳过主要内容
C

dsh-better-sidebar-onlyoffice

chendefine/dsh-better-sidebar-onlyoffice

DSH web 插件:通过自托管 ONLYOFFICE Document Server 在 better-sidebar 编辑器中打开 .docx/.xlsx/.pptx(JWT 签名配置、网络内文档/回调路由、保存回磁盘)。

安装

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

README

dsh-better-sidebar-onlyoffice

中文 · npm · GitHub

A DeepSeek Harness (DSH) web plugin that opens and edits .docx / .xlsx / .pptx in the dsh-better-sidebar file sidebar through a self-hosted ONLYOFFICE Document Server — with JWT-signed configs, atomic save-back to disk, and live refresh when the AI edits an open file.

npm license node CI stars

Features

  • Real office editing — the full ONLYOFFICE editor (words / sheets / slides) embedded in the better-sidebar viewer, not a static preview. Saves write back to the file on disk.
  • Signed and fenced by default — editor configs are HS256-JWT-signed when a secret is configured; every file download is gated by a short-lived HMAC token; browser-facing routes sit behind the dsh web trust fence; files outside the session working directory are refused.
  • Live refresh on AI edits — when the agent modifies a file that is open in the editor, the viewer swaps to the new version within ~a second via refreshFile, with no page reload and no api.js reload. A dirty editor is never auto-refreshed (a banner offers a manual reload instead).
  • Atomic saves, self-save suppression — Document Server callbacks download the saved bytes and replace the file through temp-file + rename, serialized per document key; the plugin's own writes are absorbed by the watch hub so saving never echoes a refresh back.
  • In-network save fetch — optional internalDocumentServerUrl rewrites Document Server save URLs onto a docker-network base (container-to-container) instead of hairpinning through the public reverse proxy.
  • Coexists with the office preview plugin — viewer ids onlyoffice:docx|xlsx|pptx (priority 10) never clash with @huanlin/dsh-plugin-better-sidebar-plugin-office's docx/xlsx/pptx (priority 0); either side can be disabled per-viewer in the side card settings.

How it works

HalfLocationResponsibility
Host (server)src/Registers four routes on the dsh web webServer implementing the "document storage service" role from the official ONLYOFFICE integration docs.
Browser (client)src/client/Registers the three better-sidebar file viewers; loads the Document Server api.js on demand, mounts DocsAPI.DocEditor, subscribes to the SSE watch stream.
better-sidebar viewer (onlyoffice:docx|xlsx|pptx)
   ├─ GET /sidebar/onlyoffice/config    (browser, trust-fenced)
   │     → editor config + JWT + api.js URL + HMAC file token
   ├─ GET /sidebar/onlyoffice/file      (Document Server; HMAC token = auth)
   ├─ POST /sidebar/onlyoffice/callback (Document Server; status 2/6 → atomic save-back)
   └─ GET /sidebar/onlyoffice/watch     (browser SSE; inotify-fed disk changes)
         → change → re-fetch config → docEditor.refreshFile(config)  [no reload]
  • Node half — the config route builds the editor config for an absolute path inside the session cwd (documentType mapping, content-addressed key = sha256(host+path+size+mtime)), signs the whole config as an HS256 JWT, and registers the key→file mapping. The file route serves the raw bytes to the Document Server. The callback route (JWT-verified) downloads saved bytes on status 2/6 and atomically replaces the file, forgets the key on status 4, and always acknowledges with {"error":0}. The watch route streams disk changes as SSE, fed by an inotify watch hub (fs.watch on the file and its directory, debounced, signature-filtered through the same content-addressed key).
  • Browser half — loads the Document Server api.js once per URL, mounts the editor, calls destroyEditor() on teardown; failures show an error panel plus a download fallback link (reusing better-sidebar's /sidebar/file route). While mounted it subscribes to the watch stream and reacts to outside (AI) edits by re-fetching the config and calling docEditor.refreshFile(config).

Live refresh on AI edits

When an agent modifies a file that is open in the ONLYOFFICE editor, the viewer finds out within ~a second: inotify → SSE change event → config re-fetch (whose content-addressed key changed with the file) → refreshFile. The editor iframe itself is reused — no page reload, no api.js reload, and the DS opens the new version as a fresh session (old key closed with status 4). Guard rails:

  • A dirty editor is never auto-refreshedrefreshFile unconditionally drops unsaved edits (no isDocumentModified guard on the integrator path, verified in the DS source). Instead a banner appears ("file changed on disk — reloading discards unsaved edits") with a manual Reload button; clicking it is the user's confirmation.
  • The DS's own onRequestRefreshFile (fired on reconnect / same-key saves, only while NOT modified) rides the same refresh path.
  • The plugin's own callback saves are suppressed server-side (noteSelfSave records the just-written signature), so a user save doesn't bounce a refresh back and reset the cursor.
  • File deletion pushes a removed notice; re-creation pushes a change.

Requirements

  • A DSH web profile (dsh web), Node.js ≥ 20, with the dsh-better-sidebar plugin installed.
  • A self-hosted ONLYOFFICE Document Server (verified against 9.4 community edition) that can reach the dsh web server over the network, and that the browser can reach directly.
  • With JWT enabled (the DS default), the plugin's jwtSecret must equal the DS JWT_SECRET.
  • The DS needs ALLOW_PRIVATE_IP_ADDRESS=true when document.url / callbackUrl are private addresses (the DS refuses them by default).

Installation

From the npm registry (prebuilt — no build permission needed):

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

From a GitHub repository (source — pnpm runs the prepare build; allowlist the package in profiles/web/pnpm-workspace.yaml if pnpm blocks the build script):

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

Or through the DSH plugin marketplace (设置 → DSH插件市场) — the repo carries the dsh-plugin topic and is indexed automatically.

After a bundle plugin is added to the profile layer stack, write your config into the profile's cordis.patch.yml layer (see below), restart dsh web, and hard-refresh the browser (Ctrl+Shift+R). Uninstall with dsh plugin --profile web remove dsh-better-sidebar-onlyoffice and restart again.

Configuration

All keys are optional; the profile's cordis.patch.yml layer carries them:

- id: dsh-better-sidebar-onlyoffice
  config:
    jwtSecret: "<the DS JWT_SECRET>"          # empty = unsigned (only for JWT_ENABLED=false servers)
    # documentServerUrl: http://192.168.1.10:3082  # browser-side api.js base; empty = derive from the page Origin
    # internalDocumentServerUrl: http://onlyoffice-documentserver  # server-side fetch base for DS save URLs (see below)
    # internalBaseUrl: http://172.31.255.4:3080    # how the DS reaches dsh web; empty = auto-detect
    # documentServerPort: 3082
    # defaultMode: edit                             # edit | view
    # fileLimitMb: 100
    # tokenTtlSec: 600
FieldDefaultDescription
jwtSecret''Shared secret with the Document Server (its JWT_SECRET). Editor configs and callbacks are JWT-signed/verified when set; empty only fits a DS running JWT_ENABLED=false.
documentServerUrl(derived)Browser-side DS base URL (e.g. http://192.168.1.10:3082). Empty = derive from each request's Origin host + documentServerPort.
documentServerPort3082Port appended when deriving the DS URL from the page Origin.
internalDocumentServerUrl''Base URL this server fetches DS-reported save URLs through (docker-network direct, e.g. http://onlyoffice-documentserver). Empty = fetch exactly as reported (through the public entry).
internalBaseUrl''Base URL the DS uses to reach this dsh web server (document download + callbacks). Empty = auto-detect the first non-loopback IPv4 + the webserver port.
defaultModeeditDefault open mode; the viewer's gear setting overrides it per user.
fileLimitMb100Max file size (MB) for serving and saving back.
tokenTtlSec600Signed URL token lifetime in seconds.

Each viewer's gear settings add two shared toggles: open mode (edit/view) and document server URL (browser-side override; a value set on any card applies to all three viewers).

In-network save fetch (internalDocumentServerUrl)

The three directions are configured independently: browser → DS via documentServerUrl, DS → this server via internalBaseUrl, and this server → DS (downloading saved bytes) defaults to the URL the DS reports in its callback — behind a reverse proxy that points back through the public entry, so save traffic hairpins out (DNS + TLS + terminator + gateway). Setting internalDocumentServerUrl (e.g. http://onlyoffice-documentserver, the docker-network container name) rewrites the browser-side prefix (scheme/host/sub-path) of reported save URLs onto that base — path suffix and query stay intact — for a direct container-to-container fetch.

Safety verified against Document Server 9.4: /cache/files auth is an nginx secure_link md5 over expires + request-path + server secret, where the request path is the one AFTER the proxy strips the sub-path prefix — host and stripped prefix are not signed material, so a rewritten URL still returns 200. A reported URL outside the browser base is fetched as-is with a warn log.

Deployment shape

The canonical deployment: the Document Server container and dsh web share a docker network (DS container name resolvable, e.g. onlyoffice-documentserver), DS published on host port 3082, JWT enabled with a fixed secret, and ALLOW_PRIVATE_IP_ADDRESS=true on the DS container.

Troubleshooting: editor reports errorCode:-4 "download failed"

-4 means the DS could not download document.url. Start from the target URL in the DS container log (error downloadFile:url=...):

  • URL points at the DS itself → internalBaseUrl was misconfigured to the DS address. It must be the address the DS uses to call back into dsh web (use the container name when both share a docker network); editing the profile's cordis.patch.yml hot-applies via Cordis HMR, no restart needed.
  • 404 → wrong host in the URL; 403 → token expired/bad signature (reopen the file after a dsh web restart); connection refused/timeout → network unreachable (both containers must share a network; the DS needs ALLOW_PRIVATE_IP_ADDRESS=true).
  • Quick check without a browser: fetch the config route with Host: localhost to get document.url, then docker exec onlyoffice-documentserver curl -v <that URL> — expect 200.

Known limits

  • Saves are whole-file overwrites: concurrent writes to the same file by the agent race (last writer wins). Live refresh narrows this window — a clean editor is pushed to the newest version within ~a second — but a dirty editor that saves after an AI edit still overwrites it; the stale banner is cleared on save (the overwrite resolved the divergence).
  • Self-save suppression has a millisecond race: an external write landing between the plugin's save and its noteSelfSave stat can be wrongly absorbed (one missed refresh event; the next change recovers). Negligible in practice.
  • onRequestRefreshFile requires ONLYOFFICE Docs ≥ 8.3; refreshFile itself was verified against Document Server 9.4 community edition.
  • The browser must reach the Document Server directly (mixed-content rule: an https GUI cannot load http api.js — put a proxy in front or point documentServerUrl at an https entry).
  • A dsh web restart loses the in-memory key→file mapping; editors already open in the DS will see error 1 on their next save callback (the DS retries, then gives up) — reopening the file is the fix. The watch stream reconnects on its own (EventSource retry: 3000).

Security

The host half only ever serves and writes files inside a session working directory, browser-facing routes sit behind the dsh web trust fence, and every Document-Server download is gated by a short-lived HMAC token minted per config request. The Document Server itself is user-deployed and user-configured — deploy it on a trusted network. See SECURITY.md for the full stance and threat model.

Development

pnpm install
pnpm run typecheck   # type gate
pnpm test            # vitest (69 cases: JWT/key/tokens, route fences and callback saves, in-network save-URL rewrite, viewer descriptors, inotify watch hub + SSE route)
pnpm run build       # lib/index.js (node half) + lib/client.js (ModuleLoader-wrapped browser half)

Repository layout:

src/
├── index.ts        # host entry: the four webServer routes
├── config.ts       # schemastery schema, base-URL derivation/detection
├── onlyoffice.ts   # editor config, JWT payload, file tokens, save-URL rewrite
├── jwt.ts          # minimal HS256 sign/verify (no dependencies)
├── registry.ts     # document key → file mapping, atomic save-back
├── watch.ts        # inotify watch hub (debounce, signature filter, self-save absorption)
├── trust-fence.ts  # browser-request trust check (host/origin)
├── paths.ts        # absolute-path + containment helpers
├── wire.ts         # JSON body/error helpers
└── client/         # browser half: viewers, editor mount, i18n, settings

License

MIT

相关插件