# dsh-notebook
**DSH 侧边栏里的记事本。**
`+` 新建 → 写标题与正文 → 贴图 → 点**完成**,条目以标题陈列。
点标题复制正文 · 打 `@` 引用记事 · 点**编辑**复用同一个容器。
[](https://github.com/wyzh0117/dsh-notebook/actions/workflows/ci.yml)
[](./LICENSE)
[](https://nodejs.org)
[](#兼容性)
[](./test)
`dsh-plugin` · `deepseek-harness` · `notebook` · `notes` · `sidebar`
[English](./README.md) · **中文**
**本次更新**
- **v0.2.0** —— 会话也能往记事本里写:选中文字浮现「进记事本」动作;每条回答末尾的记事本图标可把整条回复存成一条记事。两个都默认开启。
- **v0.2.1** —— 正文框随内容自动缩放:写着变高、删掉变矮,最高到窗口高度的 60%。
- **v0.2.2** —— 去掉所有原生确认框(原生模态可能把嵌入式宿主卡死);所有文案跟随 shell 语言。
- **v0.2.3** —— 「新会话自动打开记事本」现在真的会打开侧边栏。
**快速跳转** · [功能](#功能) · [安装](#安装) · [使用](#使用) · [兼容性](#兼容性) · [设置项](#设置项) · [实现细节](#实现细节) · [已知限制](#已知限制) · [FAQ](#faq) · [开发](#开发)
> **GitHub topics(仓库设置里加):** `dsh-plugin` `deepseek-harness` `notebook` `notes` `sidebar`
---
## 这是什么
`dsh-notebook` 是 [DSH(DeepSeek Harness)](https://github.com/deepseek-ai/dsh) 的 Web 插件,在侧边栏里放一个**记事本**。
它只解决一件事:随手记一条带标题、正文和图片的短笔记,然后在输入框里用起来——点一下标题就把正文送进剪贴板,打 `@` 能引用某条记事。
不引入富文本编辑器、不做云同步、不做版本历史。笔记是**全局共享**的(不按会话隔离),图片以**文件形式落在宿主磁盘**上。
## 功能
| 功能 | 说明 |
|---|---|
| **`+` 新建条目** | 面板右上角的 `+`,点击后在**面板内**弹出编辑器——不新开窗口、不新开 tab |
| **标题 + 正文 + 图片** | 单行标题、随内容缩放的正文、图片缩略图区,共用一个可滚动容器 |
| **可放图片,不可放视频** | 粘贴、拖放、「插入图片」三种入口走同一条校验:`video/*` 与 `mp4/mov/webm/mkv/avi/m4v/ogv` 一律行内拒收 |
| **「完成」后以标题陈列** | 列表一条一条以**标题**为单位显示,最新在上,次要信息是「时间 · N 张图片」 |
| **点标题复制正文** | 复制的是**正文本身**、**不含标题**;图片还原成 `[图片: 文件名]` 一行,并 toast「已复制正文(N 字)」。它**永远不往输入框里写东西** |
| **`@` 引用记事** | 输入框里打 `@`,在本地化的「记事本」分组下多出记事条目(标题 + 摘要,最新在前,最多 8 条);选中插入原子 chip,发送时只有**正文**交给模型 |
| **行内「对话引用」按钮** | 作用同 `@` 选中,从列表行里直接插;够不到输入框时如实提示,不假装成功 |
| **选中文字 →「进记事本」**(v0.2.0,默认开) | 选区旁的浮动动作,把选区**原文**存成一条记事,标题是 `未命名1`、`未命名2`……输入框内与面板自身的选区有意不提供 |
| **每条回答 →「存入记事本」**(v0.2.0,默认开) | 回答动作行末尾的一个图标,一点把整条回复存成一条记事,标题用**该会话自己的标题** |
| **两个捕获功能都可开关** | 设置里的 `selectionToNotebook` / `messageToNotebook`,下一次渲染即生效,不需重新加载 |
| **新会话自动打开**(默认关闭) | 每有会话变成当前就打开 Notebook——包括页面加载恢复出来的那个;三层 tier 都生效 |
| **「编辑」复用同一个容器** | DOM 里编辑器始终只有 1 个 |
| **图片落盘** | `dataURL` 上传,host 解码写入 `$DSH_HOME/storages/notebook-attachments//`;删除记事时一并删除 |
| **原子写,不静默丢数据** | 临时文件 → `fsync` → `.bak` → `rename`,并由 mutex 串行化。`$DSH_HOME` 不可写时降级为内存态,每个响应带 `degraded: true`,界面顶部显示非阻断提示 |
| **键盘** | `Cmd/Ctrl+Enter` = 完成,`Esc` = 取消(草稿有改动时先问一句) |
更深入的设计说明见〈[实现细节](#实现细节)〉。
## 截图
> 均为真机实测截图:tier 3 那几张来自**没装任何 sidebar 产品**的环境。
| | |
|---|---|
|  |  |
| 侧边栏里的 Notebook 列表(`+` 在右上角),展开时把会话列推挤 400px | 编辑容器:标题 + 正文 + 图片缩略图(粘贴 / 拖入 / 按钮三种入口) |
|  |  |
| 点标题后 toast「已复制正文(67 字)」——复制的是正文,不含标题 | 自绘面板(tier 3):开合按钮固定在视口右上角 |
另一种形态是融入 `dsh-better-sidebar`(tier 2),见〈[兼容性](#兼容性)〉:

## 安装
| | |
|---|---|
| DSH | `>=0.1.1-rc.2` |
| Node | `>=20` |
| 包管理器 | **pnpm**——本仓库不支持 npm |
**从仓库安装:**
```sh
dsh plugin --profile web add github:wyzh0117/dsh-notebook
```
**从源码本地挂载(开发用):**
```sh
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` 里:
```jsonc
{
"dependencies": { "dsh-notebook": "link:/abs/path/to/dsh-notebook" },
"dsh": { "profile": { "bundles": [ /* … */, "dsh-notebook" ] } }
}
```
然后在 profile 目录里 `pnpm install`。`dsh.profile.bundles` 必须包含 `dsh-notebook`,否则插件不会被加载。
> **本地开发时不要重启你正在用的那个 DSH**(比如 3080 端口的 Web GUI,重启会杀掉当前会话)。需要真机验收时,另起一个隔离环境:
> ```sh
> DSH_HOME=/tmp/dshnb-home npx -y --package @deepseek-ai/dsh dsh web --port 3099
> ```
## 使用
1. 展开右侧栏,打开 **Notebook**。
2. 点右上角 **`+`** → 编辑器在面板内弹出。
3. 写**标题**和**正文**。正文框随内容缩放(最高到窗口高度的 60%,再长就在框内滚动)。配图可以**粘贴 / 拖入图片**,或点「插入图片」——视频会被拒绝;单图上限 10 MB,单条上限 20 张(都可在设置里调)。
4. 点「完成」(或 `Cmd/Ctrl+Enter`)→ 条目以**标题**陈列。
5. 点**标题文字** → 正文进剪贴板。
6. 想让模型读某条记事:输入框里打 **`@`** 选它,或点该行末尾的 **`对话引用`**。发送时只有**正文**交给模型;页面刷新后需要重新插入引用。
7. 点行尾 **`编辑`** → **同一个容器**载入该条;点 **`删除`** 会先用面板自己的对话框问一次。
8. **让会话替你写一条记事**(v0.2.0):
- **在会话里选中文字** → 点选区旁浮现的 **「进记事本」**。
- **点回答末尾的记事本图标** → 整条回复按会话标题存成一条记事。
两者都用同一个 toast 确认,也都可以在设置里关掉。
## 兼容性
| | |
|---|---|
| DSH | 最低支持 `>=0.1.1-rc.2`;开发机运行 `0.1.5-rc.2` 并装有 `@deepseek-ai/dsh-client-ui-sidebar-right`,**tier 1 是本机实际生效层** |
| Node | `>=20` |
| `dsh-better-sidebar` | 可选。tier 2 面向 0.4.0–0.18.x;0.19+ 归入 tier 1 |
| DSH 侧可选插件 | 缺 `conversation`、`inputTriggers` 或 `sessions` 时,对应的联动能力自动关闭并退回 v1 行为 |
| 旧版 shell | 每个功能都注册在可选接缝上,只会降级不会坏:槽位不存在则捕获入口根本不出现;读不到 locale 就用插件自带的 `zh` 字典;面板不依赖 `window.confirm` 或任何 dialog API |
### 版本自适应:三层 tier
DSH 的右侧栏在 `0.1.5-rc.1` 前后换了主人:以前由 `dsh-better-sidebar` 这类插件自绘,之后由 DSH 内核自己拥有、插件注册 tab。所以本插件**在客户端做一次性探测**,按优先级落到三层之一,**任何时刻页面上最多只有一个 Notebook 入口**:
| 优先级 | tier | 触发条件 | 注册方式 |
|---|---|---|---|
| 1 | **`native`** | `ctx.get('sidebarRightTabs')` 存在(DSH ≥ 0.1.5-rc.1) | `sidebarRightTabs.register({ id, kind, priority:'extension', title, guide })` + 把 tab 体注册进 keyed 槽 `sidebar.right.pane.tab` |
| 2 | **`service`** | `ctx.get('betterSidebar')` 存在(better-sidebar 0.4.0–0.18.x 及同类产品) | `ctx.betterSidebar.registerTab({ id:'dsh-notebook:notebook', single:true, settings:{…} })` |
| 3 | **`standalone`** | 二者皆无 | 自绘右侧栏,UI 对齐 `dsh-better-sidebar` 0.12.1 |
几个刻意的选择:
- **绝不**把 `betterSidebar` 写进 `export const inject`——`inject` 里缺服务会让插件永不激活,tier 3 就死了。
- 探测是「同步先行 + 异步兜底」:两个 `ctx.get` 都不命中时观察 `ctx.inject`,并挂一个 **700ms 兜底定时器**来挂载 standalone。
- **迟到升级**:standalone 已挂上之后某个服务才出现,会**先拆掉** standalone 再注册到该服务,绝不两个入口并存。
- 所有注册都包在 `ctx.effect(() => { …; return dispose })` 里,HMR / 禁用安全。
### tier 3 的自绘面板
| 项 | 规格 |
|---|---|
| 展开按钮 | 固定在视口右上角的 28×28 按钮,16px 线性图标,延迟 tooltip,`aria-label` 随展开状态切换 |
| 面板几何 | `PANEL_MIN=280` / `PANEL_MAX=640` / `PANEL_DEFAULT=400`,按视口钳制 |
| 宽度拖拽 | 面板左边缘 6px 抓取条(`setPointerCapture` + `clientX` 差值) |
| 窄屏 | `innerWidth < 768` 时合并为全宽 `100vw` 抽屉,不提供拖拽条 |
| 布局推挤 | 设置 `--dsh-notebook-width` 与 `data-dsh-notebook-collapsed` / `-dragging`,由一份命名空间化、`dispose` 时移除的 `