跳到正文
dsh-market 浏览插件 GitHub EN

wyzh0117/dsh-notebook

侧边栏记事本:带标题与正文的记事,可插入图片(拒收视频);点标题把正文复制到剪贴板;输入框里可用 @ 引用某条记事;可选在新会话时自动打开记事本。

Star 数 ★ 0 分类 UI 增强 收录于 2026-09-18

安装

在 DeepSeek Harness 里通过 dsh-market 安装

dsh plugin --profile web add dshmarket

或使用命令行

dsh plugin --profile web add github:wyzh0117/dsh-notebook

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。

README

dsh-notebook

DSH 官方右侧栏里的记事本。

+ 新建 → 写标题与正文 → 贴图 → 点完成,条目以标题陈列。 点标题复制正文 · 打 @ 引用记事 · 点编辑复用同一个容器。

CI

dsh-plugin · deepseek-harness · notebook · notes · sidebar

English · 中文

本次更新

  • v0.3.0 —— 页面现在只长在 DSH 的官方右侧栏里,别处没有:dsh-better-sidebar 载体和插件自绘的兜底外壳都已删除,openOnStart 由官方侧栏兑现;而 peer 范围必须升到 0.2.0-rc.2,bundle 才肯被加载。
  • v0.2.0 —— 会话也能往记事本里写:选中文字浮现「进记事本」动作;每条回答末尾的记事本图标可把整条回复存成一条记事。两个都默认开启。
  • v0.2.1 —— 正文框随内容自动缩放:写着变高、删掉变矮,最高到窗口高度的 60%。
  • v0.2.2 —— 去掉所有原生确认框(原生模态可能把嵌入式宿主卡死);所有文案跟随 shell 语言。
  • v0.2.3 —— 「新会话自动打开记事本」现在真的会打开侧边栏。

快速跳转 · 功能 · 安装 · 使用 · 兼容性 · 设置项 · 实现细节 · 已知限制 · FAQ · 开发

GitHub topics(仓库设置里加): dsh-plugin deepseek-harness notebook notes sidebar


这是什么

dsh-notebook 是 DSH(DeepSeek Harness) 的 Web 插件,在 DSH 自己的右侧栏里放一个记事本——一个通过官方 tab 注册表登记的 tab,不需要任何配套插件。

它只解决一件事:随手记一条带标题、正文和图片的短笔记,然后在输入框里用起来——点一下标题就把正文送进剪贴板,打 @ 能引用某条记事。

不引入富文本编辑器、不做云同步、不做版本历史。笔记是全局共享的(不按会话隔离),图片以文件形式落在宿主磁盘上。

功能

功能 说明
+ 新建条目 面板右上角的 +,点击后在面板内弹出编辑器——不新开窗口、不新开 tab
标题 + 正文 + 图片 单行标题、随内容缩放的正文、图片缩略图区,共用一个可滚动容器
可放图片,不可放视频 粘贴、拖放、「插入图片」三种入口走同一条校验:video/* 与任何已知视频扩展名(mp4/mov/webm/mkv/avi/m4v/ogv/mpg/mpeg/3gp/flv/wmv)一律行内拒收
「完成」后以标题陈列 列表一条一条以标题为单位显示,最新在上,次要信息是「时间 · N 张图片」
点标题复制正文 复制的是正文本身、不含标题;图片还原成 [图片: 文件名] 一行,并 toast「已复制正文(N 字)」。它永远不往输入框里写东西
@ 引用记事 输入框里打 @,在本地化的「记事本」分组下多出记事条目(标题 + 摘要,最新在前,最多 8 条);选中插入原子 chip,发送时只有正文交给模型
行内「对话引用」按钮 作用同 @ 选中,从列表行里直接插;够不到输入框时如实提示,不假装成功
选中文字 →「进记事本」(v0.2.0,默认开) 选区旁的浮动动作,把选区原文存成一条记事,标题是 未命名1、未命名2……输入框内与面板自身的选区有意不提供
每条回答 →「存入记事本」(v0.2.0,默认开) 回答动作行末尾的一个图标,一点把整条回复存成一条记事,标题用该会话自己的标题
两个捕获功能都可开关 设置里的 selectionToNotebook / messageToNotebook,下一次渲染即生效,不需重新加载
新会话自动打开(默认关闭) 每有会话变成当前就打开 Notebook——包括页面加载恢复出来的那个
「编辑」复用同一个容器 DOM 里编辑器始终只有 1 个
图片落盘 dataURL 上传,host 解码写入 $DSH_HOME/storages/notebook-attachments/<noteId>/;删除记事时一并删除
原子写,不静默丢数据 临时文件 → fsync → .bak → rename,并由 mutex 串行化。$DSH_HOME 不可写时降级为内存态,每个响应带 degraded: true,界面顶部显示非阻断提示
键盘 Cmd/Ctrl+Enter = 完成,Esc = 取消(草稿有改动时先问一句)

更深入的设计说明见〈实现细节〉。

截图

这一节暂时没有截图。 原先放在这里的几张图都是在 0.3 之前的外壳里截的,而那些外壳已经不存在了;迁移到官方右侧栏之后还没有重新截图。在那之前,〈功能〉表就是准绳:面板就是带 + 表头的记事列表,加上一个可复用的编辑容器,里面是标题、正文、图片缩略图和行内的视频拒收提示。

安装

DSH >=0.2.0-rc.2
Node >=20
包管理器 pnpm——本仓库不支持 npm
配套插件 无——页面渲染在 DSH 自带的那条右侧栏里

从仓库安装:

dsh plugin --profile web add github:wyzh0117/dsh-notebook

从源码本地挂载(开发用):

git clone https://github.com/wyzh0117/dsh-notebook.git
cd dsh-notebook
pnpm install
pnpm build            # 产出 lib/index.js、lib/client.js、lib/types/**

dsh plugin --profile web add "link:$PWD"

等价的纯手工做法——在 ~/.dsh/profiles/web/package.json 里:

{
  "dependencies": { "dsh-notebook": "link:/abs/path/to/dsh-notebook" },
  "dsh": { "profile": { "bundles": [ /* … */, "dsh-notebook" ] } }
}

