🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

苹果图书伴读 ianyu1201/DSH-apple-Read

DeepSeek Harness
ianyu120123 天前有提交已提供中文文档扫描:含敏感能力

围绕你的高亮笔记,语义检索整本书并陪你聊

30/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/10 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add ianyu1201/DSH-apple-Read
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么
dsh 原生插件 · search
装得上吗
实装验证未通过(unknown),装前请到仓库确认最近更新与 issue
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 23 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
⚠︎ 实装验证未通过(unknown · 2026/10/10) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 57 处,展示前 47 条)
  • 发起网络请求
    dsh-apple-read/lib/client.js:320
    const r = await fetch(`${API}/reading-workspace`);
  • 发起网络请求
    dsh-apple-read/lib/client.js:506
    void fetch(`${API}/reading-context`, {
  • 发起网络请求
    dsh-apple-read/lib/client.js:568
    const res = await fetch(`${API}/prepare-chat`, {
  • 读取本地文件
    dsh-apple-read/lib/index.js:58
    if (existsSync(candidate)) return candidate;
  • 读取本地文件
    dsh-apple-read/lib/index.js:157
    if (existsSync(CONFIG_PATH)) {
  • 读取本地文件
    dsh-apple-read/lib/index.js:159
    config = { ...config, ...JSON.parse(readFileSync(CONFIG_PATH, "utf8")) };
  • 读取本地文件
    dsh-apple-read/lib/index.js:167
    if (existsSync(credPath)) creds = readFileSync(credPath, "utf8");
  • 读取本地文件
    dsh-apple-read/lib/index.js:167
    if (existsSync(credPath)) creds = readFileSync(credPath, "utf8");
  • 执行外部命令固定命令
    dsh-apple-read/lib/index.js:226
    const ppid = Number(execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8" }).trim());
  • 执行外部命令固定命令
    dsh-apple-read/lib/index.js:236
    const out = execFileSync("ps", ["-eo", "pid,command"], { encoding: "utf8" });
  • 读取本地文件
    dsh-apple-read/lib/index.js:269
    const j = JSON.parse(await readFile(SIDECAR_FILE, "utf8"));
  • 写入本地文件
    dsh-apple-read/lib/index.js:276
    await mkdir(dirname(SIDECAR_FILE), { recursive: true });
  • 写入本地文件
    dsh-apple-read/lib/index.js:277
    await writeFile(SIDECAR_FILE, JSON.stringify({ pid, port, startedAt: Date.now() }), { mode: 0o600 });
  • 删除本地文件
    dsh-apple-read/lib/index.js:282
    try { await unlink(SIDECAR_FILE); } catch { }
  • 发起网络请求
    dsh-apple-read/lib/index.js:290
    const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: ac.signal });
  • 执行外部命令来自配置/变量
    dsh-apple-read/lib/index.js:300
    proc = spawn(UV_BIN, ["run", ENGINE, "serve-http", "--port", "0"], {
  • 发起网络请求
    dsh-apple-read/lib/index.js:383
    return await fetch(`http://127.0.0.1:${port}${path}`, init);
  • 发起网络请求
    dsh-apple-read/lib/index.js:430
    const upstream = await fetch(url, {
  • 发起网络请求
    dsh-apple-read/lib/index.js:457
    const upstream = await fetch(url, {
  • 写入本地文件
    dsh-apple-read/lib/index.js:601
    await mkdir(dirname(this.path), { recursive: true });
  • 写入本地文件
    dsh-apple-read/lib/index.js:605
    await writeFile(temp, JSON.stringify(this.data), { encoding: "utf8", mode: 0o600 });
  • 写入本地文件
    dsh-apple-read/lib/index.js:606
    await rename(temp, this.path);
  • 写入本地文件
    dsh-apple-read/lib/index.js:1101
    await mkdir(target, { recursive: true });
  • 发起网络请求
    dsh-apple-read/test-host.mjs:79
    const r = await fetch(`${BASE}${PORT}${p}`);
  • 发起网络请求
    dsh-apple-read/test-host.mjs:85
    const r = await fetch(`${BASE}${PORT}${p}`, {
  • 读取密钥环境变量
    dsh-apple-read/test-host.mjs:151
    const apiKey = (process.env.ARK_API_KEY || "").trim() || (() => {
  • 读取本地文件
    dsh-apple-read/test-host.mjs:153
    try { cred = readFileSync(join(process.env.HOME, ".dsh/.credentials.yaml"), "utf8"); } catch { }
  • 发起网络请求
    dsh-apple-read/test-host.mjs:160
    const r = await fetch(`${BASE}${PORT}/apple-read/api/chat`, {
  • 发起网络请求
    dsh-apple-read/test-host.mjs:191
    const r = await fetch(`${BASE}${PORT}/apple-read${p}`, {
  • 发起网络请求
    dsh-apple-read/test-host.mjs:197
    const r = await fetch(`${BASE}${PORT}/apple-read${p}`);
  • 发起网络请求
    dsh-apple-read/test-host.mjs:243
    const del = await fetch(`${BASE}${PORT}/apple-read/api/reading-context?sessionId=host-test`, { method: "DELETE" });
  • 读取本地文件
    dsh-apple-read/test-host.mjs:263
    existsSync(rdWanted) ? ok("目录已真的建出来") : bad("目录没建出来");
  • 删除本地文件递归
    dsh-apple-read/test-host.mjs:309
    try { rmSync(rdTmp, { recursive: true, force: true }); } catch { }
  • 删除本地文件
    dsh-apple-read/test-host.mjs:312
    try { unlinkSync(STORE_TMP); } catch { }
  • 写入本地文件
    dsh-apple-read/test-sidecar.mjs:30
    mkdirSync(TMP, { recursive: true });
  • 写入本地文件
    dsh-apple-read/test-sidecar.mjs:35
    writeFileSync(FAKE, `
  • 写入本地文件
    dsh-apple-read/test-sidecar.mjs:50
    writeFileSync(FAKE_SH, `#!/bin/sh\nexec "${process.execPath}" "${FAKE}" "$@"\n`);
  • 写入本地文件
    dsh-apple-read/test-sidecar.mjs:51
    chmodSync(FAKE_SH, 0o755);
  • 读取本地文件
    dsh-apple-read/test-sidecar.mjs:64
    const spawnCount = () => (existsSync(SPAWN_LOG) ? readFileSync(SPAWN_LOG, "utf8").trim().split("\n").filter(Boolean).length : 0);
  • 读取本地文件
    dsh-apple-read/test-sidecar.mjs:64
    const spawnCount = () => (existsSync(SPAWN_LOG) ? readFileSync(SPAWN_LOG, "utf8").trim().split("\n").filter(Boolean).length : 0);
  • 读取本地文件
    dsh-apple-read/test-sidecar.mjs:66
    const readPidfile = () => { try { return JSON.parse(readFileSync(PIDFILE, "utf8")); } catch { return null; } };
  • 发起网络请求
    dsh-apple-read/test-sidecar.mjs:104
    const r = await fetch(BASE + path, {
  • 写入本地文件
    dsh-apple-read/test-sidecar.mjs:125
    writeFileSync(PIDFILE, JSON.stringify({ pid: 999999, port: 1, startedAt: 0 }));
  • 读取本地文件
    dsh-apple-read/test-sidecar.mjs:163
    !existsSync(PIDFILE) ? ok("pidfile 已删除") : bad("pidfile 没删掉");
  • 读取本地文件
    dsh-apple-read/test-sidecar.mjs:174
    const src = readFileSync(new URL("./lib/index.js", import.meta.url), "utf8");
  • 读取本地文件
    dsh-apple-read/test-sidecar.mjs:198
    const logPids = existsSync(SPAWN_LOG)
  • 删除本地文件递归
    dsh-apple-read/test-sidecar.mjs:218
    rmSync(TMP, { recursive: true, force: true });
扫描于 2026/11/1(1 天前) · 范围 全包扫描 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 高亮笔记语义检索
  • 整本书语义问答
  • 侧边栏伴读面板

macOS「图书」App 的向量语义伴读插件 —— DSH bundle patch + MCP 引擎 + 侧边栏面板

README

由 DeepSeek 最新模型翻译生成
DSH-apple-Read · 让 DeepSeek Harness 给你的「图书」App 当伴读 继续用 macOS 原生的「图书」App 读书(高亮、笔记、阅读进度都不变),让 DeepSeek Harness 在旁边当陪读:全书向量语义检索、 读你的高亮和笔记、围绕你划的那句聊、一键把书在「图书」App 里打开。 - 检索完全在本地跑(bge-small-zh-v1.5 向量 + 字面加权),不花 API 钱,书稿不出本机。 - 只有最后「让模型组织答案」那一步走你自己配的模型。 - 不需要把书导成 EPUB、不需要上传任何东西 —— 直接读「图书」App 自己的书库。 能做什么 | 场景 | 说法 | | 围绕你划的重点聊 | 「我标的这句什么意思?」→ 它读你划的那句 + 所在章节 + 前后原文,而不是复述整本书 | | 换说法也能搜到 | 「一个人老了以后靠什么支撑着活下去」——字面匹配一条都召回不了,向量能 | | 问全书内容 | 「《书名》里这一段为什么这么写?」 | | 看你的标注 | 「我最近在书上划了什么?」 | | 跳回原书 | 「打开《书名》」→ 在「图书」App 里打开 | 接入方式:引擎以 MCP 挂进 Harness,模型侧工具名是 mcp__apple_read__(7 个);插件另外注册 apple-read 技能和一个侧边栏面板。
展开完整文档(README 剩余 1 节)
DSH-apple-Read · 让 DeepSeek Harness 给你的「图书」App 当伴读

继续用 macOS 原生的「图书」App 读书(高亮、笔记、阅读进度都不变),让 DeepSeek Harness 在旁边当陪读:全书向量语义检索、 读你的高亮和笔记、围绕你划的那句聊、一键把书在「图书」App 里打开。

- 检索完全在本地跑(bge-small-zh-v1.5 向量 + 字面加权),不花 API 钱,书稿不出本机。
- 只有最后「让模型组织答案」那一步走你自己配的模型。
- 不需要把书导成 EPUB、不需要上传任何东西 —— 直接读「图书」App 自己的书库。

能做什么

| 场景 | 说法 |
| 围绕你划的重点聊 | 「我标的这句什么意思?」→ 它读你划的那句 + 所在章节 + 前后原文,而不是复述整本书 |
| 换说法也能搜到 | 「一个人老了以后靠什么支撑着活下去」——字面匹配一条都召回不了,向量能 |
| 问全书内容 | 「《书名》里这一段为什么这么写?」 |
| 看你的标注 | 「我最近在书上划了什么?」 |
| 跳回原书 | 「打开《书名》」→ 在「图书」App 里打开 |

接入方式:引擎以 MCP 挂进 Harness,模型侧工具名是 mcp__apple_read__(7 个);插件另外注册 apple-read 技能和一个侧边栏面板。

仓库结构

DSH-apple-Read/
├── books_rag.py # 引擎:抽取 / 分片 / 向量 / 索引 / 检索 + CLI + MCP server │ # PEP 723 内联依赖,uv 自动装,不用建虚拟环境 ├── eval_rerank.py # 精排 A/B 评测(hit@1 / hit@3 / MRR) ├── eval_questions.example.json # 评测问题集示例(你自己的那份是 eval_questions.json,不入库) ├── test_rerank.py # 精排行为测试 ├── test_eviction.py # 模型空闲回收测试 ├── mcp_test.py # MCP stdio 冒烟测试 ├── dsh-apple-read/ # DSH 插件包(宿主半 + 客户端半 + 技能 + 面板) │ ├── cordis.patch.yml # bundle patch:插入 MCP 行 + 插件行(无可移植性负担,见下) │ ├── lib/index.js # 宿主半 │ ├── lib/client.js # 客户端半(已构建好) │ ├── assets/apple-read/SKILL.md │ └── tests/ # 插件侧测试 ├── LICENSE # MIT └── .gitignore

引擎在仓库根目录,插件在 dsh-apple-read/ 子目录。 插件启动时会从 profile 目录反解出自己的安装位置,再取同级的 books_rag.py —— 所以请从克隆下来的整个 DSH-apple-Read 目录安装插件,不要只拷 dsh-apple-read/ 这一层。

前置条件

- macOS,且用「图书」App 读书(数据源就是它自己的书库)
- DeepSeek Harness 桌面版
- uv(引擎靠它跑 PEP 723 脚本,自动装 Python 依赖,无需 venv)
- 可选:给 Harness 开「完全磁盘访问权限」——不开也能用全文检索,只是读不到高亮和准确书名

uv 在 PATH 里就行。插件会依次在 PATH、~/.local/bin、/opt/homebrew/bin、
/usr/local/bin 里找它;都没有的话用 APPLE_READ_UV 指定绝对路径。

安装

0. 授权(可选,但强烈建议)

「系统设置」→「隐私与安全性」→「完全磁盘访问权限」→ 点 + 添加 DeepSeek Harness.app (如果你从终端启动 Harness,把那个终端也加进去)→ 完全退出并重启 Harness。

高亮库在 ~/Library/Containers/com.apple.iBooksX/Data/...,属于 macOS 隐私保护目录,不给权限读不到。全文在 iCloud 容器里,不需要授权。

1. 克隆

git clone https://github.com/ianyu1201/DSH-apple-Read.git DSH-apple-Read
cd DSH-apple-Read

2. 先建索引(可选,一次就好)

uv run books_rag.py list # 看看书库认得几本
uv run books_rag.py index --all # 全量建索引(增量,没变动的书会跳过)

不建也能用,只是第一次问某本书时要等几十秒。首次运行会下载 91 MB 的 bge-small-zh-v1.5 模型(一次性,之后完全离线)。索引默认落在 ~/DSH-apple-Read/index/。

3. 装插件

在 DSH 的插件页选「从本地目录安装」,目录填克隆下来的插件目录:

/path/to/DSH-apple-Read/dsh-apple-read
装完重启 Harness。cordis.patch.yml 会在启动时插入两行(MCP 连接 + 插件本体)。

4. 验收

新会话里问一句「我最近在书上划了什么?」或者「《你的书名》讲了什么?」,能看到模型调用 mcp__apple_read__ 工具就通了。

配置

环境变量

| 变量 | 默认 | 作用 |
| APPLE_READ_DIR | iCloud 里的「图书」书库目录 | 换书库位置 |
| APPLE_READ_INDEX | ~/DSH-apple-Read/index | 索引目录 |
| APPLE_READ_MODEL | BAAI/bge-small-zh-v1.5 | 嵌入模型 |
| FASTEMBED_CACHE_PATH | ~/.cache/fastembed | 模型缓存 |
| APPLE_READ_UV | 自动查找 | uv 可执行文件路径 |
| APPLE_READ_ENGINE | 自动反解 | books_rag.py 路径(一般不用设) |
| APPLE_READ_READING_DIR | ~/Documents/deepseek-harness/读书会话 | 伴读会话的专用工作区 |
| APPLE_READ_STORE | ~/.dsh/storages/apple-read/data.json | 面板设置存档 |
| APPLE_READ_EMBED_TTL / APPLE_READ_RERANK_TTL | 900 / 180 秒 | 模型空闲多久回收;设 0 常驻 |
| ARK_API_KEY / DEEPSEEK_API_KEY | — | 面板聊天用的 key(也可以在面板里填) |

模型

dsh-apple-read/assets/config.json 提供默认的两个服务商(火山方舟 / DeepSeek 官方)。
apiKeyEnv 是环境变量名,不是 key 本身。改默认值请用面板设置(存在 APPLE_READ_STORE 里),assets/config.json 只作为出厂默认。

面板聊天需要 key。优先级:面板里填的 → 同名环境变量 → ~/.dsh/.credentials.yaml 里形如 ARK_API_KEY: xxx 的条目。

命令行(不装插件也能用) bash
uv run books_rag.py list
uv run books_rag.py search --book <书名> "<问题>" -k 5
uv run books_rag.py annotations --limit 20
uv run books_rag.py annotations --context --limit 5 # 带上所在章节和前后原文
uv run books_rag.py context --id <标注 id>
uv run books_rag.py open --book <书名>
uv run books_rag.py index --all
uv run books_rag.py serve # MCP stdio server

加 --json 拿机器可读输出。

测试 bash

引擎侧

uv run test_eviction.py
uv run test_rerank.py
uv run mcp_test.py

精排评测(需要自己的问题集,见下)

cp eval_questions.example.json eval_questions.json # 改成你自己的书和问题
uv run eval_rerank.py

插件侧(在 dsh-apple-read/ 目录)

node tests/md.test.mjs # 纯逻辑,不需要书库
APPLE_READ_INDEX="../.index" node test-host.mjs
APPLE_READ_INDEX="../.index" node tests/annotation-chat.test.mjs

测试不写死书名:默认取你书库里的第一本,也可以用 APPLE_READ_TEST_BOOK / APPLE_READ_TEST_QUERY / APPLE_READ_TEST_KEYS 指定。
书库为空或模型没下载时,相关用例会跳过而不是失败。

隐私

- 索引、向量、缓存、模型全部在本地:.index/(约 50 MB)、 .cache/(模型 + uv 缓存,1–3 GB 量级,随时可删)、~/DSH-apple-Read/index/。
- 检索不联网、不调用任何 API。书稿不出本机。
- 唯一的外发是你自己触发的「让模型组织答案」那一步 —— 发给你在面板里选的服务商,内容包含检索到的原文片段和你的高亮。
- 仓库里不含任何索引、缓存、模型权重、个人书单或凭据;.gitignore 已覆盖这些。

排错

| 现象 | 原因 / 处理 |
| 读不到高亮、书名不准 | 没给 Harness「完全磁盘访问权限」;加完必须重启 Harness |
| MCP 工具在对话里不出现 | 装完没重启;或 uv 不在 PATH 里(用 APPLE_READ_UV);或插件不是从克隆的 DSH-apple-Read 目录装的 |
| 问「我标的这句」却答成整本书 | 装了新版但没重启 Harness |
| 首次提问等很久 | 正在下载模型(91 MB)或给这本书建索引,之后就快了 |
| 某本书搜不到内容 | 带 DRM 的书(App Store 购买)读不到全文;或没有本地文件 |
| 扫描版图书 | 整本图片、没有文字层,引擎会明确报错并跳过,不会建一个满是噪音的索引 |
| 面板设置改了没生效 | 宿主半改动要重启 Harness;客户端半刷新页面即可 |
| 电脑变卡 | 排查 sidecar 残留:ps -eo pid,ppid,rss,command \| grep "books_rag.py serve-http" |

已知限制

- 只读非 DRM 的 EPUB。 App Store 购买的带 DRM 书拿不到全文。
- 扫描版(纯图片)图书无法检索,会明确报错并跳过。
- 高亮库是 Apple 的非公开 schema,系统升级可能失效。失效时降级为纯全文检索,不影响主要功能。
- spine 损坏的 EPUB 有兜底:spine 正文少得不像一本书时,引擎会自动从 manifest 补齐其余 HTML。
- 面板嵌入 Harness 原生会话用到的 sessions / uiSession / inputActions / renderFactorySlot 是未文档化的内部接口,所以客户端处处做能力探测:版本不匹配时给一句人话并保留左栏功能。

许可

MIT © 2026 ianyu1201

引擎里的高亮读取依赖 Apple 未公开的数据库 schema;「图书」App 本身的版权归 Apple。
预览分享卡片长什么样 ↓ianyu1201/DSH-apple-Read
分享 · DeepSeek Harness Hub
ianyu1201/DSH-apple-Read

围绕你的高亮笔记,语义检索整本书并陪你聊

回到顶部