dsh-read
Manifest validRead local EPUB / MOBI / text books inside DeepSeek Harness: /read streams the text into the conversation like an AI reply, with speed control, pause and progress.
dsh-read
Read the EPUB / MOBI / plain-text books on your disk inside the conversation, streamed like an AI reply.
dsh-read adds a /read command to DeepSeek Harness. Type /read with a book and the text
appears as a card in the conversation flow, revealed one character at a time, with speed
control, pause, a table of contents and per-book resume.
You ▸ /read ~/Books/三体.epub
┌──────────────────────────────────────────────────────┐
│ 📖 三体 [EPUB] 42.7% · 第 118 / 642 段 │
│ ──────────────────────────────────────────────────── │
│ │ 港口的雾还没散,老周就已经坐在了堤坝上。 │
│ │ 他数着进港的船,一条,两条…… │
│ █ │
│ │
│ [暂停] [上一段] [下一段] [慢│中│快] 20 字/秒 [目录▾] │
└──────────────────────────────────────────────────────┘
Features
- In the conversation, not beside it. The reader is a real Chat node in the transcript,
rendered in the turn flow where you typed
/read. - Streams like a reply. Paragraphs are revealed character by character, with a caret on the active one.
- Speed, pause, seek. 慢 / 中 / 快 presets (10 / 20 / 60 字/秒), and a 自定义 slider from 10 to 160 characters per second that appears only once you pick it. Play/pause, previous/next paragraph, restart, and a chapter dropdown — and your speed choice is remembered.
- Chrome on demand. The header, the chapter line and the control bar fold away while you read and slide back the moment the pointer enters the card — or a control takes keyboard focus. Only the thin progress line stays behind.
- Progress everywhere. A live percentage in the card, and the same percentage listed per book in the 阅读 tab — saved per book, so reopening resumes where you stopped.
- Zero tokens.
/readruns entirely outside the model: the command's lifecycle is logged directly with no turn wrapping it, and the book text never enters the context. - EPUB, MOBI and text. EPUB is read through its OPF spine, MOBI through its PalmDB /
PalmDOC records, and
.txt/.mdthrough encoding sniffing that falls back to GB18030/GBK/Big5 for Chinese novels that are not UTF-8. - Natural-language discovery. A
book_searchtool lets the agent find a book by name; you then open it with/read.
Install
# from npm (once published)
dsh plugin --profile web add dsh-read
# from a local checkout
dsh plugin --profile web add -w link:/path/to/dsh-read
Use the profile you actually run (web for the browser GUI), then reload the GUI.
Usage
Settings → 阅读中心 (the reading centre)
Open 设置 at the foot of the sidebar, then pick 阅读中心 — the row whose icon is a book rather than the shell's generic gear. It lives here instead of as a tab beside 对话 / 轨迹, so the conversation header stays down to the two views that belong to a session.
- The directory bar starts at your library root. Type any absolute path and press 加载 to switch — that directory becomes the primary library root and is remembered.
- Subdirectories are listed first, so you can walk down to where your books live.
- Every book shows its format, size and progress (
已读 5.9% · 第 124 / 2083 段, or 未开始). - 开始阅读 / 继续阅读 runs the same
/readcommand in the session the shell currently has selected, then closes settings so you land on the card, resuming from the saved paragraph. - 重置进度 clears one book's position.
The /read command
The reading centre is a convenience over the command — both do exactly the same thing.
/read ~/Books/三体.epub # an explicit path
/read /Volumes/library/Dune.mobi
/read 三体 # a keyword, resolved against the library roots
A keyword is matched against file names under the library roots (the 阅读 tab manages the
primary one; the rest stay available for search). Defaults are ~/Books, ~/Documents/Books,
~/Downloads and ~/Documents; only directories that exist are used. You can also edit:
// $DSH_HOME/dsh-read/config.json (usually ~/.dsh/dsh-read/config.json)
{ "libraryRoots": ["/Volumes/books", "~/Calibre Library"] }
Or just ask the agent in natural language — it can call book_search and hand you the path:
帮我找一下《三体》 →
book_search({ "query": "三体" })→/Users/you/Books/三体.epub→ then/read /Users/you/Books/三体.epub
Supported formats
| Format | How it is read | Notes |
|---|---|---|
.epub | OCF/ZIP central directory → OPF manifest and spine → XHTML | Non-linear spine items are skipped; images, fonts and CSS are never inflated |
.mobi | PalmDB record table → PalmDOC LZ77 | Uncompressed and PalmDOC compression |
.azw3 / .azw | Same as .mobi, for the MOBI-7 text part | KF8-only files are rejected with a conversion hint |
.txt, .md, .markdown, .text, .log | Encoding sniffing | UTF-8, UTF-16, GB18030/GBK, Big5, Shift-JIS |
Deliberate limitations, each reported as an actionable error rather than a silent failure:
- HUFF/CDIC compression (MOBI type 17480) is not implemented — convert to EPUB.
- DRM-encrypted books are refused.
- ZIP64 EPUB archives are not supported.
- Files above 256 MB are refused.
How it works
The two halves every DSH plugin ships.
Host half (index.mjs, lib/) runs in the DSH node process and owns everything real:
filesystem access, ZIP and PalmDB parsing, HTML-to-text, the paragraph/chapter model,
library search and progress persistence.
export const inject = ['agents', 'commands', 'tools']— cordis throws when you read a service property you have not declared, and a throw insideapply()takes the entire composition down with it, so every service is declared up front.webServeris the deliberate exception.ctx.commands.register({ name: 'read', … })resolves one book and returns its metadata (path, title, format, character and paragraph counts, chapter index) as the command's settled result text.- A
book_searchmodel tool exposes library search to the agent — and nothing else. It never returns book text. - Five JSON routes back the tab and the reader card, registered through
ctx.inject(['webServer'], …)rather than the plugin'sinjectarray, becausewebServeris optional and published late and a headless profile would otherwise keep the plugin pending forever.
| Route | Purpose |
|---|---|
GET|POST /dsh-read/api/library | One directory level for the 阅读 tab: subdirectories, books, and each book's saved progress |
POST /dsh-read/api/start | Run /read <path> on a live session so the reader appears in the conversation |
GET /dsh-read/api/open?path= | Parse a book, return metadata and the table of contents |
GET /dsh-read/api/paragraphs?path=&from=&count= | One page of paragraphs |
GET|POST /dsh-read/api/progress | Read, save or clear reading positions |
/library parses only the books that already have progress: a percentage needs the paragraph
count, that is the one expensive field, and everything else in the listing is a directory
read.
Browser half (lib/client.js) is a hand-written ModuleLoader bundle — one
window.__ModuleLoader__.load({ id, factory }) call, no build step. It requires only react
and the injected slots / uiConversation services, and it uses four seams:
// The reading centre: one Settings page, id'd so it sits beside the shipped sections.
ctx.slots.register({ name: 'settings.section', id: 'dsh-read', order: 22, label: '阅读' }, LibraryView)
// Fold the durable /read lifecycle into one custom Chat node kind.
ctx.uiConversation.events.register({ kind: 'dsh-read', target: 'chat', match, start, update, buildViewNode })
// Render that node inside the conversation flow.
ctx.slots.register({ name: 'conversation.chat.node', key: 'dsh-read' }, ReaderCard)
// Replace the generic command row, which would otherwise dump the metadata JSON.
ctx.slots.register({ name: 'conversation.chat.commandview', key: 'read' }, () => null)
match claims command/run (where name === 'read') and command/done; update parses the
settled result text into the book metadata the card renders. The typewriter timing lives in
the card: one setTimeout per step, keyed on the current values, so changing speed or pausing
takes effect on the very next tick.
settings.section projects no icon field, so the shell paints its own generic gear beside every
external section. The bundle therefore tags its own nav row — matched by the label it registered —
with a data-dsh-read-settings-nav attribute and swaps that gear for a book in currentColor
(the same DOM adaptation dsh-better-sidebar uses, and undone on disposal).
The 阅读 page deliberately owns no reading state. Pressing 开始阅读 posts to /start — with the
session the shell has selected, from the useSessions standard prop — and the host runs the real
/read command there. Only once that succeeded does the page call close(), so it can never
claim a book is open when the card was not actually created.
Why the text is not a forged assistant message
The conversation's node key domain is fixed (assistant-step, user, tool-call, command,
steering, …) — a plugin cannot invent an assistant message, and it should not: forging one
would put the book into the model's context and persist it as something the model said.
Instead the plugin adds its own node kind and folds the ordinary durable command/run +
command/done pair that /read already logs. The result looks and behaves like a streaming
reply, costs no tokens, and stays fully reversible — the only thing in the session log is the
command you typed.
Because each /read is its own lifecycle, several reader cards can coexist in one session and
each keeps its own position.
Security
The reader API only ever reads files whose extension is in the supported book list, and the
/read command enforces the same whitelist — an explicit path to a non-book file is rejected
rather than decoded as text. It is not a generic file-read endpoint, which matters because
DSH's web server can be exposed beyond loopback. Progress and library roots are the only
things written, and only under $DSH_HOME/dsh-read/.
Development
node --test "test/*.test.mjs"
No dependencies and no build step. The tests build minimal EPUB and MOBI containers in memory
and read them back through the real parsers, drive the host's /read command and HTTP routes
against a fake cordis context, and verify the browser bundle's loader contract — including a
replay of the command/run + command/done fold that produces the reader node.
Layout
index.mjs host plugin: /read command, library search, routes
lib/book.mjs book model: paragraphs, chapters, LRU cache
lib/tool.mjs the book_search model tool
lib/html.mjs XHTML/HTML → text
lib/formats/epub.mjs ZIP central directory + OPF spine
lib/formats/mobi.mjs PalmDB + PalmDOC LZ77 + EXTH metadata
lib/formats/text.mjs plain text with encoding sniffing
lib/client.js browser bundle: the in-conversation reader card
cordis.patch.yml mounts the host plugin into the composition
test/ parsers, host command + API, client loader contract
License
MIT
Comments
Loading…
Similar plugins
by NakamuraIA
Read assistant replies aloud in DeepSeek Harness: text-to-speech providers with streaming playback.
★ 4
↓ 21/wk
MIT
TypeScript
Sep 14, 2026
dsh plugin --profile web add @nakamuraia/dsh-plugin-speechby cccc12138
A speaker button beside the Like button: read any DeepSeek Harness reply aloud with the browser's own speech engine. · 在点赞旁加一个小喇叭,朗读 DSH 的回复。
★ 0
MIT
JavaScript
Sep 18, 2026
dsh plugin --profile web add dsh-read-aloudIn-browser novel reader for the dsh web GUI: online book-source search, chapter-by-chapter reading in a chat-style view, and whole-book TXT download.
★ 0
dsh plugin --profile web add github:Wodexinhaoleng-Kasssa/dsh-readerby ralfsqual
dsh-pdf-reader: DeepSeek Harness plugin adding a read_pdf tool (local PDF text extraction via pdfjs-dist)
★ 0
MIT
JavaScript
Aug 15, 2026
dsh plugin --profile web add dsh-pdf-readerby zhuiyueya
Voice for DeepSeek Harness(dsh) — speech-to-text input + read-aloud TTS for text-only DeepSeek, zero API key.
★ 4
↓ 436/wk
MIT
JavaScript
Aug 15, 2026
dsh plugin --profile web add dsh-voiceby GGboya
DeepSeek Harness (dsh) 插件:论文伴读工作台 —— PDF 转录 / 检索 / 流式问答 + 内置阅读器页面。
★ 5
↓ 570/wk
NOASSERTION
TypeScript
Sep 24, 2026
dsh plugin --profile web add @ggboy123/dsh-paper-reader