中文EN

dsh-reading-workbench

uinpmMIT

DSH 阅读工作台:封面网格书架 + 单/双页分页阅读器,支持 Markdown、TXT、HTML、EPUB、MOBI、PDF,划线笔记以文本偏移锚定,并提供书目、划线、正文节选三个 Agent 工具。

下载量 / Downloads
336

安装

dsh plugin --profile web add dsh-reading-workbench

沙箱测试

这个插件还没测过。提交测试 →

README

reading-workbench 阅读工作台插件

把 DeepSeek Harness(DSH)变成阅读工作台:书架、分页阅读器、划线批注,并让 Agent 能直接读到你在读的正文。

功能

  • 书架:封面网格;支持 Markdown / TXT / HTML / EPUB / MOBI / PDF,另可把网页书源抓取成本地章节书
  • 分页阅读器:单页 / 双页,滚轮与键盘翻页,进度按「页码 + 百分比」双锚点保存
  • 划线批注:文本偏移锚点,页面重排后仍能恢复;支持与对话联动
  • 阅读设置:字号、行距、页宽、明暗主题;TXT 可切换「自动合并折行 / 保留原样」
  • Agent 工具:reading_books(书目)、reading_highlights(划线)、reading_excerpt(正文节选)

安装

dsh plugin --profile <profile> add dsh-reading-workbench

尚未发布到 npm 时,也可以直接用源码目录安装:在 profile 的 package.json 里以 link: 指向本目录,

并把包名加进 dsh.profile.bundles。Host 半代码改动需重启 DSH 才生效(Node ESM 模块缓存,

disable/enable 不重载)。

截图

待补。市场卡片优先展示作者策展的截图,且只接受 GitHub 图床地址。

开发状态

说明:下文出现的 docs/... 路径属于开发工作区(dsh-plugin/docs),不在本仓库内,
仅作为设计依据与实测记录的引用。
状态:三期完成并已通过真实例验收(2026-10-19);本轮新增滚轮翻页 + TXT 段落重排。
v1 的多格式支持(EPUB / MOBI / PDF)已在真实实例通过验收。
TXT 重排(2026-10-19 本轮):TXT 正文从「一个大 pre 原样保留源文件换行」改为
按段边界判据合并折行 + 每段一个 p + 首行缩进 2em + 段间距,并提供
「换行:自动合并 / 保留原样」设置项。判据、偏移契约与全库实测依据见
docs/txt-reflow-design.md;
独立对抗验收报告见 docs/diag-txt-linebreak/2026-10-19-reflow-verification.md。
全部自测套件全绿(21 套既有 + 本轮新增 txt-layout-selftest 193 项、
独立验收 txt-reflow-check 1817 项、真实浏览器几何 txt-reflow-browser-check 18 项);tsc 0 错误。
真实实例验收:三格式 + 封面 + 设置 + 划线 PATCH + 进度锚点全部通过(fiberPhase: active)。
本文档是该插件的唯一权威设计来源,随开发持续更新。
遵循工作区根目录 agents.md 规范;运行时契约以当天 inspect 为准,基准探查见 docs/dsh-runtime-notes.md(构建实测经验在第 10、11 节)。

Host 半运行在 vm 沙箱里(2026-10-19 两次真实事故,必读)

插件 install 成功、产物正确、静态 import 全部可解析,但 DSH 里 fiberPhase 一直 failed、

全部 REST 路由 401/404。两轮排查抓到两个独立根因。

事故一:沙箱里没有 process / Buffer

apply 第一行的 process.env.DSH_HOME 直接抛 ReferenceError: process is not defined,

cordis 记 _error、state 转 FAILED,连 14 条路由一起消失;writeJson 里的

Buffer.byteLength 是第二颗地雷(首次响应才炸)。

修复:数据目录改由 config.dshHome / ctx.get('sandbox'|'storage'|'workspace') 提供并回落 ~/.dsh;

编解码统一 TextEncoder / TextDecoder;readBody 返回 Uint8Array。

事故二:ctx.tools 没写进 inject

修完事故一仍然 failed。这次用宿主自己的启动参数起副本、把 stderr 落盘,才看到真话:

reading-workbench: Error: cannot get property "tools" without inject
    at …/lib/index.mjs:1612:12

cordis 里读未 inject 的服务是抛异常而不是返回 undefined——于是

if (ctx.tools === undefined) 这种"判空跳过"自己就会炸,且炸在 ctx.effect 回调里

会被记成整条 fiber FAILED,try/catch 兜不住。

修复:inject = ['webServer', 'tools'];服务读取移到 effect 之外并显式 try/catch,

取不到就只少三个工具,书架与阅读照常。

排查心法:list_plugins 只给一个 fiberPhase: "failed" 字符串,没有异常文本。

最快的路径永远是抓宿主 stderr,别靠本地猜。

防线:scripts/sandbox-contract-check.mjs(已接进 pnpm build / pnpm check)——

在不带 process 的 vm 沙箱 + 真实 cordis + 白名单 ctx Proxy 里跑 apply,断言 ACTIVE,

并带多条断言/反证(还原 process.env 必 FAILED;漏声明 tools 时只少工具、不拖垮宿主)。

完整事故复盘见 docs/m2-agent-tools-notes.md 第三、四节。

文本编码契约(2026-10-19 事故三,必读)

症状:导入一本 GBK 老 txt(蓝天航空公司的空姐.txt,1.05 MB)后满屏菱形问号

(�),夹杂零星拉丁/西里尔/希腊字母;同一本书打开还特别慢。

根因(两层,缺一不可):

1. 编码:File.text() / readFile(path,'utf8') 都是「UTF-8 宽松解码」,

