dsh-stream-upload
English | 中文
A dual-face DeepSeek Harness plugin for hybrid attachments from the Web composer. PNG/JPEG/WebP/GIF files are handed to the official durable image-attachment pipeline. Everything else — code, text, PDFs, DOCX/XLSX, archives — is streamed as raw bytes into the session workspace where the agent's own file tools already operate.
How it works
browser composer dsh host
┌─────────────────────┐ ┌──────────────────────────────────────┐
│ 📎 button │ image files │ native draft image + attachment store│
│ (conversation.input │──────────────────▶│ stock preview / validation / model │
│ .left slot entry) │ other raw bytes │ trust + quota → exclusive temp file │
│ draft ← path insert │──────────────────▶│ sha256 → atomic publish in workspace │
└─────────────────────┘ └──────────────────────────────────────┘
- Native images: raster images use DSH's
createDraftImages→addImagespath and stock attachment rail; this plugin never stores a duplicate image object. - Bounded-memory files: non-images stream socket→exclusive
0600temp file while a full SHA-256 is calculated. Quotas pass before an atomic hard-link publish; aborted and rejected bodies leave no partial file. - Trust fence:
/api/uploadis a longer prefix than/api, so it wins dispatch and must re-implement the official browser fence itself — Host ∈ loopback ∪ connection.trustedHosts,sec-fetch-site ≠ cross-site, Origin authority match. Reverse-proxy deployments behind--trusted-hostkeep working unchanged; everything else gets403. - Per-session isolation: a live session resolves its own workspace, storage roots and
session directories are realpath-contained and cannot be symlinks, directories are
0700, and the API returns only workspace-relative references. - Lifecycle and quotas: full-SHA-256 content dedup, per-file/session/workspace caps, file count and free-space admission, bounded concurrency, request timeout and per-file TTL sweep.
- Composer-native queue: a compact horizontal strip lives in the official
conversation.input.dockimmediately above the composer. It shows every selected file, byte-accurate progress for workspace uploads, native-image attachment state, size, completion or the retained failure reason. Active batches can be cancelled; successful/cancelled batches collapse after five seconds while failures remain until dismissed. Visual progress uses ARIA progress bars; state transitions use a polite live region. One borderless strip fills the composer's 780 px maximum width: a single attachment expands across the available row and the cancel/dismiss action stays inside the same visual surface. Multiple attachments share the row and scroll horizontally only after reaching their compact minimum width. - Composer integration: the button is a
conversation.input.leftslot entry. Every successful non-image upload appends one CommonMark resource link,[display name](<workspace/relative/path>), through the officialinputActions.setDraftprovide-channel face — no DOM scraping anywhere.
Install
This package uses ESM and has no prepare script, so installing it from Git does not require allowBuilds. Pin DSH to @deepseek-ai/dsh@0.1.2-rc.1.
dsh plugin --profile web add github:roojay/dsh-stream-upload#v0.4.0
Install from npm (once published):
dsh plugin --profile web add dsh-stream-upload@0.4.0
Install from a GitHub Release tarball:
dsh plugin --profile web add https://github.com/roojay/dsh-stream-upload/releases/download/v0.4.0/dsh-stream-upload-0.4.0.tgz
Install from a local directory:
cd /absolute/path/to/dsh-stream-upload
pnpm install --ignore-scripts
dsh plugin --profile web add "$PWD"
Restart the Web process after installation:
systemctl restart dsh # or however your deployment restarts `dsh web`
Remove the plugin:
dsh plugin --profile web remove dsh-stream-upload
Config (cordis.patch.yml row)
| key | default | meaning |
|---|---|---|
maxBytes |
99614720 (95 MiB) | hard cap for one non-image upload body; stays below Cloudflare's 100 MB request ceiling |
allowedExtensions |
[] |
lowercase allowlist; empty = all allowed |
uploadDirName |
.dsh-uploads |
workspace-relative storage directory |
uploadTtlMs |
604800000 (7 d) | stale upload lifetime |
sweepIntervalMs |
3600000 (1 h) | sweep interval; 0 disables |
maxConcurrentUploads |
2 | concurrent upload bodies |
maxSessionBytes |
1073741824 (1 GiB) | retained bytes per session |
maxTotalBytes |
2147483648 (2 GiB) | retained bytes below one workspace upload root |
maxFilesPerSession |
100 | retained files per session |
minFreeBytes |
536870912 (512 MiB) | filesystem free-space reserve |
requestTimeoutMs |
120000 | idle request timeout |
Upload feedback
The UI follows the established status-bar/file-card pattern: queued → uploading/attaching →
complete, failed or cancelled. The paperclip remains in conversation.input.left; the status strip
is a separate conversation.input.dock entry at order 30, sharing one isolated observable store per
session. Ordinary files use XMLHttpRequestUpload.progress because the browser Fetch API does not
expose portable upload-byte progress; the host route and raw streaming protocol are unchanged.
Success is not inferred from reaching 100% of sent bytes: the chip becomes complete only after the
server returns a validated workspace-relative reference.
Limits / roadmap
- v1 stores binaries as-is: reading PDF/DOCX content needs an agent-side tool or conversion step (see ecosystem plugins like markitdown converters if you want automatic document→Markdown).
- No folder upload or global drop handler; use the paperclip. The smaller surface avoids directory traversal, recursive quota and conflict problems.
- Uploads are whole-request transfers, not resumable chunks. Cancelling removes the host
.partfile; retrying a failed file currently means selecting it again.
Tests
npm test # node --test test/*.test.js — fence scenarios, transport, sweeper
No comments yet. Be the first to write one.