Repository navigation
Releases
v0.18.9 - 修论文会话上下文串台
修复论文会话上下文串台
切换论文后,顶部当前论文已经是新论文,但具体会话里还带着上一篇论文的上下文。根因是「按工作区取最近会话」时只按 cwd 过滤,没有限制会话必须属于当前论文;于是同一个 Active SLAM 工作区里的其他论文会话可能被复用。
现在只复用 zotero-paper-<当前条目 key>(以及它的 -2、-3 实例),不会再复用工作区里别的论文或泛用会话。没有当前论文专属会话才新建。
复用会话时如果 chat store 没登记或没有上下文时间戳,会重新注入当前论文的 meta 上下文;并同步更新本地 chat store。
另外,Zotero 侧现在会把 PDF 附件 key 归一化为父条目 key,host 侧也会再次兜底。这样标题、Zotero key、PDF 和当前会话始终属于同一篇论文。
实测当前论文 99L7JHSL 的模型回复为:
Active view planning for visual SLAM in outdoor environments based on continuous information modeling / 99L7JHSL。
这次还重新构建了 host 与 client 两端;之前只跑 host build 会导致前端轮询代码没有进入 lib/client.js。
v0.18.8 - 修「会话没挂进工作区」
修了什么
「打开会话」按下去以后,DSH 停在「选择工作区开始」——会话切过去了,但界面认为它不属于任何工作区。
根因:sessionController.create() 只在拿到 workspaceId 时才会调 workspace.attachSession();只传 cwd 的话,会话 header 里的 cwd 是对的,但它不在任何工作区的 sessionIds 里,于是侧栏看不到、切过去也停在「选择工作区」。两者还不能同时传(gateway/bad-request)。之前的实现传的正是 cwd。
现在:
- 会话走
workspaceId创建,挂载由 controller 自己完成; - 已经存在的会话也补挂一次(早先的会话只带 cwd、没进过 sessionIds),
attachSession对已挂的是幂等的。补挂也走sessionController.create—— 它在会话已存在时是 adopt 语义,而workspaceController根本不暴露get,拿不到实体就调不了attachSession。
实测 workspace.json 里从 sessions=[] 变成 sessions=[zotero-paper-99L7JHSL],切过去之后输入框上方的选择器显示的是那篇论文的工作区,不再是「选择工作区」。
顺带两处提速
browsingContext 就绪等待从常量 500ms 改成每 20ms 探一次(上限 1s)。 以前是一刀切 sleep,而实测通常几十毫秒就好了——切换论文时这段是纯白等。
URL 解析加 30 秒缓存。 切换论文会重建整个 pane,每次都要读一遍日志尾部取 token,而日志可能几 MB、token 在一次实例生命周期里却是不变的。30 秒恰好覆盖「连续切几篇」,又不会把重启后的新 token 拖太久。
清掉的历史残留
<DshHome>/sessions/--C-Users-15775-.dsh-zotero--/ 下躺着 4 个 cwd 指向 DSH_HOME 的旧会话(zotero-paper-7R47AUHQ / CUILYLGY / GIHHY4ZF / GIHHY4ZF-2)。它们的 cwd 永远对不上任何论文工作区,所以永远挂不上、也永远不会被「按论文找会话」认领。整个目录挪到了 sessions-orphaned/,没有删除。
v0.18.7 - 选段带的是附件 key
那次会话为什么慢
一轮「这句话什么意思」,实际打了 11 次工具调用,其中 5 次是白打的:
| 调用 | 结果 | 原因 |
|---|---|---|
zotero_get_item CUILYLGY |
条目不存在 | key 是 PDF 附件,不是论文条目 |
zotero_retrieve CUILYLGY |
条目不存在 | 同上 |
zotero_health |
fetch failed | 当时 Zotero 没开(现在正常) |
glob **/* |
空 | 论文工作区目录本来就是空的 |
search_fulltext ×3 |
error | MCP 那侧的抖动,重试也没好 |
主因:选段送的是附件 key
Zotero 阅读器的 selection 事件里的 reader.itemID 指的是正在读的那个 PDF 附件,不是论文条目。插件直接把它的 key 当论文 key 送了出去,于是选段文件里写着 Zotero key:CUILYLGY——那是附件,条目其实是 99L7JHSL。模型拿着附件 key 去查条目,两次都只能得到「条目不存在」,然后才改走 MCP 那条路。
两头都修了:
- Zotero 侧顺着
parentItem换成条目再送,附件 key 另存一栏(要精确定位到哪个 PDF 时还有用)。 client.getItem拿到附件也会跟一步parentItem。这一层是兜底——历史选段、模型自己从别处拼出来的附件 key,都能用。
实测:GET /api/item?key=CUILYLGY 现在返回 found=true, key=99L7JHSL, title=Active view planning for visual SLAM...。
另外两处
科研预设里补了两条:选段的 key 现在是条目 key、工具会自动跟到父条目不用自己查;以及论文工作区目录一开始是空的,那是放自己产出的地方,别用 glob 去里面翻上下文——论文正文只在 Zotero 里。
还没动的
search_fulltext 的连续失败来自 zotero-mcp-plugin 那一侧,不是这套插件;zotero_health 那次失败只是 Zotero 没启动,现在 23119 正常应答。
v0.18.6 - 修面板卡在「加载中」
修了什么
面板有时会卡在「加载中...」,DSH 区域一片空白。
根因是两个 load() 在并发跑。Zotero 会在条目切换、面板重排时反复调 renderPane,每次都起一次嵌入流程;而 unmountEmbedded 是个全局清扫——它按 browser[data-zotero-dsh-embed] 把文档里所有嵌入浏览器都删掉,包括另一个正在跑的实例刚建的那个。于是两边都拿不到标题,各自跑满 5 轮 probe,最后那次再把状态栏写回「加载中」。
日志里的样子是一堆交织的 probe 编号(probe#0 #1 #2 #3 混着出现),最后跟一条 embed final title=""。
现在每次 load() 发一个代号:
- 旧的在每个
await点检查自己是否已被取代,是就把自己建的 browser 摘掉、直接返回; - 被取代的那次不许再写状态栏 —— 它会把后来者刚写好的「已连接」盖回「加载中」。
部署后 Mac 的日志立刻干净了:embed gen=1 superseded before load 紧跟着 embed final title="DeepSeek Harness"。
升级
两端 Zotero 都要更新 zotero-dsh.xpi。
v0.18.5 - 面板加「打开会话」
新增:面板上的「打开会话」
Zotero 面板顶部多了一个按钮,和「为这篇论文建工作区」并排。点它直接跳到这篇论文工作区里最近用过的那个会话;没有就现建一个再跳过去。
之前从 Zotero 想看这篇的会话,得先在侧栏里找到这篇论文的工作区、展开、再点会话行——三步。现在一步。
怎么实现的
Zotero 是另一个进程,够不到浏览器里的界面状态。所以分两段:
POST /papers/open-session在服务端算这篇论文的工作区目录(和papers/workspace共用同一个resolvePaperDir,两边不会分叉),ensure 出工作区,在sessionPersistence.list()里挑 cwd 等于它的会话取最新的那个,没有就按zotero-paper-<itemKey>建一个。然后把 id 存起来。- DSH 前端每 2 秒轮询
GET /pending-open,拿到 id 就调sessions.open(id)——和点侧栏会话行走的是同一条路。取走即清,否则下一次轮询会把用户从当前会话里再拽出去一次。
轮询只在 document.visibilityState === 'visible' 时发请求:这个页面常年挂在 Zotero 侧栏里,切到后台还在打自己的 HTTP 纯属浪费。
用 list() 而不是去猜 sessions/ 下的目录名——那个名字是 cwd 的自定义转义(~5B66 这类),反推规则等于把一个内部实现抄进插件。
顺带
paperWorkspace 里算目录的那段抽成了 resolvePaperDir,两个端点共用。各算一遍的话,迟早会在某个集合结构下分叉,然后就出现「工作区在这儿、会话在那儿」的错位。
v0.18.4 - 当前论文上下文 + 科研预设
这一版解决什么
之前的 dsh-zotero 只会「你点一下,它读一下」:模型不知道你此刻在 Zotero 里选中哪篇,也不知道那篇的 PDF 落在磁盘哪里。你得先把论文送进去,它才开始工作 —— 否则它只能反问,或者更糟:拿记忆里的论文硬答。
这一版把这件事变成每一轮都成立的上下文:
- Zotero 半边在选中变化时(节流 1.5 秒)把当前条目的标题、作者、年份、所在集合、PDF 磁盘路径、当前页码推给 DSH 的
POST /focus。 - DSH 半边用
systemPrompt.context注册一个zotero:focus段,文本在每次组装时重新求值。换一篇选中的论文,模型下一步就看到了 —— 不需要点按钮,也不用重开会话。 - 没选中任何东西时返回空串,组装层会把空段丢掉,所以不会往提示词里塞垃圾。超过两小时没动过也当作你已经离开那篇。
实测(模型回答原文):
以上全部来自本轮运行时上下文里的「当前论文 · 用户在 Zotero 里正选中这一条」那一段,我没有调用任何工具,也没有去文件系统核实这个路径。
那条 PDF 路径本身是给你核对用的,按规矩我不能拿它当读取入口 —— 要读正文我得走 zotero_read_pdf / zotero_read_fulltext / zotero_retrieve。
科研预设
包里多了一份 presets/research/:一个 DSH agent preset(「科研模式」),装配照抄内置的 standard,换掉人格段落。
人格里写死了三件事,都是文献工作里代价最高的失误点:
- Zotero 资料一律走 zotero_ 工具*,不许用文件系统去翻 Zotero 的存储目录 —— 那些路径是内部实现,翻到 PDF 也不知道它对应哪条。
- 引用纪律:转述必须能指回章节名 / offset / 批注 key;原文、批注、一般知识三者不要混;没读到就说没读到,不许按标题"补全"正文。
- 调研走 harvest 四步(scout → extract → verify → audit),不许用一次网页搜索当结论。
安装脚本会把它拷进 <DshHome>/.agent-presets/research,已存在就不覆盖 —— 那是给人改的东西。
安装
macOS / Linux:
bash install-dsh.shWindows:
pwsh -File install-dsh.ps1两端的 Zotero 插件(zotero-dsh.xpi)也要更新 —— 选中项的推送在那一半。
附带修掉的
zotero-dsh的version常量不再硬编码。之前升级后日志里还打着旧版本号,看日志的人会误判"新代码没生效"(我自己就被骗过一次)。- 界面文案改写器的占位符替换漏了根元素本身 —— React 新挂进来的 composer 元素自带
data-placeholder,而querySelectorAll只查后代,于是新会话永远显示旧占位符。
v0.18.1 - 修好 Zotero 10 上的面板嵌入
修了什么
Zotero 10 上面板一直显示"加载失败",且没有任何可用报错。
根因:Zotero 10 的 PageData actor 删掉了 loadURI 消息(只剩 prepareLoad)。于是 sendQuery("loadURI") 不再抛错,而是安静地 resolve 成 undefined —— 浏览器停在 about:blank,代码却以为导航已提交。
改成 Zotero 10 自己的做法(见 chrome/content/zotero/HiddenBrowser.mjs):
PageData.sendQuery("prepareLoad")- 父进程
browser.loadURI(Services.io.newURI(url), { triggeringPrincipal: getSystemPrincipal() })
旧的 sendQuery("loadURI") 保留为回退,Zotero 9 行为不变。
顺带修掉的两个坑
register-zotero-addon.py 现在会检测 Zotero 是否在运行并拒绝写入。Zotero 退出时会用内存里的插件表覆写 extensions.json,运行期间写进去的记录会被静默抹掉 —— 现象是"装的时候一切正常,重启后插件没加载",很难查。(macOS 上别用 pgrep -x Zotero 判断:Zotero 的进程名是小写 zotero,带 -x 的精确匹配是假阴性。)
注册的版本号改为从 xpi 的 manifest.json 读取,不再和脚本里的常量两处手改 —— 那两处迟早会漂。
安装
macOS / Linux:
bash install-dsh.shWindows:
pwsh -File install-dsh.ps1zotero-dsh v0.18.0 — PDF 选区送进 DSH
装什么
| 文件 | 装到哪 |
|---|---|
zotero-dsh.xpi |
Zotero 7 / 9 |
dsh-zotero-0.1.1.tgz + install-dsh.ps1 |
DSH |
Zotero 侧:工具 → 插件 → 齿轮 → Install Plugin From File… → 选中 xpi → 重启。
DSH 侧:两个文件放同一目录,pwsh -File install-dsh.ps1,然后重启 DSH。脚本只解包、建链、补一行锚点。
前提
- Zotero 7 或 9
- 一个跑得起来的 DSH Web 实例(
npm i -g @deepseek-ai/dsh,然后dsh web) - Zotero 的本地接口要打开:设置 → 高级 → 勾选 Enable Local API。
不开的话zotero_*系列工具全部报 "Local API is not enabled"
这一版
Zotero 侧 0.18.0(新)
- PDF 选区送进 DSH:在 Zotero 阅读器里选中文字,气泡里出现「送入 DSH」和「问 DSH」。
会话按 itemKey 懒开,不必先打开面板;首次送入选段会自带论文元数据。
走官方的renderTextSelectionPopup事件,不劫持原生行为 - 地址解析:base 取自偏好项、token 每次从实例日志现取(DSH 重启不再把面板打成 401)
- 挂载后回读标题落地确认;修正指向死仓库的 update_url
DSH 侧 0.1.1
- 新增
POST /quote:选段 → 该论文会话,ask:true时追加一轮解读 - 论文 ↔ 工作区映射(区/组两级);12 个
zotero_*工具 + 右侧栏面板
已知问题
pnpm install在干净机器上会 404(依赖 DSH 内部未发布的@deepseek-ai/dsh-type-meta),
所以直接发预构建产物- 选区按钮的呈现由 Zotero 的气泡渲染决定,主题差异下样式可能不完美
zotero-dsh v0.17.0 — 预构建安装包
装什么
| 文件 | 装到哪 |
|---|---|
zotero-dsh.xpi |
Zotero 7 |
dsh-zotero-0.1.1.tgz + install-dsh.ps1 |
DSH |
Zotero 侧:工具 → 插件 → 齿轮 → Install Plugin From File… → 选中 xpi → 重启。
DSH 侧:把 tgz 和 ps1 下到同一目录,跑 pwsh -File install-dsh.ps1,然后重启 DSH。
脚本只解包、建链、补一行加载锚点,不编译。
前提
- Zotero 7
- 一个跑得起来的 DSH Web 实例(
npm i -g @deepseek-ai/dsh,然后dsh web)
这一版
Zotero 侧 0.17.0
- 地址解析改成 base 取自偏好项、token 每次都从实例日志现取。
旧实现信任偏好项里的 token,DSH 一重启面板就停在 401 页 - 候选日志按优先级扫:
~/.dsh-zotero/web-3081.log→~/.dsh/restart-web.stdout.log→~/.dsh/web.stdout.log - 挂载后回读页面标题,401 也能导航成功,所以要落地确认
- 修掉指向不存在仓库的 update_url,转为从本仓库自更新
DSH 侧 0.1.1
- 论文 ↔ 工作区映射(区/组两级,组映射到真实目录)
- 12 个
zotero_*agent 工具 + 右侧栏面板 - MinerU 全文解析、标注回写、证据检索
已知问题
pnpm install在干净机器上会 404(依赖 DSH 内部的@deepseek-ai/dsh-type-meta,
该包未发布)。所以这一版直接发预构建产物,源码构建需要先有 DSH 环境- Zotero 侧更新检查依赖
packages/zotero-addon/updates.json,首次发版后生效