然后在 profile 目录里 pnpm install。dsh.profile.bundles 必须包含 dsh-notebook,否则插件不会被加载。如果它已经列在里面、插件行却仍显示为已禁用,那是版本闸门跳过了整个 bundle——见〈插件根本不加载时〉。

本地开发时不要重启你正在用的那个 DSH(比如 3080 端口的 Web GUI,重启会杀掉当前会话)。需要真机验收时,另起一个隔离环境:

DSH_HOME=/tmp/dshnb-home npx -y --package @deepseek-ai/dsh dsh web --port 3099

使用

  1. 展开右侧栏,从它的 +(新建 tab)列表里选 Notebook——那一行带着插件自己的标题、说明和图标。右侧栏按会话各存一份界面,新建的会话里它默认是空的,所以要么每次手动加,要么打开「新会话自动打开记事本」。

  2. 点右上角 + → 编辑器在面板内弹出。

  3. 写标题和正文。正文框随内容缩放(最高到窗口高度的 60%,再长就在框内滚动)。配图可以粘贴 / 拖入图片,或点「插入图片」——视频会被拒绝;单图上限 10 MB,单条上限 20 张(都可在设置里调)。

  4. 点「完成」(或 Cmd/Ctrl+Enter)→ 条目以标题陈列。

  5. 点标题文字 → 正文进剪贴板。

  6. 想让模型读某条记事:输入框里打 @ 选它,或点该行末尾的 对话引用。发送时只有正文交给模型;页面刷新后需要重新插入引用。

  7. 点行尾 编辑 → 同一个容器载入该条;点 删除 会先用面板自己的对话框问一次。

  8. 让会话替你写一条记事(v0.2.0):

    • 在会话里选中文字 → 点选区旁浮现的 「进记事本」。
    • 点回答末尾的记事本图标 → 整条回复按会话标题存成一条记事。

    两者都用同一个 toast 确认,也都可以在设置里关掉。

兼容性

DSH >=0.2.0-rc.2——只有这条版本线带上本插件要注册进去的官方右侧栏(ctx.sidebarRightTabs)
Node >=20
配套插件 没有。host 半侧只需要 webServer、cordis 与 schemastery,tab 要出现不需要再装任何东西
DSH 侧可选接缝 缺 conversation、inputTriggers 或 sessions 时,对应的联动能力自动关闭,记事本本体照常工作
旧版 shell 每个功能都注册在可选接缝上,只会降级不会坏:槽位不存在则捕获入口根本不出现;读不到 locale 就用插件自带的 zh 字典;面板不依赖 window.confirm 或任何 dialog API

页面住在哪儿——官方右侧栏

只有一个载体:DSH 自己那条右侧栏。tab 分两阶段注册,先本体后类型:

  1. tab 本体进 keyed 槽 sidebar.right.pane.tab,key: 'dsh-notebook';
  2. tab 类型进 ctx.sidebarRightTabs.register({ id: 'dsh-notebook', kind: 'dsh-notebook', priority: 'extension', title, guide: [{ id, order: 60, title, description, icon }] })。

三条由注册表说了算、不容商量的约束:

  • 槽位的 key 必须等于第一阶段的 id——席位是拿「当前 kind 对应类型的 id」去派发条目的。在 keyed 槽上使用列表形状的 { id, order } 不是「差一点就对」,而是一次失败的注册,而侧栏给出的回应是一个永远画不出来的 Notebook tab;
  • guide 条目必须自带稳定的 id,否则注册表抛 duplicate guide entry id;
  • 先本体后类型,于是一个只注册了一半的宿主绝不会显示一个画不出来的 tab——任何阶段失败,同批注册一起拆掉。

tab 本体通过槽位注入的 useTabInfo() 钩子读自己的可见性,因为席位渲染 tab 时给的是空的 owner share,tab.visible 根本不会作为普通 prop 到达。这正是「隐藏的 tab 真的不加载、不轮询」得以成立的原因。