GBK 字节被逐段解成 U+FFFD(�);恰好构成合法 UTF-8 序列的字节对被解成别的文字

(D5 B9 → 希伯来文等)。更糟的是导入时把已损坏的字符串写回书库副本

(library/<id>.txt),损坏被永久固化 —— 该副本 2.44 MB,含 681,143 个替换符。

2. 性能:txt 没有章节结构,旧实现把整本书当成一个排版单元塞进 CSS 多栏容器

(column-fill: auto + 固定高度),55 万字要一次排出 400~600 列。

现在立的规矩(改代码时必须守住):

1. 导入只收原始字节:客户端用 file.arrayBuffer() 走二进制通道

(/books/upload-binary,format=txt|md 也走它),Host 侧统一入口

store.importTextBookFromBytes(paths, meta, bytes)。

任何地方都不得先解码再上传/落盘。

2. 解码严格、二选一、失败即报错:shared/text-encoding.ts 的 decodeTextBytes

按 BOM → UTF-8(fatal) → GB18030(fatal) → 抛 TextDecodeError 判定,

绝不接受宽松解码产出的替换符。GB18030 是 GBK/GB2312 的超集,覆盖中文老书。

3. 两道损坏闸门:

  • 落库前 inspectReplacementDamage(>1% 且 ≥20 个)拒绝导入(importBookFromContent);
  • 打开时同一判据命中就报错「本书导入时被错误解码损坏,请重新导入」

(store.readBookContent)—— 已损坏的副本无法修复,只能重导,

绝不把乱码当正常内容渲染给用户。

4. 副本恒为规范 UTF-8:library/<id>.<format> 只有这一种编码,读取端不再猜。

txt 虚拟单元(性能):client/txt-units.ts 的 splitTextUnits 把正文切成若干单元

(章节标记行优先,否则约 2 万字一处;整本无换行的长文按字符强制切),

阅读器一次只渲染一个单元。偏移契约:unit.text === text.slice(unit.start, unit.end),

且 start 是全书字符偏移 —— 因此划线锚点仍是全书偏移,旧划线无需迁移,

渲染时用 toUnitRange 换算成单元内偏移。实测同一本书:556,817 字 → 27 个单元,

最大单元 36,364 字,单次排版量降到整本的 6.5%。

配套自测:scripts/encoding-selftest.mjs(编码表驱动 + 损坏判据 + 切分偏移契约)、

scripts/host-upload-selftest.mjs 的 10~12 号用例(GBK 二进制导入 / 非法编码拒绝 /

旧 JSON 通道拦替换符)。已损坏的历史数据用 docs/diag-blue-sky-encoding/repair-damaged-import.mjs

重导(自动备份副本与 data.json)。

Agent 工具(M2,2026-10-16)

三个只读工具把书库接进 DSH 会话(在对话里问「我有哪些书」「那本小说讲了什么」时,Agent 可直接查):

工具 · 参数 · 说明

reading_books · — · 书架列表(书名/作者/格式/进度);render 输出人读友好的书单

reading_highlights · bookId? limit? · 划线批注(含笔记),按时间倒序;可跨书查

reading_excerpt · bookId offset? length? · 正文节选(单次上限 4000 字),章节书剥成纯文本、图片占位为 [图片]

关键实现决策:不 import @deepseek-ai/dsh-tools

首版直接 import { defineTool } from '@deepseek-ai/dsh-tools',结果在测试阶段就炸了:

dsh-tools 0.1.0-rc.6 的 ESM 依赖 @deepseek-ai/dsh-llm 的 CallId 导出,

而本机运行时的 dsh-llm 0.2.0-rc.2 已把它改名 ToolCallId —— 版本族错位,import 阶段即抛

SyntaxError: does not provide an export named 'CallId',会炸掉整个宿主启动。

复盘 defineTool 的源码后确认它只是「schema 编译 + 参数校验包装」的纯数据变换,

因此按同形态自实现(含参数校验、ToolArgsError、parametersToJsonSchema),

不引入任何 @deepseek-ai/* 运行时依赖,工具对象与官方形态逐字段对齐。

tools-selftest 验证:注册形态、schema 编译、execute 输出、render 文本、过滤/倒序/limit、HTML 剥离、越界与报错,共 26 项。

设计取舍:工具只读(不提供写入/删除)——划线由人类在 GUI 里做,Agent 只消费。

划线批注(M3,2026-10-16)

正文选中文字 → 浮动「划线」按钮 → 入库并即时高亮;工具栏「划线 N」打开笔记抽屉。

定位模型:文本偏移锚点(重排后仍可恢复)

页码/屏号会随字号与视口变化,文本不会,因此划线锚点记录「章节 + 单元内字符偏移 + 长度」:

章节书:mark:ch=2:off=17:len=9     (ch 为章节序;段间 : 分隔、段内 key=value)
文本类:mark:off=120:len=30
  • 恢复时按偏移在单元文本里重建 <mark> 包裹;找不到(内容改版)静默跳过,不报错——text 始终随划线存库,笔记栏永远能展示原文。
  • 分隔符刻意设计:段间 :、段内 =。首版把 key/value 也用 : 连接(mark:ch:5:off:17),split 后 key 与 value 分家、全部字段解析失败——mark-selftest 的往返断言抓住了它。
  • 选区超长(>600 字符)拒绝并提示,防止把整章划成一条。
  • 笔记随划线存库,PATCH /highlight?id= 更新(正文不可改,只允许 note);空

…(truncated)

以上内容来自 npm 上的 README,我们只做格式化,不改内容。

版本

npm 上共发布:2,最后更新 2026-10-31

链接

GitHub · Homepage · npm

同类插件