READMESource: main@549248ee
便签墙 · dsh-plugin-sticky-wall
DSH 侧栏里的一面便签墙:随手贴、随手拖、随手改,并且模型能直接往上贴。
不是待办清单——待办有 todo_write;便签墙放的是那些「先记一下」的碎片,
要清单也能就地长成清单。






功能
- 侧栏一个图标,主面板一面墙:注册
sidebar.panellist(id =sticky-wall)+main同名 key, 点图标切到主面板,和 QQ 机器人面板同一套机制。 - 一行一种格式:正文 / ☐ 待办 / • 无序 / 1. 有序,四种混排。
待办点方框即「已完成」(打勾 + 删除线),抬头显示
1/3进度。 行首敲「- 」「1. 」「- [ ] 」直接变成对应格式;悬停纸条底部还有四个格式按钮, 作用在「当前这一行」上,点一下就能切换,再点「≡」回到普通正文。 - 横线纸可选:抬头那个「带横线的纸」图标,点一下在横线纸 / 空白纸之间切换,逐张便签独立记。 横线按 14px/22px 的行距画在每行下沿,字正好落在线上;纸高按整行增长,永远不会切半行。
- 纸的质感:8 种纸色(亮纸 + 深墨)+ 细颗粒噪声 + 左上柔光 + 下沿压暗 + 8px 圆角(纸面内容按圆角裁切)+ 内外两层阴影。不做倾斜——旋转层里的文字会丢次像素抗锯齿, 看着发虚,所以纸是摆正的。
- 可调大小:右下角的斜线手柄(悬停出现)拖一下就能改大小——宽 196–560px, 高按 22px 的整行增减,双击手柄回默认;窄到 240px 以下时底部调色盘自动收起,把位置让给行格式按钮。
- 一键折叠:抬头最左边那个「∨」把纸条收成两行(底部渐隐 + 一个「展开」小胶囊),点正文或再点一次「∧」展开。 折叠只截高度、不动宽度——宽度是你拖出来的,不该被自动改掉。折叠状态跟着便签存。 折叠时正文是只读的,但可以直接选词复制:拖选不会把纸展开(只有不带选区的点击才展开)。
- 回收站:删除(胶头 🗑、清空墙面、模型工具都算)只是把便签移进右侧回收站,不会立刻销毁;
工具栏右边的「回收站」(带数量)点开是抽屉:逐张「恢复」或「彻底删除」,也可以一次清空回收站。
抽屉默认收起,图钉可固定(固定后点别处不会自动收起,状态记在本地);删除后 toast 里还有「撤销」直达;
Esc也能关抽屉(正在写便签时 Esc 优先给输入框)。 恢复时如果原来那块地方已经被别的纸占了(重叠超过自身面积 20%),会自动挪到第一个空格子并提示。 - 随手摆放:按住纸头(含六点抓手的那条)拖到任意位置,落点按百分比存, 面板宽窄变了也不会散架。双击空白处即在点击处新建一张。
- 就地编辑:每行都是一个输入框,点进去直接改;输入 0.65s 后自动落盘,
失焦 /
⌘/Ctrl+Enter/Esc都会立即保存。回车拆行、行首退格并回上一行、↑↓ 跨行移动。 - 纸色 / 置顶 / 删除:悬停纸条出现调色圆点;置顶的纸永远在最上层(顶部一道墨色压边)。
- 搜索 / 只看置顶 / 整理 / 清空:搜索按正文过滤;「整理」是流式重排(见下); 清空需要点两次。
- 整理 = 对齐,不是重排:按你摆的位置把便签聚成行(y 差 56px 内算同一行), 行内保持你的左右顺序、统一左对齐、间距 16px;行与行按「这一行最高 + 16px」往下排, 左边距 18px。不会因为某张纸大就把它排到后面去——宽纸还待在它那一行里。 一行真的比面板宽时,先把行内间距压到 8px,仍放不下才把这一行末尾的几张挪到下一行。
- 跨会话持久化:
$DSH_HOME/sticky-wall.json(默认~/.dsh/sticky-wall.json),原子写。 - 和模型联动:4 个工具,说一句「把这条记到便签墙」就会真的贴上;
模型贴来的 Markdown(
- [ ]/-/1.)会自动解析成复选框和列表; 模型贴的便签 6 秒内自动出现在墙面上(面板可见时轮询)。
交互一览
| 操作 | 效果 |
|---|---|
| 点侧栏「便签墙」图标 | 切到便签墙主面板 |
| 点「新建便签」 | 新建并聚焦第一行 |
| 双击墙面空白处 | 在该处新建一张 |
| 按住纸头拖动 | 移动便签(松手保存) |
| 回车 | 在当前行下方拆出一行(同格式) |
| 行首退格 | 列表/待办行先退回正文;空行则并到上一行 |
| 敲「- 」「1. 」「- [ ] 」 | 当前行变成无序 / 有序 / 待办 |
| 点待办方框 | 打勾(已完成,带删除线) |
| 悬停 → 底部 ≡ ☐ • 1. | 把当前行切成 正文 / 待办 / 无序 / 有序 |
| 悬停 → 右下角斜线手柄 | 拖动改大小(宽 196–560px、高按整行);双击回默认 |
| 悬停 → 抬头 ∨ / ∧ | 折叠成两行 / 展开(点正文也能展开) |
| 悬停 → 抬头方格图标 | 横线纸开 / 关 |
| 悬停 → 调色圆点 | 换纸色 |
| 悬停 → 📌 / 🗑 | 置顶 / 移进回收站(可撤销) |
| 工具栏「回收站」(带数量) | 打开右侧抽屉(图钉可固定,状态记在本地) |
| 回收站 → 恢复 / 彻底删除 | 放回墙面(最上层)/ 真正销毁 |
| 墙面右下 ↑ / ↓ | 一键到最上 / 最下(只有内容超出一屏时才出现,到头的那个变灰) |
| 「只看置顶」筛选 | 只看置顶便签(和抬头那个「置顶」不是一回事) |
| 「整理」 / 「清空」 | 对齐排布 / 连点两次清空(清空也进回收站) |
| 页脚右侧的路径 | 数据文件位置,点一下复制路径 |
模型工具
| 工具 | 用途 |
|---|---|
sticky_wall_list |
读墙上的便签(id、纸色、时间、正文;清单会带 - [ ] / 1. 标记),可按 pinned/query 过滤;回收站里的不列 |
sticky_wall_add |
贴一张新便签(text 必填,支持 Markdown 清单,color、pinned 可选) |
sticky_wall_update |
改正文 / 纸色 / 置顶(正文会重新解析成行,勾选状态会重置;回收站里的便签要先恢复) |
sticky_wall_remove |
移到回收站(软删除,可用 sticky_wall_restore 恢复) |
sticky_wall_restore |
把回收站里的便签恢复回墙面(恢复后在最上层) |
返回文本里带 [n-xxxxxx] 这样的短 id,后续 update/remove 用它。
数据与隐私
- 全部数据只存在本机(默认
~/.dsh/sticky-wall.json,路径可配置,见下),不上传、不联网。 - 一张便签 =
lines[](每行kind/text/done)+ 纸色/位置/置顶/横线开关 + 尺寸(w、minLines)- 折叠状态(
collapsed)+ 回收站标记(trashed、deletedAt);text是从行派生的可读副本,给搜索和工具用——所以老数据(只有text)能直接读, 下次保存时自动升级成行结构。
- 折叠状态(
- 回收站是同一份数组里的软删除(打标记),所以「恢复」是无损的:位置、尺寸、纸色、勾选状态全都还在。 容量 500 张 / 30 天:超期在加载时清掉;超过 500 张则在删除那一刻挤掉最旧的一张并打日志 ——「丢」只发生在看得见的删除动作上,读盘时绝不裁剪(老版本会按 100 截断,清空后重启会静默丢便签)。
- 上限:500 张便签、每张 200 行 / 每行 1000 字 / 合计 4000 字;超出直接拒绝而不是悄悄截断。 回收站里的不占墙面名额(各算各的 500)。
- 首次加载会在文件缺失时放两张欢迎便签,之后不再写入任何默认内容。
配置:把数据放到别处
默认落在 $DSH_HOME/sticky-wall.json(一般就是 ~/.dsh/sticky-wall.json)。
想换位置就改插件配置里的 storePath,然后重启 DSH:
# cordis.patch.yml
- insert:
- id: sticky-wall
name: dsh-plugin-sticky-wall
config:
storePath: D:/notes/sticky-wall.json # 绝对路径、~/notes/...、或相对 $DSH_HOME 的路径都行
- 换路径时如果新文件还不存在、而默认文件在,会先把默认文件复制过去,不会让老便签凭空消失。
- 当前实际用的路径显示在面板页脚右侧(
…/.dsh/sticky-wall.json),点一下复制;改过配置会高亮成主题色。 - 这个配置只在宿主侧生效,改完需要重启 DSH(和其它宿主改动一样)。
结构与实现要点
dsh-plugin-sticky-wall/
├── package.json # dsh.bundle.patch + dsh.client.platform=web + exports["./client"]
├── cordis.patch.yml # 插入一行:id=sticky-wall, name=dsh-plugin-sticky-wall
├── index.js # Host 半侧:行模型 / 存储 / HTTP 端点 / 5 个模型工具(零第三方依赖)
├── client.js # Client 半侧:ModuleLoader bundle,两处 slot 注册 + 整面墙的 UI
└── docs/ # 截图
- 双面插件:Host 起
webServer前缀路由/api/dsh-sticky-wall/*,Client 同源fetch读写, 不碰 typert/remote,也就没有版本适配问题。 - 端点:
/snapshot、/upsert、/remove(软删除)、/restore、/purge、/purge-all、/clear(也是软删除)。 除了 upsert 的 saved/skipped/full,回包统一带{ notes, trash, trashCount, colors, store },客户端一份合并逻辑就能覆盖所有动作。 - 两条瘦身通道(回收站和墙都可能很大,别每次全量搬):
/snapshot?trash=0只回trashCount,明细留空。客户端轮询用它,条数变了才补拉一次明细(抽屉打开时也会补)。/upsert带light: true时不回整面墙(保存回包实测 1.1MB → 几 KB)。客户端保存只等 ack,本地就是权威。
- 写盘:全量 JSON 原子写(tmp + rename),但内容没变就跳过;写失败会作废「已落盘」标记,下次同样的内容还会再写。
- 同源校验:每个请求都过一道
Sec-Fetch-Site/Origin检查,跨站直接 403;POST 只接受application/json(跨站的「简单请求」只能用 text/plain,这一条把它们全挡掉)。这样本机别的页面就没法偷偷改/清你的便签了 —— 顺手也挡住了 CSRF 那类玩法(实测:本机 4398 端口的页面写入被拒,数据 0 变化)。 - 层级:便签 z-index ≤ 40000(
100 + z,置顶 +20000)→ 滚动按钮 43000 → 抽屉遮罩 44000 → 抽屉 44100 → toast 50000。 抽屉和遮罩都挂在新加的.sw-main(工具栏以下那一块)里,所以既盖得住便签,又不会糊住工具栏和页脚。 - 未知字段原样保留:
normalizeNote只收敛已知字段,其余键(跳过__proto__之类)原样写回。 这样以后客户端只加 UI 字段时,不用再改宿主、也不用重启。 - 折叠:
collapsed布尔值存进便签;CSS 用max-height: calc(lh*2 + 上下内边距)截断 +mask-image渐隐, 正文设pointer-events: none让点击落到 body 上「点一下展开」;底部菜单同时display: none。 - 回收站抽屉的「固定/收起」偏好存在
localStorage(dsh-sticky-wall:trash),不占便签数据。 client.js是手写的 ModuleLoader bundle(window.__ModuleLoader__.load({ id, factory })), 只require('react')——平台种子表提供,不需要构建步骤,改完刷新页面即生效。- 渲染性能:每张便签
React.memo,只收note+focusId两个 prop,不订阅 store; 打字时只有正在编辑的那一张重渲染。实测 200 张便签下从 42.7ms/键降到 6.7ms/键(20 张 4.5ms/键)。 - 行模型:
lines: [{ id, kind, text, done }],kind ∈ text|todo|ul|ol。 有序列表按「连续 ol 段」在前端重新编号,所以增删行不用重写编号;textFromLines()/linesFromText()在两端各有一份同构实现,保证工具写入与老数据解析一致。 - 横线是 background 画的,不是额外元素:
.sw-body用linear-gradient画 1px、background-size: 100% 22px,background-position-y = 行距 + 上内边距,所以第一条横线正好是第一行的下沿; 每个行盒高度都吸附到 22px 的整数倍,横线不会被切半。改行距只需同步LINE_H(JS)与--sw-lh(CSS),NOTE_W决定网格列数。 - 写入的并发安全:每次本地改动会给该便签打一个自增 revision;保存回包返回时 只有 revision 没变才清 dirty,否则保留本地版本(避免「勾选的同时旧保存回包把状态盖回去」)。
- 尺寸:
w(px,196–560)与minLines(1–30,按 22px 行高换算)。 拖手柄时只改元素的内联宽高与--sw-min-lines(实时预览),松手才写回 store; 高度用min-height表达,所以便签永远「至少这么高、内容多了自己长」。 - 窄便签自适应:
.sw-note开了container-type: inline-size, 容器查询在 ≤240px 时收起调色盘;纸用overflow: hidden+ 8px 圆角,横线会被圆角自然裁掉。 - 整理的量尺:直接读 DOM(
offsetWidth/offsetHeight)拿每张纸的真实尺寸, 被搜索过滤掉、量不到的才用行数 × 22 + 头尾估算;先按现有 y 聚行、行内按 x 排序, 再统一落位。整定是幂等的:连点两次结果一样(第一次排完就已经是对齐状态)。 排完若内容高于画布,把画布min-height撑到内容高 / 0.84, 既让点阵随之延长,也保证最后一行换算成百分比后不超过宿主侧 88% 的 y 上限。 - 图标:全部是 24 网格的 path,不用文字字形(
≡ ☐ • 1.那种会受字体影响、基线与重量都不齐)。 描边按尺寸取:≤20px 用 2(12px 时正好 1px 实心线),更大的用 1.6;圆头圆角。 行格式四个按钮是 18px 图标配 22px 按钮,抬头三个小图标 13px 配 20px 按钮; 小尺寸的图形都做了减法(垃圾桶去掉内部竖线、横线纸只画两条线、抓手点放大到 1.5), 免得 12–13px 时糊成一团。 - 纸面质感全在 CSS:SVG
feTurbulence噪声(data URI)+ 柔光 + 下沿压暗 + 内外阴影, 文字不做变形,保证 1x 屏上也是清楚的。 - 颜色一律走 DSH 主题令牌(
--dsw-alias-*),纸色是固定的亮纸深墨, 所以浅色/深色主题下都像「桌上的纸」而不是「界面里的卡片」;横线与复选框描边是color-mix(in srgb, currentColor 16~58%, transparent),跟着纸的墨色走。 - 样式通过
data-plugin+data-plugin-css标记注入;热替换时若旧标签还在,installStyles()会比较内容并原地改写,避免新样式被幂等判断挡掉。
安装 / 卸载
# 装(profile 里以 file: 依赖链接本目录)
dsh plugin add D:/workspace/dsh/dsh-plugin-sticky-wall
# 或直接让 agent 调 plugin_manager install_bundle
# 卸载
dsh plugin remove dsh-plugin-sticky-wall
已知限制
- 便签是自由摆放的:窗口变得很窄时可能出现重叠,用「整理」重排即可。
- 高度只有「最少几行」这一个维度:内容比它高时纸会自己长,所以拖高只会变高、不会把内容裁掉。
- 列表只有一层,不支持嵌套;每行一个格式,不混排(正文行里不能再嵌入列表)。
- 轮询间隔 6 秒,多窗口同开时另一边的改动最多滞后一个周期。
- 工具改正文会重建行,勾选状态会丢;要保留勾选请只在 UI 里改。
- 删除有回收站兜底(30 天 / 500 张),但「彻底删除」和「清空回收站」不可撤销。
- 写盘是整文件重写:实测 50 张(94KB)一次保存往返 3.4ms,顶到上限 500 张 + 100 张回收站(1.1MB)是 11.4ms ——够用,但再往上(几千张)就该换成增量格式或 SQLite 了。这也是「单文件 + 全量写」这个设计的真实上限。
- 不做富文本/附件——便签就该是纯文本。
- 鉴权沿用 DSH 自身的本地回环边界:接口做了同源校验(跨站 403、非 JSON 415), 但同一台机器上的进程(脚本、其它本地程序)仍然能直接读写那个 JSON 文件——这是本地应用的固有边界。
No comments yet. Be the first to write one.