src/client/hosts/controller.ts 先探测 sidebarRightTabs;若这个服务还没到(cordis 按依赖顺序派发插件,不是侧栏优先),就用 ctx.inject(['sidebarRightTabs'], …) 等一次。没有兜底外壳:服务始终不出现就什么都不注册,并把原因写进日志。所有注册都包在 ctx.effect(() => { …; return dispose }) 里,HMR / 禁用安全。

「打开」本身就是「展开」。 ctx.sidebarRight.openTab(kind) 在打开 tab 的同一次意图里就展开了这一列,而在屏幕上没有 Session 时它会抛。isExpanded() 回答的是席位上一次已提交渲染时绑定的 surface——所以处在成功打开的那个同步块里时它依然报 false——而 toggleExpanded() 翻的是实时值。读前者、再用后者去「纠正」,就会把刚被打开揭示出来的那一列收回去;这就是 v0.2.2 的 bug,所以这个调用之后什么也不能再接。

插件根本不加载时

dsh 0.2.0-rc.2 的 dsh-app-boot 会读 package.json 里每一个名为 @deepseek-ai/dsh 或以 @deepseek-ai/dsh- 开头的 peerDependency,然后跑 semver.satisfies(runtimeVersion, range, { includePrerelease: true })。任何一条不匹配,整个 bundle 就被跳过:模块根本不会被 import,插件行显示为已禁用,日志里是

dsh: skipping profile bundle "dsh-notebook": … is incompatible with dsh 0.2.0-rc.2: peerDependencies { … }

由此引出两件会让人意外的事:

  • rc.2 并不校验 dsh.plugin.json 的 engines.dsh——只有 peer 范围说话,所以 engines 写得再对也救不了一个过期的 peer;
  • 服务端那半不是原因:本插件从来只需要 webServer、cordis 和 schemastery。把它挡在 0.2 外面的是 package.json 里的版本闸门,不是代码。

逃生门是 dsh plugin allow-version,它把一条精确版本的豁免写进 <profile>/compatibility.json;对应的 disallow 命令可以撤销。用它去看清一次不匹配,而不是长期带着它——peer 不满足通常意味着真实存在的 API 差异。

设置项

一份定义,一个席位。真值存在 host 的 NotebookDoc.prefs——读写一律走 PATCH /notebook/api/prefs——而八个开关全部出现在 DSH 的全局设置页里,因为官方侧栏没有 per-tab 的设置页。

键 类型 默认 说明
sortOrder 'updated' | 'created' | 'title' 'updated' 列表排序
copyImagesAsName boolean true 复制正文时把图片写成 [图片: <文件名>] 一行
maxImagesPerNote number 20 单条记事图片数上限(1–100)
confirmDelete boolean true 删除前用面板自己的对话框确认;关闭则点一下直接删
openOnStart boolean false DSH 启动即展开右侧栏并打开记事本
autoOpenOnNewSession boolean false 每当有会话变成当前就打开 Notebook
selectionToNotebook boolean true 在会话里选中文字时显示浮动的「进记事本」动作
messageToNotebook boolean true 在每条回答的动作行末尾显示「存入记事本」图标

这个设置区由侧栏宿主自己注册(hosts/settingsSeat.ts → settings.section,id dsh-notebook,order 100),而槽位没被声明时注册就是个 no-op——所以一个没有设置对话框的组合永远不算错误。两个自动打开的开关走的是和用户点 tab 完全相同的手势:openOnStart 在宿主注册好时触发一次,偏好从 host 读回来时再触发一次,而在屏幕上没有会话时它是个被安全吞掉的 no-op。

实现细节

以下内容面向读代码或改代码的人。只想用插件的话可以直接跳到〈已知限制〉。

架构

                     ┌──────────────────────── 浏览器 ────────────────────────┐
                     │ lib/client.js  (CJS 模块表工厂, id = "dsh-notebook")   │
                     │  controller:探测 sidebarRightTabs → 原生宿主          │
                     │  NotebookView ─ NotebookEditor(唯一实例) ─ clipboard    │
                     │  capture (v0.2.0):选区浮动条 + 回答图标 → capture.ts  │
                     │  正文缩放 (v0.2.1):textarea → autoGrow.ts             │
                     │  确认框 (v0.2.2):面板内对话框 → locales.ts            │
                     └───────────────────────────┬───────────────────────────┘
                                    fetch JSON   │
                     ┌───────────────────────────┴───────────────────────────┐
                     │ lib/index.js   (cordis 插件, ctx.webServer prefix)     │
                     │  routes.ts ─ store.ts (原子写 + mutex) ─ attachments.ts│
                     └───────────────────────────┬───────────────────────────┘
                                                 │
                            $DSH_HOME/storages/notebook.json
                            $DSH_HOME/storages/notebook.json.bak
                            $DSH_HOME/storages/notebook-attachments/<noteId>/<attachmentId>.<ext>
  • src/client/autoGrow.ts 是正文框几何唯一的决定处:它测量 <textarea> 的内容并写 height / maxHeight / overflowY,而「什么时候测」由 NotebookEditor 决定(挂载时、文本变化、宽度变化、视口高度变化)。
  • tab 体通过槽位注入的 useTabInfo() 钩子读可见性(席位给它的是空的 owner share),所以隐藏的 tab 真的不加载、不轮询。
  • 图片用 dataURL 上传(客户端 FileReader),host 解码落盘——避免 multipart 解析依赖。

