dsh-files
taxueseek/dsh-files
文件上传(彩色附件卡片、会话隔离存储、sha256 去重、TTL 清扫)+ 内容嗅探的 read_document 文档读取(PDF/DOCX/XLSX/TXT)。
安装
dsh plugin --profile web add github:taxueseek/dsh-filesREADME
dsh-files
In one line
Let the AI actually read the documents you upload — and let your files come back out.
The two annoyances it removes
You have probably hit both:
- You hand the AI a contract PDF or a spreadsheet, and it says "I can't read this file." The file is fine — the built-in read tool only accepts plain text and refuses binary content outright.
- You want the AI to look at that attachment you uploaded days ago, but the attachment store is write-only: you cannot browse it, and you cannot pull the file back onto your machine.
dsh-files fills exactly those two holes. Read: the AI gets the text out of PDF / Word / Excel. Fetch back: you and the AI both see what is in the store, and you can pull a file back out.
What changed in this release
- SDK aligned with the host at 0.2.0-rc.2: fixes "the plugin button is missing and native upload is dead in the desktop App" — the plugin used to be built against the old SDK, colliding with the host's newer bundled components in the client module graph
- Errors now speak plainly: a failure used to hand you a cold error code; now it tells you what to change next. Reach the server over your LAN and the attachment routes answer 403 — the response prints the exact
trustedHostsline to add, ready to copy - LAN / domain deployments are documented now: that section used to be blank, leaving you to guess
- The division of work with the host is written down: what the host already does (upload, images, document preview) versus what remains unique to this plugin — so the wheel is not reinvented
Here is a file's four-stage session lifecycle, one official gap filled per stage:
- Ingest: the folder button next to the native paperclip (plus a same-named entry in the official "+" command menu) — the browser flattens the directory (Office lock files,
.DS_Store,.envand other system/hidden files are filtered), and every file enters the official native attachment pipeline - Read: the
read_documenttool — structured text extraction for binary documents (PDF / DOC / DOCX / XLSX) plus enhanced text reading (encoding fallback, paging, sheet-level access) - Manage: the
attachment_list/export_attachmenttools — make the attachment store visible to the model (name/size/sha) and copy a file into the workspace for read/edit/bash to work on - Fetch back: the attachment dock (one official pill below the composer card) plus download/export routes and an
@attachment source — the store becomes visible to users and files can be pulled into the browser (the right path for remote/LAN deployments); the@menu inserts the official handle line, identical to what the model saw at upload
Upload, images and
@reference were removed in 0.5.0 — harness 0.1.3 ships them natively (universal file upload, the image vision pipeline, unified@file/@sessionreference), and does it better. This plugin is part of the taxueseek plugin matrix; the flagship is argo.
Why it exists
Native upload in harness 0.1.3 stores files as byte objects and hands the model one handle line (name, size, digest, read-only path) to read with file tools — but the built-in read tool rejects binary content with FS_NOT_TEXT. Structured text extraction for PDF / DOC / DOCX / XLSX, plus attachment-store listing and export (the official GC is on the roadmap and the store is invisible to the model today), are the gaps the official stack leaves open; this plugin fills them.
Capabilities
- Content sniffing: PDF header / OLE Compound File (Word 97-2003) / ZIP central-directory members / UTF-8 (fatal) / UTF-16 BOM / GB18030 — decided from bytes, never from extensions; disguised files (an exe renamed .pdf) are rejected. The format hint is only a last resort when bytes are fully unknown
- Legacy .doc: macOS uses the system
textutil(most complete body and date lines in the gold-standard comparison), other platforms fall back to pure-JSword-extractor - Encoding chain: UTF-16 BOM → UTF-8 (fatal, NUL rejected) → GB18030 (fatal) → UTF-16 without BOM (high-confidence guard); GBK Chinese and BOM-less UTF-16 both read
- Paged reads: line numbers + offset/limit; the per-call character budget differs by format (text full, xlsx 3/4, pdf/doc/docx 1/2), overflow truncates with an explicit remaining-lines marker
- Line-number policy: text (code/config) carries line numbers for precise edits; PDF/DOC/DOCX/XLSX are paragraph flows without line numbers (saves tokens)
- XLSX sheet-level reads:
list_sheetsnames the sheets, thesheetparameter reads one sheet in full (no row cap), out-of-range errors list the available sheets
Install
Requires harness ≥ 0.1.3-alpha.1.
curl -fsSL https://raw.githubusercontent.com/taxueseek/dsh-files/main/install.sh | sh
# restart dsh web
Manual equivalent:
dsh plugin --profile web add git+https://github.com/taxueseek/dsh-files.git
# restart dsh web
The npm package named
dsh-filesis an unrelated third-party placeholder — install only via the script or the git command above.
Compatibility
| dsh-files | Harness | Notes |
|---|---|---|
| 0.5.5 | 0.2.0-rc.2 (current, measured) | SDK aligned with the host 0.2.0-rc.2 (dsh-fs / dsh-tools / dsh-client-ui-primitives all at the host version), so client components resolve without a version ambiguity. |
| 0.5.3–0.5.4 | 0.1.7-alpha.1 | SDK pinned at 0.1.7-alpha.1; on a 0.2.0-rc.2 host the client icons may fail to resolve (see below). |
| 0.5.x | ≥ 0.1.3-alpha.1 | Older SDK pins (0.1.0-rc.x); attachment dock and @ source predate the host's conversation.composer.dock slot. |
| 0.6.x | — | Never released (folded into 0.5.2/0.5.3); do not use. |
The plugin targets the alpha line the maintainer runs locally (0.1.7-alpha.1); npm latest (0.1.5-rc.3 at the time of writing) is older, so prefer the git install above over any registry version.
What was actually checked on 0.2.0-rc.2 (2026-09-29, web profile) and what it means:
- Surfaces still resolve:
conversation.input.left,conversation.composer.dock, the@source, thecommandUimenu contribution and everydsh-client-ui-primitivesicon used here exist in the official browser roster. - Attachment-store layout is unchanged (
files/<sha2>/<sha>/<name>); the library scan reads the real store, and theAttachmentStore.readFileStream/llm.fileRequestTextseams are unchanged. - The host plugin contract only grew:
dsh-tools0.2.0 adds optional members (ToolDefinition.projectContent?,PreToolDecision.ask.displayReason?); nothing the plugin relies on was removed. - One counter-example worth remembering:
@deepseek-ai/dsh-client-runtimeis a row, not a seed module. Declaring it indsh.client.injectrequires the host roster to carry that row; the official 0.2.0 roster dropped it, so a client half that declares it fails to load and takes the whole web boot with it. dsh-files does not declare it and is unaffected.
Configuration
- id: files-toolkit
name: 'dsh-files'
config:
maxFileBytes: 25165824 # byte cap for one document read
readLimit: 2000 # lines returned per call (paging is cheap)
sheetRowLimit: 200 # rows kept per worksheet
maxSheets: 5 # sheets read per workbook
maxOutputChars: 24000 # per-call window character budget (truncated with a marker)
readTimeoutMs: 120000 # per-call timeout (raise for huge PDFs)
# attachmentsDir: /path/to/attachments/v1 # attachment store root; empty = DSH_HOME / ~/.dsh autodetect
attachmentsEnabled: true # master switch for the attachment loop (dock/download/export/@ source)
maxDownloadBytes: 209715200 # per-download/export byte cap (answers 413)
trustedHosts: [] # non-loopback host[:port] authorities; required for LAN/domain (same semantics as --trusted-host)
Remote / LAN deployment
The attachment routes are fenced on the Host header (same semantics as the host's --trusted-host). A loopback deployment needs no configuration — opening http://127.0.0.1:3080 in a browser just works. The moment you reach the server through a LAN address, an internal hostname, or a reverse proxy, the Host is no longer loopback and every attachment route answers 403. That is the fence working, not a fault.
The only step to make it work is to put the authority from your browser's address bar into trustedHosts verbatim:
- id: files-toolkit
name: 'dsh-files'
config:
trustedHosts:
- '192.168.1.20:3080' # host:port matches that exact port
- 'dsh.example.com' # a bare host matches any port on that host
You do not have to guess: the 403 body prints the rejected authority verbatim in its hint, ready to copy. The shape is:
{
"error": "host-not-trusted",
"hint": "Browser host \"dsh.example.com:8443\" is not loopback and not in trustedHosts. … trustedHosts: [\"dsh.example.com:8443\"].",
"docs": "https://github.com/taxueseek/dsh-files#configuration",
"detail": { "host": "dsh.example.com:8443", "trustedHosts": ["dsh.example.com:3080"] }
}
detail.trustedHosts is the current allow-list, which is how you spot the most common cause of a 403 in one glance: the deployment moved ports (:3080 → :8443). A bare-hostname entry absorbs that; an exact host:port entry does not.
The panel and the @ attachment source surface the same hint (the @ source also logs one line with the HTTP status), so the interface itself tells you what to change — no log digging.
Failure response contract
Every failure from /plugins/dsh-files/attachments* has the same shape: a machine-readable error code, an actionable hint, a docs anchor, and (when there are live numbers) a detail payload.
error | HTTP | Meaning and next step |
|---|---|---|
host-not-trusted | 403 | Host is neither loopback nor in trustedHosts; the hint carries the authority to allow |
invalid-ref | 400 | ref must be sha256:<64 hex>, taken from the list route's ref field |
missing-parameters | 400 | Export needs both session and ref |
method-not-allowed | 405 | Export is a POST route; returns the Allow: POST header too |
session-without-workspace | 400 | That session has no workspace directory to export into |
attachment-not-found | 404 | No such content reference in the library; list it first |
attachment-object-missing | 404 | The index lists it but the object is gone; re-upload |
attachment-corrupt | 409 | Bytes failed the integrity check; re-upload the source rather than retrying |
attachment-too-large | 413 | Reports real size, cap and maxDownloadBytes; the other transfer path may still work |
list-failed / export-failed / attachment-read-failed | 500 | Carries the underlying reason and what to check |
Security
- Parsing dependencies are read-only and maintained:
pdfjs-dist(Mozilla),mammoth,read-excel-file,word-extractor(.doc fallback) - ZIP central-directory probing never expands members; malicious archives are rejected safely
- Reads and export destinations go through
ctx.fs, inheriting the session sandbox, same rights as the built-in read tool; the attachment-store scan is a host-side read-only walk with internally-constructed paths - Attachment download/export flows through the official
AttachmentStore.readFileStream(integrity check, no absolute paths), on top of the Host trust fence +sha256:reference whitelist + size cap; there is no delete route (content-addressed objects may be referenced by historical messages; deletion stays with the official future retention) - The dock and
@source are UI-layer data: no systemPrompt injection, no model tools, zero tokens
Development
pnpm install
pnpm test
pnpm build
npx tsc --noEmit
License
MIT