安装
在 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 官方右侧栏里的记事本。
+ 新建 → 写标题与正文 → 贴图 → 点完成,条目以标题陈列。
点标题复制正文 · 打 @ 引用记事 · 点编辑复用同一个容器。
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-plugindeepseek-harnessnotebooknotessidebar
这是什么
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
使用
展开右侧栏,从它的
+(新建 tab)列表里选 Notebook——那一行带着插件自己的标题、说明和图标。右侧栏按会话各存一份界面,新建的会话里它默认是空的,所以要么每次手动加,要么打开「新会话自动打开记事本」。点右上角
+→ 编辑器在面板内弹出。写标题和正文。正文框随内容缩放(最高到窗口高度的 60%,再长就在框内滚动)。配图可以粘贴 / 拖入图片,或点「插入图片」——视频会被拒绝;单图上限 10 MB,单条上限 20 张(都可在设置里调)。
点「完成」(或
Cmd/Ctrl+Enter)→ 条目以标题陈列。点标题文字 → 正文进剪贴板。
想让模型读某条记事:输入框里打
@选它,或点该行末尾的对话引用。发送时只有正文交给模型;页面刷新后需要重新插入引用。点行尾
编辑→ 同一个容器载入该条;点删除会先用面板自己的对话框问一次。让会话替你写一条记事(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 分两阶段注册,先本体后类型:
- tab 本体进 keyed 槽
sidebar.right.pane.tab,key: 'dsh-notebook'; - 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 图片标记(
),不是富文本。 - 引用只送正文,不送标题——
@[标题](…)里的标题是给人看的标签,序列化时被有意丢弃。 - 未发送的
@引用在页面刷新后会退化成字面 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
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。