宿主 HTTP API

注册在 ctx.webServer(kind: 'prefix',path: '/notebook/api'),JSON over HTTP,所有响应带 Cache-Control: no-store。

方法 路径 请求 响应
GET /notebook/api/state — { doc, degraded, degradedReason? }
POST /notebook/api/notes { title, body, attachments: [{ name, mime, size, dataUrl }] } { note }
PATCH /notebook/api/notes/:id { title?, body?, attachments? } { note }
DELETE /notebook/api/notes/:id — { ok: true }
PATCH /notebook/api/prefs Partial<NotebookPrefs> { prefs }
GET /notebook/api/attachments/:noteId/:file — 图片字节 + Content-Type
GET /notebook/api/health — { ok, version, degraded }
  • 安全栅栏:只接受 loopback 请求(req.socket.remoteAddress ∈ 127.0.0.1/::1),并校验 Origin/Host 属于允许的本地来源,否则 403;路径参数做防穿越校验;单请求 body 上限 32 MB(超限 413)。
  • 400/403/404/413/415/500 都返回结构化 { error: { code, message } }。
  • host 端二次校验视频(不信任客户端):命中即 415。

数据落盘

路径 内容
$DSH_HOME/storages/notebook.json NotebookDoc(version: 1、notes[]、prefs),原子写
$DSH_HOME/storages/notebook.json.bak 上一次成功版本;JSON 损坏时先尝试用它恢复
$DSH_HOME/storages/notebook-attachments/<noteId>/<attachmentId>.<ext> 图片字节

$DSH_HOME 优先级:显式注入 > process.env.DSH_HOME(纯空白视为未设置)> ~/.dsh。.bak 也损坏时从空文档开始,并把坏文件改名为 notebook.json.corrupt-<ts>。

与 DSH 会话联动(v1.1)

这里的接缝都只走公开 seam 且全部可选:客户端 bundle 只能 require() 平台 seed 模块——今天恰好就是 react 与 react/jsx-runtime——import 不了 DSH 的 UI 包,所以一律鸭子类型 + 逐调用守卫。

@ 引用。 插件通过 ctx.inputTriggers.registerSource 注册名为 dsh-notebook 的 @ 源(order: 20),输入 @ 时与文件、会话并列。候选按输入大小写不敏感匹配标题与正文,最新在前、最多 8 条;查询被 abort 或读取失败时返回空列表,而不是打断整个菜单。选中一行插入原子 chip,其剪贴板形式是 @[标题](dsh-notebook:<noteId>)。发送时只序列化正文(标题绝不发给模型),每个图片标记写成一行 [图片: <文件名>]:记事已删除则这段引用自然消失,真实读取失败则拦下发送并给出可见错误,而不是把 mention 悄悄降级。

自动打开。 触发条件是会话列表里当前会话变成另一个——同一会话的快照重复发布不算,激活那一刻已经是当前的会话也不算。openTab 在席位还没持有 binding 时会抛,所以按 0 / 200 / 500 / 1200 / 2500 ms 重试,会话再次变化即放弃。由于插件在会话列表到达之前就已激活,页面加载恢复出来的选中项同样算「变成当前」。

输入框附件桥:已实现、有单测、但没有接线。 点标题必须只复制、绝不自己往输入框里写,所以它唯一的入口被移除了(产品决定,2026-09-15)。接缝才是贵的那部分且已有覆盖,因此 composer.ts 与它的单测被刻意保留,等一个属于它自己的显式动作来接。当前没有任何 UI 能触发它。

为什么「打开」本身就是「展开」(v0.2.2 的 bug,v0.2.3 的修复)。 sidebarRight.openTab(kind) 在打开的同时就展开这一列(store 计划里的第一个操作就是 planSetExpanded(state, true)),而 isExpanded() 回答的是席位在上一次已提交渲染时绑定的 surface——所以与 openTab() 处于同一个同步块里的读取落后一次 React 提交。v0.2.2 读到那个过期的 false,又用 toggleExpanded() 去「纠正」,而它翻的是实时值,于是把刚被打开揭示出来的那一列收了回去。DSH 在 ctx.sidebarRight 上没有幂等的 setExpanded(),外部没有安全的纠正手段,所以现在只调 openTab;test/auto-open.test.ts 按宿主真实语义建模,并断言最终是展开的、且 isExpanded / toggleExpanded 一次都没被调用。

从对话里捕获(v0.2.0)

