书架(dsh-bookshelf)
DeepSeek Harness 插件:把豆瓣某个用户的「想读」书单变成一个书架页,并把它和你自己目录里的电子书配对,可以在这个面板里直接读;没有的书可以去公版书库下载,或点开正版渠道。
它做什么
- 豆瓣想读书单:读取
book.douban.com/people/<ID>/wish,列出书名、封面、作者、出版社、年份、评分、ISBN、内容简介,按想读时间排序,支持搜索。 - 对豆瓣很克制,但会自动补全:刷新一次只发 2 个请求(列表页 + 卡片页拿封面),随后后台每 1.5 秒补一本详情(封面、评分、简介、ISBN),29 本约 1 分钟补齐;面板会自己轮询,不用点。豆瓣对匿名客户端的风控很紧,所以这个队列遇到 302/403 会立刻停手,20 分钟后自动接着补。
- 封面即继续读:书架上点封面直接进阅读页,并跳回上次读到的章节与页码(存在浏览器
localStorage,按文件记,最多留 120 条);卡片下方的书名/作者区域仍然打开详情页。卡上会出现「续读 第 N 节」的标记。 - 阅读页按书排版,整本连读:整本书铺成一条连续的双栏流(章节标题只是流里的块),翻到一章末尾会顺势进下一章,页码是全书页码;
←→/PageUpPageDown/ 滚轮都能翻页(一次滚动手势只翻一页:滚动增量累积到 40px 触发一次,之后同一次手势的余量被锁住,直到滚轮静止 260ms 才解锁,所以触控板的惯性滑不会连翻十几页),右上角的章节下拉退化成「目录跳转」。衬线↔黑体一键切换、字号 13–26px(存 localStorage),段首缩进 2 字、行距 1.95、两端对齐;纯文本单次最多渲染 30 万字。
双语对照(按章翻译)
阅读器右上角有一个 译 开关。打开后,只翻译当前正在读的那一章(微信读书的做法),译文以更小的灰色字排在每段原文下面,方便逐段对照:
翻译由 Harness 自带的
translator服务完成(匿名 Google / Bing 通道,或在设置里选定的付费通道),插件不自己接任何第三方接口,也不存账号密码插件自带一条可用通道:宿主直接请求匿名 Google 批量端点
translate_a/t(无需 key),不依赖 Harness 的translator服务- 一次请求带 最多 20 段 / 5000 字(该端点支持重复
q参数,返回按段对齐),所以一章通常只有 1 个请求、约 1 秒 - 不用
translate_a/single:它只收单段q,突发几个请求就会被 429 限流并跳转错误页 - 429 会按 700ms / 1.8s / 3.6s 退避重试 3 次;仍被限流则静默 30 秒(期间直接返回「稍后会自动恢复」,不再锤接口),阅读器底部显示原因并支持点击重试
- 背景一:
translator服务是给 agent 作用域组合的,profile 级插件拿不到(ctx.get('translator')为undefined),所以不能用它当唯一通道 - 背景二:即使拿得到,它的默认通道是 Bing,而
edge.microsoft.com/translate/auth现在返回 404,默认通道本身就是坏的 - 因此路由顺序是:能拿到
translator就先用它(google → 配置通道)→ 否则/失败则走插件内置通道;成功的那条会被记住,不会每段重试 - 只把当前章的段落文本发给翻译端点,不带任何账号信息;翻译端由 Google 提供
- 一次请求带 最多 20 段 / 5000 字(该端点支持重复
失败时阅读器底部直接显示真实原因(例如
译文失败:TRANSLATION_UNAVAILABLE: google / …(重试)),点一下就重试,不再只给一句笼统的提示只发哪些内容:当前章的段落,一批最多 18 段 / 9000 字;整本 800 页的书只翻一章,不翻整本
翻页进入新的一章时,才会去请求那一章;把
译关掉就停止请求译文有本地缓存(
$DSH_HOME/storages/bookshelf/translations.json,按「目标语言+原文」的 SHA-1 存),同一段第二次阅读不再走网络原文是英文则译成中文,原文是中文则译成英文(按字母/汉字比例判断)
只翻译文字:阅读器只把 EPUB 的段落排出来,图片本来就不进正文,所以不存在翻译图片的问题
失败时底部会显示「译文失败,点击重试」,成功部分照常显示
续读靠章节偏移:记录的是「第几章 + 章内第几屏」,换字号后仍然落在同一章同一屏附近,不会跑偏到别的章节。
翻页永远落在栏边界上:栏宽是实测的(拿一段跨栏的正文取
getClientRects()相邻片段的左边缘差),不是按clientWidth + gap推算——Chrome 的栏宽常是小数(实测 709.6667px),按公式累加每页会丢 0.3px,读到几十页后每行开头就被切掉几个字。每次翻页都是从页码反算绝对位置再对齐到实测栏宽,所以 200 页之后仍然不偏;字体换载或滚动停下时都会重新量一次并二次对齐。不用先配目录就能导入:
libraryDir留空时,导入的书会存到~/Documents/书架(自动创建),导入成功立即出现;设置的「书库目录」里还有「用下载目录 / 用文稿目录 / 用桌面」三个快捷键,~/Downloads这种写法会自动展开成绝对路径。工具栏的「导入本地书」不需要先打开某本书。可以和对话一起用:装了一个
bookshelf工具,可以直接在对话里问「我书架上哪本书讲过 X」「读一下《人月神话》第 3 章」——它会检索你本地 EPUB/TXT 的正文并返回原文片段。本地书库配对:扫描你指定的目录(epub / pdf / txt / mobi / azw3 …),按 ISBN 优先、书名其次和豆瓣书目自动配对。
资料区:和书一样排列:没和豆瓣书目配对上的文件(论文 PDF、说明书、随手丢进来的 TXT)不再隐形,书架下方单独一栏,按修改时间倒序排成封面卡片:PDF 用第一页渲染成封面(
pdftoppm,480px 宽,按2:3裁切只露页首,所以标题永远在封面上),标题优先取 PDF 自己的/Title,没有就从第一页正文里挑标题行(跳过作者、邮箱、arXiv 行;识别不了的乱码/模板占位/合同编号一律退回文件名),标题下面一行是「页数 · 大小」。卡片hover 无遮挡,操作在标题下方一行小字:显示 / 重命名 / 删除;区块标题右侧还有「打开文件夹」「导入资料」。封面按文件版本缓存在$DSH_HOME/storages/bookshelf/covers/papers/,第二次打开不再渲染。默认书库目录是~/Documents/书架(面板里直接显示这个路径,「打开文件夹」一键跳过去);从旧版本升级时,第一次扫描会把旧默认目录~/.dsh/storages/bookshelf/library里的文件搬过来(不覆盖同名文件,搬不动就留在原处并在面板上说明)。- 重命名只改名字、保留扩展名,会挡掉路径分隔符和开头的
.(否则会被扫描器当成隐藏文件而"消失"),同目录同名会拒绝;重命名后阅读进度跟着走(本地进度按文件 id 存,改名后由面板把旧 id 的记录迁到新 id)。 - 删除不是真删:文件被移到书库目录下的
.trash/(扫描器会跳过),面板里消失,需要时还能从 Finder 拖回来。
- 重命名只改名字、保留扩展名,会挡掉路径分隔符和开头的
论文 PDF 也能翻译:PDF 阅读页右上角多一个「提取文字」,调用 poppler 的
pdftotext把文字层抽出来,按页切成章节,换成和 EPUB 一样的双栏阅读/双语对照(只翻当前页,按需请求);「原文 PDF」随时切回原版。两份结果都按文件版本缓存,重新打开不再解析。- 整页挤成一段的情况(双栏论文被
pdftotext压平,一页 4000 字没有空行)会在句号处切成多段,单段上限约 900 字:读起来是段落,翻译时也是合适的单位。 - 图表不会丢:文本模式右侧默认并排显示当前页的原样扫描图(
pdftoppm渲染,120dpi,按页缓存在covers/pages/,上限 400 张),跟随翻页自动换页,工具栏「原文页」可收起/展开,选择记在 localStorage。 - 页面不顶到边框:右栏的灰底上居中放一张整页的「纸」(圆角、细边、投影,按栏高缩放到整页可见),右栏头部有「放大 / 整页」切换(也可以直接点图),放大时按栏宽铺满、上下滚动;窗口窄于 900px 时改成上下堆叠(文本上、原页下 45%)。为什么不做「把图片塞进正文」:论文里的图表大多是矢量绘制,抽出来只能变成低分辨率位图,表头/公式还会被拆散,位置也难对齐;直接展示原页既保真又便宜。文本栏变窄时自动从双栏切单栏。
- 就算真的收到超长段落(>2000 字),宿主也会在句子边界拆块、逐块翻译再拼回去,不会因为一段太长就让整页翻译失败;只有整批都拿不到译文才算失败,个别段落缺译文时底部显示「N 段未译」并保留其余译文。
- 整页挤成一段的情况(双栏论文被
面板内阅读:EPUB 在这里解析成章节段落直接渲染(跟随明暗主题);TXT 自动识别 UTF-8 / GB18030;PDF 用浏览器内置阅读器。
公版下载:从 Project Gutenberg 搜同名条目并直接下载 EPUB 到书库目录;Open Library 提供借阅 / 购买入口。
正版渠道:微信读书、豆瓣阅读、京东、当当、Open Library 的搜索链接。
导入自己的文件:选中一本书 → 导入本地文件,文件会存进书库目录并自动配对(适合你已经买过的电子书)。同名文件不会覆盖已有的,会存成
书名 (2).epub。不太可能拖垮 Host:解析上限 48MB(超了给提示而不是读进内存)、PDF 走 HTTP Range 分段读取、封面只允许
image/*回源并带nosniff。
发布到插件市场(可选)
除了发 zip,这个包也能像市场里的插件那样被安装:DSH 的「发现」页读取的是索引文件(format 1),索引里写「包名 + 精确版本 + 来源(npm 或 Git)」。
已经准备好的东西:
package.json去掉了private、补了keywords,npm pack只发 11 个文件(80 KB,无依赖、无构建脚本)publish/build-index.mjs:读package.json和locale/*.json生成publish/index.json(中英标题/描述、内联图标、版本、时间戳、engines.dsh),支持--git <repo> <commit>走 Git 路线publish/index.json:已用 DSH 自己的校验器(@deepseek-ai/dsh-plugin-market-index)验证通过,安装目标解析为dsh-bookshelf@1.0.0
四条路:
- npm:
npm publish+ 托管publish/index.json,别人加索引源即可一键装(要 npm 账号) - Git:索引里钉 commit,
source: { type: "git" }(不要 npm 账号) - 官方目录:向 awesome-dsh-plugin 提 PR,一个文件
data/plugins/<owner>__<repo>.yml(模板见publish/catalog-entry.yml,图标已生成assets/icon-180.png)。这条路不发 npm 也能收录,但仓库要创建满 1 天、加dsh-plugintopic;而「发现」里的市场索引只收 npm 版本,GitHub-only 条目只在列表页显示安装命令。 - 自己留档:zip +
install_bundle指向解压目录
别人那边是「插件 → 发现 → 管理索引源… → 填索引 URL → 安装」。逐步操作见仓库里的 publish/README.md(这个目录只是发布工具,不进 npm 包)。
分享给别人
这个插件是一个标准的 Harness bundle 目录(package.json + cordis.patch.yml + 宿主 index.js + 客户端 client.js + lib/ + locale/ + icon.svg),整个目录拷走就能装。
打包
cd ~/deepseek/workspace/书架
zip -r dsh-bookshelf.zip plugin -x '*.DS_Store' -x '*/node_modules/*'
对方安装
- 解压到任意位置,例如
~/dsh-bookshelf(目录里应当直接看到package.json、index.js、client.js)。 - 在 Harness 里安装这个本地 bundle:把解压后的目录绝对路径作为 target 调
plugin_manager的install_bundle(它负责写 profile 的依赖与 patch,不要手改~/.dsh/profiles/*)。 - 重启 Harness:宿主端(路由/后台任务)只在启动时加载,客户端只刷新页面即可。
- 侧边栏出现「书架」后,先点「设置」填自己的豆瓣 ID(豆瓣个人主页地址
/people/<ID>/wish里那串数字)——插件不预置任何人的 ID。 - 书库目录默认是
~/Documents/书架,导入或把电子书丢进去即可;也可以在设置里换成任意目录。
对方机器需要什么
- Harness ≥ 0.2.0、Node ≥ 22(
package.json的engines会挡住不满足的版本)。 - 「在 Finder 中显示」用 macOS 的
open,其他系统会提示不支持,不影响阅读。 - 论文 PDF 的封面缩略图和文字提取都用 poppler:
brew install poppler(pdftoppm/pdftotext/pdfinfo会依次在/opt/homebrew/bin、/usr/local/bin、/usr/bin里找——App 从 Finder 启动时PATH里没有 Homebrew)。找不到时面板会给出这句安装提示,卡片退回纯文字封面,PDF 仍可原版阅读,只是不能显示第一页/转文本。 - 翻译走匿名 Google 端点,无需 key,但需要能访问
translate.googleapis.com。
注意
- 每个人的豆瓣 ID、书库目录、翻译缓存都存在各自的
~/.dsh/storages/bookshelf/下,插件目录本身不含个人数据,可以直接分享。 - 不要把自己的
~/.dsh/storages/bookshelf/一起打包:那里面有你的书单缓存、翻译缓存和阅读记录。
配置
配置写在 cordis.patch.yml 的 row 里(也可以直接用界面里的 书架 → 设置):
| 字段 | 含义 | 默认 |
|---|---|---|
doubanUserId |
豆瓣用户 ID(个人主页 /people/ 后面那段) |
空(首次运行时在面板里填) |
libraryDir |
你自己的电子书目录 | 空 → 用 ~/Documents/书架 |
界面里改的偏好保存在 $DSH_HOME/storages/bookshelf/prefs.json,优先级高于 patch 里的配置;想回到 patch 的值,删掉这个文件即可。
数据与缓存
全部在 $DSH_HOME/storages/bookshelf/:
shelf.json:豆瓣书目 + 你打开过的书的详情缓存(没打开过的书不抓详情)prefs.json:用户在界面里保存的偏好covers/<subjectId>.img:封面缓存(豆瓣图床需要 Referer,由 Host 代取)- 书库目录本身不复制、不移动,只是索引并原样读取。
被豆瓣限流时
Douban 对匿名请求有频控。命中后 Host 会返回 豆瓣要求人机验证(HTTP 302) 或 HTTP 403,插件会:
- 保留已有书架(不清空),面板顶部显示提示与「重试」;
- 20 分钟内不再重试,避免把封禁时间拖长;
- 新书详情按需读取,不影响已经在书架上的书。
等一会儿再点「刷新」即可。
不做什么
这个插件不接入 Z-Library 之类的影子书库:那些站点分发的多数是未经授权的版权内容,自动化下载它们属于侵权。它只读取你自己的文件,以及公版书库(Project Gutenberg / Open Library)的合法内容,其余书目给出正版渠道入口。
结构
plugin/
├── index.js Host 半:/api/bookshelf/* 路由、缓存、后台抓详情
├── client.js 浏览器半:书架页、详情、阅读器、设置
├── lib/douban.js 豆瓣页面抓取与解析
├── lib/library.js 书目扫描、配对、EPUB(ZIP) 解析、纯文本解码
├── lib/sources.js Gutenberg / Open Library 检索与下载、正版链接
├── cordis.patch.yml Loader row(配置入口)
├── locale/{zh,en}.json 插件管理器里的标题与描述
└── icon.svg 插件图标
开发提示
安装方式(已在当前 profile 中完成):
plugin_manager install_bundle target=<这个目录的绝对路径>
改完 client.js / index.js 后:改 Host 半需要重启 Harness;只改浏览器半刷新页面即可(有 client HMR 时更省事)。本地没有打包/构建步骤,源码即产物。
No comments yet. Be the first to write one.