会话有两条路可以往记事本里写,二者都长在 shell 里而不是 Notebook tab 里(所以 tab 开没开都会提供),也都默认开启。它们共用同一条写入路径——client/capture.ts——所以标题编号、保存顺序与 toast 不会各走各的。

selection ──► selectionAction.ts ─┐
                                  ├──► capture.ts ──► POST /notebook/api/notes ──► NotebookDoc.notes
assistant answer ──► answerAction.ts ─┘        (标题编号 · 串行写入 · 订阅者 · toast)
选中文字 → 记事本 回答 → 记事本
入口 选区旁的浮动小条(shell.overlay) 已定稿消息动作行末尾多出来的一个图标
偏好 selectionToNotebook messageToNotebook
标题 编号默认名 未命名n——取没有任何既有标题占用的最小 n,所以删掉 未命名2 之后下一次捕获会补上这个号 该会话自己的标题;会话还没有标题时用编号默认名,而不是一个用户从没选过的占位标题
正文 选区逐字原文(不 trim) 回答里每个 text 块按顺序拼接、中间空一行;reasoning、tool-call、image 块一律略去
拒收 空或纯空白的选区;落在输入框、任何可编辑控件、或本插件面板内的选区;会话之外的选区 读不到快照时不提供;行上没有可用 message id 时也隐藏(被打断的回答本来就不带 id)

这个动作落在行里的哪一格。 DSH 的槽位渲染在消息行的扩展带里,也就是硬编码的复制与分支按钮之间。order: 20 让本插件排在官方那对好评按钮之后,所以这个图标是这个槽位能表达的最后一项——它不可能排到「分支」之后。

编号归捕获服务所有。 一个标题由「宿主当前持有的记事」加上「这次激活已经铸出的标题」共同决定,并一直占着直到那次请求落定:即便记事列表读不到,连续捕获也不会撞号;而保存失败会释放编号,重试仍铸同一个。保存是串行的,所以这个读改写不会交错。

选区范围,如实说。 DSH 没有暴露选区服务,所以这个功能自己盯着 document,并从 shell 的语义化 DOM 钩子([data-chat-flow]、[data-conversation-scroll],再到 [data-slot=…])判断什么算「在会话里」。将来的 shell 若改掉这些名字,该功能会降级为「输入框之外、也不在我们自己面板里的任意选区」,而不是静默地永不触发。

已知限制

以下为 v1 / v1.1 / v0.2.0–v0.3.0 有意不做的部分。

  • 不支持视频 / 音频等富媒体——需求明确排除项,client 与 host 双侧都拒绝。
  • 不做多用户、云同步、分享、实时协同、AI 自动整理,也没有版本历史(只保留最近一次内容)。
  • 笔记全局共享,不按会话隔离。
  • 正文框最高只到视口高度的 60%:更长的记事在框内滚动。不封顶就会把标题和「完成 / 取消」挤出面板,代价是编辑超长记事时无法一次看全。手调高度随拖拽把手一起移除,没有做成按记事保存的偏好。
  • 正文是纯文本 + Markdown 图片标记(![name](attachment:<id>)),不是富文本。
  • 引用只送正文,不送标题——@[标题](…) 里的标题是给人看的标签,序列化时被有意丢弃。
  • 未发送的 @ 引用在页面刷新后会退化成字面 mention:DSH 把未发送的草稿按剪贴板投影存进 localStorage,而本插件没有 host 半侧的 mention 解析器,所以模型收到的是字面文本 @[标题](dsh-notebook:<noteId>)。规避:发送前别刷新,或刷新后重新插入引用。在 agent/pre-step 接同一条 seam 做展开是已确定的修法,v1.1 有意不做。
  • SVG 走不了输入框附件桥(只收 png/jpeg/webp/gif)。记事本身仍支持 SVG,而这条桥当前也没有 UI 入口。
  • 右侧栏按会话各存一份界面:新建的会话一开始没有任何 tab,所以 Notebook 得从 + 列表里重新加一次——要么就把 autoOpenOnNewSession 打开。
  • 屏幕上没有会话时「打开」是个空操作:openTab 在那儿(hero 屏)会抛,插件把它吞掉,于是启动时或新会话时的那次揭示根本不会发生。极窄的窗口上 DSH 还会在放进 tab 后强制收起右栏——这是宿主的空间规则,本插件有意不与它较劲。
  • 回答图标排不到「分支」之后:槽位渲染在扩展带里,order: 20 已经是它所能表达的最后一位。
  • 回答是按正文存的,不是按对话记录存的:只有 text 块会被存下来,回答里渲染出来的图片也不会复制进记事。
  • 选区浮动条跟着 shell 的 DOM 钩子走:将来的 shell 改掉这些名字只会让范围降级而不是坏掉;但对于浏览器已经报不出几何信息的陈旧选区,它无法提供这个动作。
  • 捕获到的记事是立即写入的,没有确认步骤:这正是这两个动作的意义,但也意味着一次误点就会落一条记事,用户随后得手工删掉。
  • v1.1 的会话联动、v0.2.0 的两个捕获入口与 v0.3.0 的侧栏宿主都没有人工点过 GUI:契约是读已发布的 0.2.0-rc.2 包对齐出来的,并由单元 / 组件测试钉住(见〈验证状态〉)。
  • 不修改 DSH 源码(硬约束)。

FAQ

我贴了 mp4,为什么没反应? 不支持视频。客户端与 host 双侧都会拒收,并提示「不支持视频文件」。

为什么点标题复制出来的正文里图片变成了 [图片: 文件名]? 剪贴板里写的是纯文本,而图片是磁盘上的文件。把设置里的 copyImagesAsName 关掉,图片标记就会整段略去。

点标题会把记事里的图片送进输入框吗? 不会。附件桥已实现但未接线,当前没有任何 UI 能触发它:点标题永远只把正文复制到剪贴板。

@ 引用发出去的是什么? 只有正文——chip 上显示标题,但标题不会交给模型。被引用的记事如果已删除,这段引用什么都不贡献;读取真失败时发送会被拦下并报错。

为什么刷新页面后,之前插的 @ 引用变成了 @[标题](dsh-notebook:xxx)? DSH 把未发送的草稿按剪贴板投影存进 localStorage,而本插件没有 host 半侧的 mention 解析器,所以 chip 退化成字面文本。发送前别刷新,或刷新后重新插入引用。

笔记存在哪里?会上传吗? 全部在本机 $DSH_HOME/storages/ 下。没有任何远端上传,HTTP API 只监听 loopback。

我在会话里选了文字,为什么没有出现「进记事本」按钮? 有四种刻意的拒收:选区是空的或纯空白;选区落在输入框或任何可编辑控件里(提示词草稿不是记事);选区落在记事本面板自身内;浏览器报不出它的几何信息。只要 shell 暴露了自己的对话记录容器,会话之外的选区同样不提供。若始终不出现,也可能是被关掉了:到设置里看「选中文字可存入记事本」。

回答里的哪一部分会被存下来?标题从哪来? 存的是正文:每个 text 块按顺序拼接、中间空一行(整条回答只有工具调用时会如实说明,而不是写一条空记事)。标题取该会话自己的标题,会话还没有标题时用编号默认名。存下来之后它和别的记事一样。

图片会不会丢? 不会静默丢。上传失败的条目会保留为错误项并可重试;$DSH_HOME 不可写时降级为内存态,每个响应带 degraded: true,界面顶部显示非阻断提示。

还要另外装侧栏插件吗? 不用。页面渲染在 DSH 自带的右侧栏里,>=0.2.0-rc.2 就是唯一前提。插件过去对接的那些第三方载体都已删除,其中一个在 0.2 上根本加载不了。

插件行显示为已禁用、什么都不出现,先查什么? 先查 bundle 到底加载了没有。dsh 0.2.0-rc.2 只要 package.json 里有一条 @deepseek-ai/dsh* 的 peer 范围过不了 semver.satisfies(runtimeVersion, range, { includePrerelease: true }),就会跳过整个 bundle,日志里是 dsh: skipping profile bundle "dsh-notebook": … incompatible …。rc.2 不校验 dsh.plugin.json 的 engines.dsh,所以 peer 范围是唯一的闸门。装上与你版本线匹配的那一版,或用 dsh plugin allow-version 给这一对开豁免(对应的 disallow 命令可撤销)——见〈插件根本不加载时〉。

pnpm install 报 ERR_PNPM_NO_MATCHING_VERSION: @deepseek-ai/dsh-*? 那是 pnpm 在自动安装 peer。本仓库根的 pnpm-workspace.yaml 已设 autoInstallPeers: false(pnpm 11 从此处读项目配置);若你在别处复刻包配置,加上同样的设置即可。

开发

pnpm install        # 只用 pnpm,npm 在本仓库不受支持
pnpm typecheck      # tsc --noEmit
pnpm test           # vitest run(host 用 node 环境,*.test.tsx 用 jsdom)
pnpm build          # tsc -p tsconfig.build.json && tsdown → lib/index.js + lib/client.js + lib/types/**
pnpm watch          # tsdown --watch

pnpm-workspace.yaml 只有一条配置 autoInstallPeers: false。原因:@deepseek-ai/* 这些 peer 由 DSH 宿主通过 profile 的模块表提供,插件绝不能自带一份,可 pnpm 仍然会去解析它们——而这些包只发布在预发布线上(0.2.0-rc.2),普通的 ^0.2.0 范围永远匹配不上,安装会以 ERR_PNPM_NO_MATCHING_VERSION 失败。本仓库真正需要构建的包全部是显式 devDependencies(已精简到源码真正 import 的那些),关掉 peer 自动安装不影响任何东西。

客户端产物不是普通 ESM,而是注册到全局模块加载器的 CJS 闭包工厂:

window.__ModuleLoader__.load({ id: "dsh-notebook", factory: (require) => {
  var module = { exports: {} }; var exports = module.exports;
  /* …打包后的 CJS 代码… */
  exports.apply = apply; exports.inject = inject;
  return module.exports;
} });

因此 src/client/** 里禁止 node:* 导入,也禁止任何 @deepseek-ai/* 的值导入:工厂里的 require 只回答平台 seed 模块,而今天整个 bundle 只 require 了 react 和 react/jsx-runtime。这就是每个图标都是内联 SVG、每个 DSH 服务都从 context 取而不是 import 的原因。codeSplitting: false 也是必须的(那个 require 无法解析相对 chunk URL)。

验证状态

项 方式 结果
tsc --noEmit 全仓 0 错误
单元 / 组件测试 vitest run 311 passed (19 files)
构建 tsc -p tsconfig.build.json && tsdown lib/index.js(host,ESM)+ lib/client.js(client,CJS 闭包工厂)+ map + lib/types/**
客户端 bundle 形态 CI 里用 stub require 实际执行 lib/client.js id=dsh-notebook、inject===['slots','locale']、只 require react 与 react/jsx-runtime、零 node: require
官方右侧栏宿主 test/native-sidebar-host.test.ts(16)用一个会强制 keyed-slot 规则的假 ctx 跑完整次激活 本体以类型的 id 为 key、guide 条目自带稳定 id、揭示手势只有 openTab、服务迟到时只 ctx.inject 等一次、卸载时每条注册都被拆掉、没有右侧栏时保持失效并留下日志
隐藏的 tab test/native-tab-body.test.tsx(3) 可见性经注入的 useTabInfo() 钩子读取、隐藏期间不碰 host、旧版纯 prop 形态依旧生效
持久化 (host 半侧,自 v0.2.x 起未变) 重启 + 换安装通道后复测 notebook.json、.bak、附件目录落盘正确,换成 release tarball 安装后数据仍在
发布产物 用 release tarball 装进干净 profile dsh plugin 挂载成功,host 路由与客户端 bundle 均正常
CI GitHub Actions,Node 22 本地验收门槛的四步,外加产物形态断言
host 路由在线 GET /notebook/api/state、…/attachments/<noteId>/<file> state → 200 application/json 返回真实文档;附件 → 200 image/png

持久化与发布产物这两行是 0.3 之前留下的证据;host 半侧此后没有改动,但 0.3.0 没有重跑它们。

浏览器里做的事全都没有人工点过 GUI:v0.3.0 的侧栏宿主、v1.1 的用户可见功能(@ 引用、新会话自动打开)与 v0.2.0 的两个捕获入口都是如此。契约是读已发布的 0.2.0-rc.2 包对齐出来的(注册序列记录在 src/client/hosts/native.ts 文件头),而实机走一遍需要重新构建 lib/client.js 再由刷新后的页面加载,这是用户侧的步骤,不是本仓库测试套件能断言的。相对地,v0.2.1 的正文缩放在真浏览器里确认过,不过是在那个此后被删掉的外壳里:空编辑器 120px、60 行正好顶到 round(929 × 0.6) = 557px 上限并改为框内滚动、删回 1 行回到 120px、只改视口高度时上限重新钳制、一次无关的 React 重渲染零次 style 写入。规则本身由 test/auto-grow.test.ts 与 test/editor-autogrow.test.tsx 钉住。

功能 测试 断言到哪一步
输入框附件桥(未接线,无 UI 入口) composer.test.ts(31);view-actions.test.tsx(7)断言的是相反的行为 目标解析、落盘图片读回成 File、createDrafts + addAttachments 调用序列、拒收时释放草稿、SVG 跳过、张数上限、三条文本写入路径、不谎报成功。UI 级别:点标题只复制并把输入框完全放在一边
@ 引用 + 「对话引用」 reference.test.ts(17)+ view-actions.test.tsx(7) 只注册一次且可 dispose、注册被拒时按预算重试、过滤 / 排序 / 8 条上限、chip 与规范 mention、序列化只含正文不含标题、已删除的记事序列化为空、真实读取失败向上抛、refUnavailable
新会话自动打开 auto-open.test.ts(16)+ native-sidebar-host.test.ts(16)+ native-tab-body.test.tsx(3) 激活时已存在的会话不触发、会为页面加载恢复出来的会话打开、同一会话的快照不重复触发、席位无 binding 时按 0 / 200 / 500 / 1200 / 2500 ms 重试,以及手势只有 openTab——最终展开、isExpanded 一次未读、toggleExpanded 一次未调
捕获:选中 → 记事本 selection-action.test.tsx(35)+ capture.test.ts(21)+ capture-surfaces.test.tsx(19) 经真实 DOM 钩子做范围判定、各种拒收、四边位置钳制、点击时不能先取消选区、正文是逐字原文、一次点击只产生一条记事、偏好即时生效
捕获:回答 → 记事本 answer-action.test.ts(18)+ capture-surfaces.test.tsx(19)+ native-sidebar-host.test.ts(16) 两种快照形态与版本错配形态、无 id 回答绝不被空 id 匹配、只拼 text 块、会话标题与其兜底、空回答如实提示、order > 10
标题编号 + 写入顺序 capture.test.ts(21) 取最小空闲序号、连续捕获不复用、按调用顺序串行化、保存失败释放编号、纯空白输入 no-op
正文自动缩放 auto-grow.test.ts(12)+ editor-autogrow.test.tsx(8) 上限与兜底、让「能缩回去」成为可能的 height: auto 复位、120px 下限、超上限切 overflowY: auto、幂等,以及驱动真实 <textarea> 的变高 / 缩回 / 重测
确认框与语言 editor.test.tsx(15)+ locales.test.ts(10)+ capture-surfaces.test.tsx(19) 面板自己的 alertdialog、三种「不删」、关闭确认时直接删、连点两下只发一次请求,以及 window.confirm 从未被调用;语言读取顺序与全部兜底
偏好的 HTTP 往返 api.test.ts(12) 规范化、缺失的偏好回落到开启而不是被丢掉、id 百分号编码、错误信封

另有一条防漂移守卫:test/routes.test.ts 的「accepts EVERY preference key the plugin exposes」断言 PATCH /notebook/api/prefs 接受的 key 集合等于 Object.keys(DEFAULT_PREFS)——正是它抓出了 /prefs 静默丢掉 autoOpenOnNewSession 的真实 bug。

参与贡献

欢迎提 issue 和 PR。先说两条硬规矩:

  • 只用 pnpm。 本仓库不支持 npm / yarn;由其它包管理器造成的 lockfile 不一致,我们无法处理。
  • 绝不修改 DSH 源码。 这是项目的硬约束,不是个人偏好。

本地验收门槛——CI 在 Node 22 上跑的正是这四条(pnpm 11 自身需要 Node ≥ 22.13,而插件本身仍运行在 engines 声明的 Node ≥ 20),外加用 stub require 实际执行 lib/client.js 断言产物形态:

pnpm install && pnpm typecheck && pnpm test && pnpm build

报 bug——请用 bug report 模板。真正决定处理速度的字段是 dsh-notebook 版本、DSH 版本,以及 bundle 到底加载了没有——日志里有没有那行 dsh: skipping profile bundle,比任何代码细节都更早给出答案。

提 PR——清单在 PR 模板 里。要点:贴上真实的 typecheck / test / build 输出;说明改动触及哪些部分(侧栏宿主、两个捕获入口、@ 引用、host 半侧,或仅构建 / CI / 文档);用户可见行为或设置项有变化时,两份 README 都要更新。

PR 不能破坏的约束:

  • 不新增视频 / 音频支持路径——v1 明确排除富媒体,双侧都在拦截;
  • cordis(裸包名)绝不能进 dependencies / peerDependencies / optionalDependencies;
  • 不新增 preinstall / install / postinstall / prepare 脚本;
  • src/client/** 里不得导入 node:*,也不得新增任何 @deepseek-ai/* 的值导入——bundle 的 require 只回答平台 seed 模块,今天就是 react 与 react/jsx-runtime;
  • 客户端 bundle 注册的 id 必须始终等于包名(dsh-notebook)。

版本历史

里程碑 状态 内容
v0.1.0 已打 tag 记事本本体:新建 / 编辑 / 删除、标题 + 正文、图片(拒视频)、点标题复制正文、唯一的可复用编辑容器、三层 tier 自适应(v0.3.0 已删除)、原子写与 .bak 恢复、仅监听 loopback 的 HTTP API。
v1.1 已并入 main,尚未打 tag @ 引用与行内「对话引用」按钮、新会话自动打开。输入框附件桥以代码 + 单测形式落地,但没有任何 UI 入口。
v0.2.0 已发布 会话 → 记事本捕获:选区上的浮动「进记事本」动作、每条回答末尾的「存入记事本」图标;两者共用同一条串行写入路径,都可开关。
v0.2.1 已发布 正文框随内容自动缩放,下限 120px、上限视口高度的 60%;手动拖拽把手移除。
v0.2.2 已发布 两个确认框搬进面板内部(代码里不再有任何 window.confirm),所有文案跟随 shell 语言。
v0.2.3 已发布 「新会话自动打开记事本」真的会打开侧边栏——把刚打开的列收回去的「先读后翻」已删除;偏好还在加载时变成当前的会话也不再被丢掉。
v0.3.0 本次发布 迁到 DSH 的官方右侧栏上,并且只有它:tab 分两阶段注册(本体按 tab 类型的 id 作为 key 进 keyed 槽,再登记类型与它的 guide 条目)、dsh-better-sidebar 与自绘两层连同各自的宿主模块一起删除、openOnStart 改由官方侧栏兑现、peer 范围升到 ^0.2.0-rc.2——真正一直阻止 bundle 加载的就是它。devDependencies 精简到源码实际 import 的包。

package.json 与 dsh.plugin.json 声明的是 0.3.0;v1.1 的改动作为 v0.2.0 的一部分一起发布,没有单独打 tag。

许可

MIT © 2026 wyzh0117

内容来自项目 README(GitHub)↗

评论

评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。