ez-dsh-memo 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  中文 · [English](README.en.md)
4
4
 
5
+ ![the wall](docs/wall.png)
6
+
5
7
  A sticky-note wall for **[DeepSeek Harness](https://github.com/deepseek-ai)** — jot ideas in one keystroke, organize with categories, set reminders, link notes wiki-style, and optionally mirror everything to a [fast-note-sync](https://github.com/) Obsidian vault as Markdown.
6
8
 
7
9
  > Screenshots: coming soon.
@@ -72,6 +74,18 @@ Examples: *“把墙上关于项目 A 的便签归类到「工作」”*, *“
72
74
  | Skins | 5 accent colors | — | 9 skins |
73
75
  | AI read/write | **`memo` tool (list/add/update/remove)** | send to chat | read & write |
74
76
 
77
+ ## Wiki links between notes
78
+
79
+ Write `[[another note]]` inside a note and click it to jump there.
80
+
81
+ - Typing `[[` in a note's **edit mode** opens a suggestion list (`↑↓` choose, `Enter` insert, `Esc` cancel)
82
+ - Alias form: `[[target note|label]]`
83
+ - Rendered as a clickable link that opens the target in a **new floating card**, so you can follow a chain without losing the current one
84
+ - Unknown targets fall back to plain text (no error)
85
+ - Works in quick capture, the viewer and the inline editor
86
+
87
+ ![viewer](docs/viewer.png)
88
+
75
89
  ## Shortcuts
76
90
 
77
91
  | Keys | Action |
package/README.md CHANGED
@@ -1,203 +1,161 @@
1
- # dsh-memo — 便签墙
2
-
3
- 左栏常驻一个便签图标,点开就是一面便签墙:随手记下一闪而过的灵感,之后再回来整理。
4
-
5
- ## 有什么
6
-
7
- - **左栏入口**(`sidebar.panellist` → `main`):图标跟随侧栏选中态变色,提示文字走宿主语言(中文 / English)。
8
- - **快速记录**:顶部一个输入框,`Enter` 保存、`Shift+Enter` 换行、`⌘/Ctrl + Enter` 保存(输入法组字时不会误提交)。
9
- - **贴纸**:五种颜色(琥珀 / 玫瑰 / 天蓝 / 薄荷 / 紫罗兰),底色由「贴纸色 × 主题表面色」混出来,亮色 / 暗色主题都自适应,不写死背景色。
10
- - **每张便签**:置顶、换色、双击(或铅笔按钮)编辑、删除(行内二次确认,不弹系统对话框)。
11
- - **可改尺寸**:拖右下角把手即可自由拖宽拖高;**默认 260×150 同时也是缩小的下限**,宽度最多长到当前行右边缘(再宽会换行,所以拖到边缘就停),高度上限 900;把手双击 = 恢复默认;把手聚焦后方向键 ±16px、按住 Shift ±48px。
12
- - **拖拽排序**:**按住便签本体**(移动超过 6px 才算拖,所以点选/双击编辑/选文字都不受影响)或按住右下角按钮行最左的三点手柄,拖到任意位置、松手才落盘;拖到置顶段之内会自动置顶,拖到之外自动取消置顶。拖拽时光标是抓手,被拖的便签**拎起来跟着指针走**(轻微放大 + 倾斜 + 投影),原位置留一个**透明虚线的占位框**标出松手落点,其余便签用 FLIP 动画(200ms)平滑让位;拖到列表上下边缘会自动滚动。
13
- - **文字随尺寸自适应**:字号跟宽度走(宽度 160→900px 对应 11.5→15.5px,按 1:20 比例夹紧),窄便签字小、宽便签字大;高度固定后文字按可用空间重新排版,装不下就在便签内部滚动(细滚动条,不撑破卡片);高度留空则随内容长。
14
- - **搜索**:按内容即时过滤;置顶的永远在前,其余按创建时间倒序。
15
- - **Markdown 渲染**:正文按 Markdown 子集渲染成富文本;双击进入编辑时看到的仍是 Markdown 原文(原文才是存储格式,渲染只是展示)。
16
- - **同步到 fast-note-sync**:每张便签镜像成 vault 里的一个 Markdown(frontmatter 带颜色/置顶/尺寸),双向——Obsidian 里改正文、新建 md、重命名、删除都会同步回来。头部有颗状态胶囊,点一下立刻同步。
17
- - **冲突不丢字**:两边都改过时按时间取新的一侧,败方正文另存一张标了「冲突副本」的便签。
18
- - **持久化**:数据存在宿主侧 `~/.dsh/dsh-memo/notes.json`,浏览器只做展示——换浏览器、换设备看到的都是同一面墙。尺寸也一起存。
19
- - **Agent 工具 `memo`**:`list` / `add` / `update` / `remove`,与界面共用同一个存储实现,所以你说「记一下……」,模型可以直接贴上墙。
20
-
21
- ## 数据与接口
22
-
23
- - 数据文件:`~/.dsh/dsh-memo/notes.json`(原子写:写临时文件再 rename,写入串行化)
24
- - 浏览器接口:`POST /dsh-memo/api`,请求体 `{ "method": "list|add|update|remove", "args": { … } }`,响应 `{ "ok": true, "result": { "notes": […] } }`
25
- - 便签字段:`{ id, text, color, pinned, width, height, order, createdAt, updatedAt, remote?, syncConflict? }`
26
- - `width` / `height`:像素整数,`null` 表示「默认尺寸」(260×150)
27
- - 边界:宽 260–900,高 150–900(`index.js` 里的 `DEFAULT_NOTE_WIDTH`…`MAX_HEIGHT`);越界会被夹紧,非法值按 `null` 处理
28
- - `order`:墙面上的手工顺序(越小越靠前);老数据首次加载时按「置顶优先 + 创建时间倒序」自动补 0..n-1
29
- - `remote`:`{ path, lastTime, version, payloadHash, syncedAt }`,同步用的远端坐标与指纹(`payloadHash` 由宿主按存储内容计算,不接受外部传入)
30
- - `syncConflict`:真冲突时保留下来的「冲突副本」标记
31
- - 顶层还有 `tombstones`(本地删掉的便签对应的远端路径,等同步去删)
32
- - 布局:每张便签自带尺寸,所以墙是 flex 换行排版(不再用 CSS 多列),改尺寸时其余便签实时重排
33
- - 上限:单条 4000 字、500 条(都写在 `index.js` 顶部的常量里)
34
-
35
- ## 图片
36
-
37
- - **两种入口**:输入框底部的图片按钮(选文件)、或直接把图片**粘贴**到输入框 / 便签的编辑框里(`Ctrl/⌘+V`)。粘进去立刻上传并插入 `![](dsh-memo://…)`,保存后便签里就显示图片。
38
- - **大图自动压缩**:长边超过 1600px 或超过 900KB 的图,会先在浏览器里缩到 1600px 再转 WebP(保留透明通道);GIF 原样上传,不会被压成静态图。
39
- - **存储(两份)**:
40
- 1. **fast-note-sync 的 vault**:`attachments/<YYYY-MM-DD>/<sha256前24位>.<ext>`(日期取上传当天,目录名可在 `sync.json` 里用 `imageFolder` 改)。便签正文里写的是 vault 根路径 `![](/attachments/2026-11-04/xxx.png)`,**Obsidian 里能直接显示**。
41
- 2. **宿主本地缓存** `~/.dsh/dsh-memo/images/<name>`:墙上的 `<img>` 走 `GET /dsh-memo/images/<name>`(一年不可变缓存),所以断网/没配同步也能看图。内容寻址,同一张图只存一份,远端路径记在 `images/index.json` 里,重复上传不会重复推。
42
- - 传远端失败(没网、没配同步)时自动降级成本地引用 `![](dsh-memo://<name>)`,墙上照常显示,控制台留一条 warn。上限单张 8MB。
43
- - **旧图自动迁移**:正文里还是 `dsh-memo://` 的老图片,启动 30 秒后会自动补传到 vault 并改写成 vault 路径(只改「内容没被同时改动」的便签,失败下轮再试,幂等)。
44
- - **清理**:启动 20 秒后扫一次「没有任何便签引用、且放下超过一天」的图片并删除,避免越积越多。
45
- - **Markdown 里**:写的是 `![](dsh-memo://<name>)`。注意这是**便签墙内部的引用**——同步到 vault 的 md 在 Obsidian 里显示不出来(图片文件不在 vault 里)。要的话我可以再加一步「把图片一起传到 fast-note-sync 的附件目录并改写成 Obsidian 能认的路径」。
46
- - **归档**:分类栏的「归档」筛选;便签操作行的**归档盒**按钮一键归档/取消。归档的便签不在「全部/待办/分类」里出现,但数据保留(md 也留在 vault 里)
47
- - **回收站**:删除不再是永久删除——进回收站保留 **7 天**(头部垃圾桶按钮进入,可「恢复/彻底删除/清空」;数据里是 `trash[]` + `deletedAt`)
48
- - **撤销**:`Ctrl/Cmd+Z` 撤销最近一次改动(改文本/颜色/分类/提醒、拖拽排序、归档、删除、新建),最多 20 步;在输入框里仍走浏览器自带的文本撤销
49
- - **清单勾选**:便签里写 `- [ ] 买牛奶` → 直接**点方框**就打勾(改的是正文里的 `[x]`)
50
- - **搜索增强**:关键词命中处 `<mark>` 高亮;搜索框旁三个筛选开关——**有提醒 / 有图片 / 最近 7 天**,可与分类、关键词组合
51
- - **每日回顾推送**:`remind.json` 里加 `daily: { "enabled": true, "time": "09:00" }`,到点推一条「今日待办 N 条(逾期 M)+ 待办清单 + 新增数」,同一天只推一次
52
- - **会话页快速便签**:对话输入框右侧有个 📌 按钮(或任意界面按 **Alt+N**)→ 弹出「快速便签」小窗:写一句 **Enter 保存**(Shift+Enter 换行)、选颜色、**可顺手设提醒时间**(设了就自动成为待办);下方列出**最近 5 条便签**(点一条直接跳到便签墙并打开它),还有「打开便签墙」入口。Esc 关闭
53
- - **右侧便签抽屉**:对话输入框旁的列表按钮 → 从右侧滑出「便签」抽屉(可搜索、显示颜色点和提醒标记);**点任意一条就在会话里浮出查看卡**(不切到便签墙、**不遮挡后面内容**、按住标题栏可拖拽、**拖右下角可改尺寸、文字自动重排**),卡里的 `[[双向链接]]` **点开就是另一张卡**(和便签墙里一致),**点空白只关抽屉、不关卡片**(卡片只能点右上角 × 或按 Esc 关闭,一次 Esc 关卡片、再按一次关抽屉)。底部可「快速便签」或「打开便签墙」(此时才是显式切过去)
54
- - **快速便签归类**:面板里有个「🏷 分类(Alt+C)」按钮(不再是 `#` 语法,避免和 Markdown 标题冲突),点开或按 `Alt+C` 在**面板内就地展开**分类列表(**↑↓ 移动、空格勾选、回车/再按 Alt+C 关闭**;用的是普通文档流,不会被输入框或浮层遮住),选中后按钮显示分类名、保存时归到该分类
55
- - **选中即存**:在任意页面选中一段文字(包括对话消息),旁边会浮出「存为便签」小气泡,点一下就把选中的内容带进快速便签
56
- - **快速便签里看已有便签**:点列表里的一条 → 就地弹出**可拖拽悬浮卡**,快速便签窗口不关闭;卡里的 `[[链接]]` 还能继续点开
57
- - **焦点回流**:关掉快速便签/抽屉后,焦点自动回到打开前的输入框;输入框里已有文字就把光标放到**末尾**。快速便签重新打开时,光标也在已输入内容的末尾(内容会保留)
58
- - **快捷键**:`Alt+N` 快速便签、**`Alt+M` 便签抽屉**(**再按一次关闭**)、`Alt+C` 快速便签里的分类面板(↑↓ 移动、空格勾选、回车关闭)(设置 → 快捷键里能看到「快速便签」并改键);注册成功时不再自带监听,不会双触发
59
- - **弹层方向自适应**:便签靠左时,分类弹层 / 时间面板 / 色板会自动改成**向右展开**(`popSideFor` 比较锚点右边缘与面板左边),不再被 DSH 侧栏挡住
60
- - **弹窗内直接编辑**:查看弹窗底部「编辑」→ 原地变成大文本域(同样支持 `[[` 补全),`⌘/Ctrl+Enter` 保存、`Esc` 取消;不用再回卡片里改
61
- - **便签互链(wiki 链接)**:在便签/输入框里打 `[[` → 弹出可筛选的便签列表(↑↓ 选择、Enter/Tab 插入、Esc 取消)→ 变成 `[[便签标题]]`;标题 = 该便签第一行非空文本。查看态点链接会**叠一个可拖拽的浮窗**(原窗口不消失,按住标题栏拖动),浮窗里还能继续点链接逐层打开;没找到目标的链接显示为**红色删除线**(broken)。也支持 `[[标题|别名]]`
62
- - **看全文**:便签内容被裁掉时,底部会出现**渐变 + ⌄**(点它或点操作行的**眼睛**按钮都行)→ 弹出限宽 720px 的阅读弹窗,完整渲染 Markdown(图片可点开大图),`Esc` 关闭、`←`/`→` 在当前筛选里连续翻,底部「编辑」直接进编辑态
63
- - **自动排序**:标题右侧的九宫格按钮,一键把**当前筛选下可见的所有便签**恢复成默认尺寸(260×150)并重新装箱——墙立刻回到整齐排列。只动尺寸,不动分类/颜色/顺序。
64
- - **查看大图**:查看状态下点便签里的图片 → 面板内弹出灯箱(先按窗口适应,可 `1:1` 看原尺寸);**滚轮以光标为锚点缩放**(0.08×–10×)、**拖动平移**、`Esc` 或点空白关闭,底部显示缩放比例与原图分辨率。
65
- - 安全性:只接受 png / jpeg / webp / gif 的 data URL;路由按白名单校验文件名,`../` 之类一律 404。
66
-
67
- ## 提醒(日程)
68
-
69
- 便签可以带一个提醒时间,等于给它加了个「时间维度」:
70
-
71
- - **三种地方都能设**:**新建便签时**(输入框工具条右侧的闹钟按钮,面板**向上**展开、实色背景,选完在输入框里显示一个带 × 的时间 chip,提交后直接就是待办)、**编辑便签时**(编辑态工具栏里也有闹钟)、以及**查看态**便签操作行的闹钟
72
- - **设置**:闹钟图标 → 面板里选日期时间 + 重复规则(不重复 / 每天 / 每周 / 每月),可清除、可标记完成;不设时间就是普通便签,设了就是待办
73
- - **角标**:便签左下角显示 `今天 09:00` / `明天 09:00` / `11-08 09:00`,带重复后缀;到点变红并脉冲(`现在提醒`),提醒过但没完成的显示 `已逾期`(琥珀色),完成的显示 `已完成`
74
- - **待办视图**:分类栏最前面一个「待办」筛选,按提醒时间正序(到点/逾期的自然排最前)
75
- - **触发**:宿主每 15 秒扫一次,到点就原子地「标记 + 推进」——一次性提醒保留时间(继续显示逾期直到你勾完成),重复提醒自动推到下一次
76
- - **通知(不自动消失)**:提示条挂在 `shell.overlay`(框架级浮层),**切到任何面板都能看到,而且一直留着**——直到你点「完成」(不再提醒)、「延后 5 分钟」(5 分钟后再响)或「关闭」;同时来了多条时有「全部关闭」。同时发系统通知(首次保存提醒时会申请权限)
77
- - **墙上也能看出来**:分类栏的「待办」在**有到点未处理**的便签时变红并显示数量;便签角标:到点=红色脉冲 `现在提醒`、提醒过但没完成=琥珀色 `已逾期`、未来=灰色时间
78
- - **不静默失败**:保存后立刻回执一条「提醒已设为 今天 19:09」;没选时间会留在面板里提示,不会悄悄关掉
79
- - **默认与快捷**:打开面板默认填「10 分钟后」,另有 `1 分钟后`(方便自测)、`10 分钟后`、`1 小时后`、`明天 9:00` 四个快捷键
80
- - **错过也补**:宿主启动时先扫一次,关机期间到点的提醒会立刻补一次;重复提醒会跳过中间那些次,直接推到下一个未来时刻
81
-
82
- ### 推送通道(可选,手机不打开网页也能收到)
83
-
84
- 写 `~/.dsh/dsh-memo/remind.json`(不写就只在网页里提醒):
85
-
86
- ```json
87
- {
88
- "enabled": true,
89
- "url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",
90
- "method": "POST",
91
- "contentType": "application/json",
92
- "body": "{\"msgtype\":\"text\",\"text\":{\"content\":\"⏰ {{text}}\"}}"
93
- }
94
- ```
1
+ # ez-dsh-memo
2
+
3
+ DeepSeek Harness 的**便签墙**插件:随手记灵感、分类整理、设提醒、便签之间双向链接,还能把聊天内容一键收进便签。
95
4
 
96
- 模板里可用 `{{text}}`(便签正文)、`{{time}}`(这次提醒的时刻)、`{{id}}`(便签 id);`contentType` 含 `json` 时自动做 JSON 转义。
5
+ 中文 · [English](README.en.md)
97
6
 
98
- - **企业微信机器人**:`url` 用群机器人 webhook,`body` 如上
99
- - **ntfy**:`url` 用 `https://ntfy.sh/你的主题`,`contentType: "text/plain"`,`body: "⏰ {{text}}"`
100
- - **PushDeer**:`url` 用 `https://api2.pushdeer.com/message/push?pushkey=xxx&text=...`,`contentType: "text/plain"`
7
+ ![便签墙](docs/wall.png)
101
8
 
102
- ### 数据与 markdown
9
+ ## 安装
103
10
 
104
- ```yaml
105
- dsh-memo-remind: 2026-11-05T09:30:00+08:00
106
- dsh-memo-repeat: weekly:3 # daily / weekly:<0-6> / monthly
107
- dsh-memo-done: true # 完成才写
11
+ ```bash
12
+ # 从 npm(推荐)
13
+ dsh plugin --profile web add ez-dsh-memo
14
+
15
+ # 或从 GitHub Release
16
+ dsh plugin --profile web add https://github.com/liudiw/ez-dsh-memo/releases/latest/download/ez-dsh-memo.tgz
108
17
  ```
109
18
 
110
- 提醒字段也算进 `payloadHash`,所以改提醒时间会正常同步到 md,不会和远端正文互相覆盖。
19
+ 装完**重启 harness**。要求 Node.js ≥ 20、DSH 具备标准客户端插槽(`main` / `sidebar.panellist` / `shell.overlay` / `conversation.input.right` / `settings.section`)。
111
20
 
112
- ## 手机端
21
+ > 每台运行 harness 的机器(含桌面版)都要单独装一次;便签数据默认各机独立,需要共享就配 fast-note-sync 同步。
113
22
 
114
- - **做成 App(PWA / WebAPK)**:插件会往页面注入 `manifest`(名称「便签墙」、`start_url: /#memo`、`display: standalone`、192/512 图标 + maskable、主题色)并提供一个**惰性 service worker**(不接管任何请求,只为满足部分 Chrome 的安装判定,`Service-Worker-Allowed: /`)。手机 Chrome 打开站点 → 菜单「**添加到主屏幕**」→ 桌面出现独立图标,全屏打开、直接落在便签墙;iOS Safari「添加到主屏幕」同样可用(走 `apple-touch-icon`)。需要 HTTPS——tailnet 域名本来就是。
115
- - **深链**:URL 带 `#memo`(或 `?panel=memo`)会直接打开便签墙——把书签/主屏快捷方式设成 `https://<你的域名>/#memo` 就能一步进去。
116
- - **窄屏适配**(≤640px):分类栏改成横向滚动、工具按钮加大到 30px、便签默认**占满一整行**(手动调过尺寸的保持原尺寸)、触屏设备上操作按钮和缩放手柄常显(没有 hover 也不会点不到)。
117
- - 访问方式见 [[环境·Nest与大春]] 里的 tailnet 说明;首次用 `?token=…` 换一次 30 天有效期的签名 cookie,之后书签不用再带 token。
23
+ ## 核心能力
118
24
 
119
- ## 分类(多对多)
25
+ ### 便签墙
120
26
 
121
- - 顶部一条**分类栏**:`全部` / 每个分类(带便签计数)/ `未分类`(有这类便签时才出现)/ `+ 新分类`。
122
- - 点分类胶囊 = 只看这个分类;再点 `全部` 回到全览。
123
- - 悬停分类胶囊会出现两个小按钮:**重命名**(原地变输入框,回车确认、Esc 取消)和**删除**(行内二次确认)。删除分类会把便签上对它的引用一并摘掉,不会删便签。
124
- - 一张便签可以**同时属于多个分类**:便签右下角操作行里的**标签按钮**打开多选清单,点一下挂上/摘下;便签左下角会显示所属分类的小胶囊,点它直接筛到这个分类。
125
- - Agent 也能用:给 `memo` 工具传 `categories: ["工作","生活"]` 即可(不存在的分类会自动创建),`list` 结果里每条便签都带 `categories` 与全部现有分类名。
126
- - **分类是本地概念**(存在 `notes.json` 里),不写进 vault 的 Markdown——这样同步链路完全不用为分类操心,vault 的 md 也不会因为改分类而反复重写。如果你希望分类也出现在 md 的 frontmatter 里,说一声,我加个单向标注。
27
+ - **错落排布 + 密集装箱**:默认每行 5 张,宽度自适应;空白处会被自动填满
28
+ - **拖拽调尺寸**:拖右下角改宽高,松手后整墙重新排布
29
+ - **拖拽排序**:按住便签拖动,其他便签用 FLIP 动画让位
30
+ - **就地编辑**:双击便签进入编辑,`Ctrl/Cmd+Enter` 保存
31
+ - **搜索高亮**:关键词命中处标黄,可与分类、筛选条件叠加
127
32
 
128
- ## Markdown 支持
33
+ ### 分类
129
34
 
130
- 渲染器是随插件内联的 ~150 行受控实现(客户端半不能引第三方库),**只生成 React 元素、不拼 HTML 字符串**,所以不需要额外净化:文本永远走 textContent 路径,写 `<script>` 只会显示成字面量。
35
+ - **多对多**:一张便签可属于多个分类
36
+ - 顶部分类栏显示每类计数,点击筛选;`Alt+C` 或「分类」按钮打开选择面板(**↑↓ 移动、空格勾选、回车确认**)
37
+ - 分类可拖拽排序、重命名、删除(删除只解除关联,不删便签)
131
38
 
132
- | 写法 | 效果 |
133
- |---|---|
134
- | `# / ## / ###` | 标题(渲染成 h3–h6,按 1.34/1.22/1.1em 缩放) |
135
- | `**粗**`、`*斜*`、`~~删~~`、`` `代码` `` | 行内样式;行内代码优先,`` `**x**` `` 不会被当成粗体 |
136
- | ` ```js ` 围栏 | 代码块(等宽、可横向滚动,保留原文缩进) |
137
- | `- / * / +`、`1.` | 无序 / 有序列表;缩进续行并进同一项 |
138
- | `- [ ] / - [x]` | 任务列表(勾选框只做展示,不可点) |
139
- | `> 引用` | 引用块(左侧细线 + 次要文字色) |
140
- | `---` | 分隔线 |
141
- | `[文字](https://…)`、裸 `https://…` | 链接;**只允许 http/https/mailto**,其余(`javascript:`、`data:`…)退化成纯文本 |
142
- | `[[某篇笔记]]` | 双链样式的小胶囊(只做展示,点击不跳转) |
143
- | `\|a\|b\|` + `\|---\|:-:\|` | 表格(GFM):支持 `:-:` 居中、`--:` 右对齐;列多时表格自己横向滚动,不撑破便签 |
144
- | 空行 / 单个换行 | 分段 / 段内软换行(与 Obsidian 默认一致) |
39
+ ### 提醒
145
40
 
146
- 图片语法先按原文显示(不发起外部请求)。这些元素全部用主题 token 着色,亮/暗主题都自适应。
41
+ - 每张便签可设**提醒时间 + 重复规则**(每天/每周/每月)
42
+ - 宿主每 15 秒扫描一次,到点弹出**不自动消失**的提醒卡(完成 / 延后 5 分钟 / 关闭)
43
+ - 支持 webhook 推送(企业微信、ntfy、PushDeer 等)与每日回顾摘要
44
+ - 逾期未完成的便签在墙上显示醒目标记
147
45
 
148
- ## 同步到 fast-note-sync(可选)
46
+ ### 便签之间的双向链接 `[[...]]`
149
47
 
150
- 配置放在 `~/.dsh/dsh-memo/sync.json`(`chmod 600`,**含 API 令牌,别提交到任何仓库**):
151
-
152
- ```json
153
- {
154
- "enabled": true,
155
- "api": "https://obsidian-fns.iepose.cn",
156
- "apiToken": "<从 FNS 网页端生成的 API 令牌>",
157
- "vault": "Memo Vault",
158
- "folder": "便签墙",
159
- "intervalMs": 60000
160
- }
161
- ```
48
+ 便签正文里可以写 `[[另一张便签]]`,点击即可跳到那张便签。
162
49
 
163
- - `api` 可以带不带 `/api` 后缀;`folder` 是 vault 里的目录(默认 `便签墙`);`intervalMs` 是后台轮询间隔(最短 15s)。
164
- - 没有这个文件 / 字段不全 / JSON 坏掉 → 同步自动关闭,便签墙照常本地可用,界面上不显示同步胶囊。
165
- - 令牌是「服务端密钥 + 机器 ID」签名的,**只能由同一个 FNS 实例生成**;换了实例要重新生成。
50
+ - **编辑时输入 `[[`** 会弹出便签候选列表:`↑↓` 选择、`Enter` 插入、`Esc` 取消
51
+ - 支持别名写法 `[[目标便签|显示文字]]`
52
+ - 插入后渲染成**可点链接**,点开是**新的悬浮查看卡**(不覆盖当前这张),可以顺着链接一路看下去
53
+ - 链接目标不存在时显示为普通文字,不会报错
54
+ - 快速便签、查看卡、便签编辑态里都能用
166
55
 
167
- ### 落地的 Markdown 长什么样
56
+ > 如果你打着 `[[` 没看到候选,请确认是在**便签的编辑态**里(查看卡 → 编辑,或便签卡双击),而不是聊天输入框。
168
57
 
169
- `便签墙/2026-11-03-2237-摘语片段-f294.md`:
58
+ ### 查看卡
170
59
 
171
- ```markdown
172
- ---
173
- dsh-memo-id: f2940a7f-9158-4608-be2c-cb276b152341
174
- created: 2026-11-03T14:37:26.652Z
175
- dsh-memo-color: rose
176
- dsh-memo-pinned: true
177
- dsh-memo-width: 372
178
- dsh-memo-height: 208
179
- ---
60
+ ![查看卡](docs/viewer.png)
180
61
 
181
- 便签正文,原样保留换行。
182
- ```
62
+ - 独立悬浮窗,可**拖动**、可**拖右下角缩放**,正文随宽度自动重排
63
+ - 卡内可以直接编辑、设提醒、加分类、导出图片
64
+ - 卡里的 `[[链接]]` 点击后**另开一张卡**,多张卡可同时存在
65
+
66
+ ### 快速便签
67
+
68
+ ![快速便签](docs/quick.png)
69
+
70
+ - 全局快捷键 **`Alt+N`**,或点会话输入框旁的 📌
71
+ - `Enter` 保存、`Shift+Enter` 换行、**`Alt+S` 保存**、**`Alt+R` 重置**
72
+ - 可选颜色、分类(多选)、提醒时间
73
+ - **草稿自动保存**:正文/颜色/分类/提醒一起存到本地,关窗口、刷新、重启后都能接着写
74
+ - 关闭时若有内容,会问「存草稿 / 不存,清空 / 取消」
75
+ - 保存后窗口自动关闭,光标回到会话输入框末尾
76
+
77
+ ### 便签抽屉
78
+
79
+ - **`Alt+M`** 或点会话输入框旁的列表按钮,从右侧滑出
80
+ - 可搜索,`↑↓` 选择、`Enter` 打开
81
+ - 点任意一条 → **在会话里浮出查看卡**,不打断聊天;点抽屉外面的空白关闭抽屉(卡片保留)
82
+
83
+ ### 把聊天内容收进便签
84
+
85
+ ![聊天收集](docs/capture.png)
183
86
 
184
- 文件名只在首次推送时生成;之后改正文沿用同一个文件(不会在 vault 里越推越多)。
87
+ - 每条助手回复的操作条上有 **📌 存成便签**:点一下把这条回复收进便签
88
+ - 收集时**还原 Markdown**:标题、列表(含嵌套)、代码块(保留语言与缩进)、表格(自动补分隔行)、引用、链接、粗体斜体
89
+ - 只取正文,**不会把「已编辑 client.js / 用量 1.3M tok」这类状态条混进来**
90
+ - 也可以选中任意文字,用选区右下的气泡「存为便签」
185
91
 
186
- ### 双向语义
92
+ ### 其它
187
93
 
188
- | 场景 | 行为 |
189
- |---|---|
190
- | 本地新增/改正文/换色/置顶/改尺寸 | 推送(新增用 `POST /api/note`,有映射则原地覆盖) |
191
- | 在 Obsidian 里改正文/颜色/置顶/尺寸 | 拉回本地 |
192
- | 在 Obsidian 里**新建** md(放在 `folder` 目录里) | 导入成便签,frontmatter 里的颜色/置顶/尺寸生效 |
193
- | 在 Obsidian 里**重命名** md | 靠 frontmatter 的 `dsh-memo-id` 认回原便签(rebind),不会变成两条 |
194
- | 在 Obsidian 里**删除** md | 本地便签跟随删除(远端是进回收站,能救回) |
195
- | 本地删除便签 | 远端对应的 md 进回收站(删不掉的记成「墓碑」,下次重试) |
196
- | 两边都改过(真冲突) | **一律本地优先**(不比两台的墙钟——机器差几秒就会随机翻转判定);远端那份正文另存一张标「冲突副本」的便签,不丢字。单轮最多 5 份、同一文案只留一份 |
94
+ - **回收站**:删除进回收站保留 7 天,可还原或彻底清空
95
+ - **归档**:归档后不在墙上显示,但可搜索到
96
+ - **撤销**:`Ctrl/Cmd+Z` 撤销编辑、移动、归档、删除(最近 20 步)
97
+ - **图片**:粘贴或上传,按内容寻址存本地,同步时上传到 vault 的 `attachments/<日期>/`
98
+ - **任务清单**:`- [ ]` 渲染成可点的复选框
99
+ - **导出图片**:把一张便签画成纸感 PNG 下载
100
+ - **AI 读写**:内置 `memo` 工具(`list` / `add` / `update` / `remove` / `get` / `sync`),模型可以直接操作这面墙
101
+
102
+ ## 同步到 fast-note-sync(可选)
103
+
104
+ 便签**始终**保存在本机 `~/.dsh/dsh-memo/notes.json`,同步只是把它镜像成 Markdown。
105
+
106
+ 在 **设置 → 便签墙** 里填:服务地址、访问令牌、Vault 名称、便签目录(默认 `便签墙`)、附件目录(默认 `attachments`)、同步间隔。
107
+
108
+ - 令牌只显示尾号,**不会明文回显**;留空表示不修改
109
+ - 图片上传到 vault 的 `attachments/<YYYY-MM-DD>/<hash>.<ext>`
110
+ - 前端带 `dsh-memo-*` frontmatter 记录颜色/置顶/提醒等元数据
111
+
112
+ ## 快捷键
113
+
114
+ | 按键 | 作用 |
115
+ | --- | --- |
116
+ | `Alt+N` | 快速便签(开/关) |
117
+ | `Alt+S` | 保存快速便签并关闭 |
118
+ | `Alt+R` | 重置快速便签 |
119
+ | `Alt+C` | 分类选择面板(↑↓ / 空格 / 回车) |
120
+ | `Alt+M` | 便签抽屉(开/关) |
121
+ | `Alt+W` | 关闭最上层浮窗 |
122
+ | `Ctrl/Cmd+Z` | 撤销 |
123
+ | `Ctrl/Cmd+Enter` | 保存编辑 |
124
+
125
+ ## 数据位置
126
+
127
+ ```
128
+ ~/.dsh/dsh-memo/
129
+ ├── notes.json 便签本体(原子写,0600)
130
+ ├── images/ 图片缓存(内容寻址)
131
+ ├── sync.json 同步配置(0600,令牌不回显)
132
+ ├── remind.json 推送通道 / 每日回顾(可选)
133
+ └── pwa.json 可选:存在时注入 PWA manifest
134
+ ```
135
+
136
+ ## 与同类插件对比
137
+
138
+ | | ez-dsh-memo | dsh-sticky-note | dsh-sticky-notes |
139
+ | --- | --- | --- | --- |
140
+ | 形态 | **独立墙面** | 输入框旁浮层 | 可拖拽便签纸 |
141
+ | 排版 | 密集装箱 + 拖拽调尺寸/排序 | 列表/堆叠 | 自由拖动 |
142
+ | 分类 | **多对多 + 拖拽排序** | 固定三类 | — |
143
+ | 提醒 | **时间 + 重复 + 推送 + 每日回顾** | — | — |
144
+ | 便签互链 | **`[[双向链接]]`** | — | — |
145
+ | 聊天收集 | **单条回复 → 便签(Markdown 还原)** | — | — |
146
+ | Markdown 同步 | **fast-note-sync(vault + 附件)** | 归档目录 | — |
147
+ | 回收站/归档/撤销 | **都有** | 归档 + 自动清理 | — |
148
+ | AI 读写 | **`memo` 工具** | 发送到对话 | 读写 |
149
+
150
+ ## 开发
151
+
152
+ ```bash
153
+ node --check index.js && node --check client.js && node --check sync.js
154
+ ./scripts/release.sh 0.1.7 # 在临时目录改名打包,不动工作区的 @local 包名
155
+ ```
197
156
 
198
- 同步失败不会挡住本地写入:先落 `notes.json`,失败只在日志里告警、界面上胶囊变红,下次再试。
199
- Agent 也可以用 `memo` 工具的 `sync` 动作手动触发一次。
157
+ 工作区安装名固定为 `@local/dsh-memo`(本机软链),发布包名 `ez-dsh-memo`。
200
158
 
201
- ## 安装 / 卸载
159
+ ## 许可
202
160
 
203
- 用 `plugin_manager` 安装本目录(bundle 名 `@local/dsh-memo`,行 id `dsh-memo`),安装后左栏即出现图标;停用该 bundle 即从侧栏消失,数据文件保留。
161
+ MIT
package/client.js CHANGED
@@ -146,9 +146,9 @@ window.__ModuleLoader__.load({
146
146
  'set.errApi': '服务地址要以 http:// 或 https:// 开头',
147
147
  'set.errVault': 'Vault 名称不能为空',
148
148
  'set.tip': '便签始终保存在本机 ~/.dsh/dsh-memo/notes.json,同步只是把它镜像成 Markdown。',
149
- 'capture.button': '收进便签',
149
+ 'capture.button': '存成便签',
150
150
  'capture.collect': '收集本轮',
151
- 'capture.button': 'Save to notes',
151
+ 'capture.button': '存成便签',
152
152
  'capture.collect': 'Collect turn',
153
153
  'quick.reset': '重置(清空全部,Alt+R)',
154
154
  'quick.keepDraft': '内容还没存成便签,要留着当草稿吗?',
@@ -380,6 +380,11 @@ window.__ModuleLoader__.load({
380
380
  .dsm-root[data-skin='warm']{--dsm-skin-bg:linear-gradient(180deg,#fff8f1,#ffefe3)}
381
381
  .dsm-collectBtnUnused{font:inherit;font-size:11.5px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:.8}
382
382
  .dsm-collectBtn:hover{opacity:1;color:var(--dsw-alias-brand-primary)}
383
+ .dsm-tip{position:relative}
384
+ .dsm-tip:hover::after{content:attr(data-tip);position:absolute;left:50%;top:calc(100% + 6px);transform:translateX(-50%);white-space:nowrap;background:#1b1b1f;color:#fff;font-size:13.5px;line-height:1;padding:7px 10px;border-radius:7px;pointer-events:none;z-index:2147483000;box-shadow:0 8px 20px -8px rgba(0,0,0,.5)}
385
+ .dsm-confirmPop{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:40;width:min(320px,80vw);padding:14px 16px;border-radius:14px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,var(--dsw-alias-border-l1));background:var(--dsw-alias-bg-base);box-shadow:0 26px 60px -26px rgba(0,0,0,.55),0 0 0 1px color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent);pointer-events:auto}
386
+ .dsm-confirmText{font-size:13px;line-height:1.6;color:var(--dsw-alias-label-primary);margin-bottom:12px}
387
+ .dsm-confirmActions{display:flex;gap:8px;justify-content:flex-end}
383
388
  .dsm-captureGroup{display:inline-flex;align-items:center;gap:2px}
384
389
  .dsm-mention{position:fixed;z-index:2147482300;width:min(340px,70vw);max-height:240px;overflow-y:auto;padding:5px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);box-shadow:0 18px 40px -18px rgba(0,0,0,.45)}
385
390
  .dsm-mentionItem{display:block;width:100%;text-align:left;font:inherit;font-size:12.5px;padding:5px 8px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -610,7 +615,8 @@ window.__ModuleLoader__.load({
610
615
  .dsm-quickBtn{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:none;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
611
616
  .dsm-quickBtn:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
612
617
  .dsm-quickHost{position:fixed;inset:0;z-index:2147482000;pointer-events:none}
613
- .dsm-quickSel{position:fixed;transform:translate(2px,4px);pointer-events:auto;display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 9px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:11.5px;box-shadow:0 8px 20px -8px rgba(0,0,0,.4);cursor:pointer}
618
+ .dsm-quickSel{position:fixed;transform:translate(2px,6px);pointer-events:auto;display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 14px;border-radius:999px;border:none;background:var(--dsw-alias-brand-primary,#4c6ef5);color:#fff;font:inherit;font-size:13px;font-weight:600;letter-spacing:.2px;box-shadow:0 10px 26px -8px rgba(0,0,0,.5);cursor:pointer}
619
+ .dsm-quickSel:hover{filter:brightness(1.06)}
614
620
  .dsm-quickSel:hover{background:var(--dsw-alias-bg-layer-1)}
615
621
  .dsm-quickRemind{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}
616
622
  .dsm-quickTime{font:inherit;font-size:12px;height:24px;padding:0 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}
@@ -646,7 +652,7 @@ window.__ModuleLoader__.load({
646
652
  .dsm-setNote{font-size:12px;color:var(--dsw-alias-label-secondary)}
647
653
  /* 快速便签 */
648
654
  .dsm-quickLayer{position:fixed;inset:0;z-index:2147482000;pointer-events:auto;display:flex;align-items:flex-start;justify-content:center;padding-top:12vh;background:color-mix(in srgb,var(--dsw-alias-label-primary) 20%,transparent)}
649
- .dsm-quickPanel{pointer-events:auto;box-sizing:border-box;width:min(520px,92vw);display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:14px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 30px 70px -30px rgba(0,0,0,.55)}
655
+ .dsm-quickPanel{pointer-events:auto;position:relative;box-sizing:border-box;width:min(520px,92vw);display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:14px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 30px 70px -30px rgba(0,0,0,.55)}
650
656
  .dsm-quickHead{display:flex;align-items:center;gap:7px;font-size:13px}
651
657
  .dsm-quickConfirm{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px;padding:7px 8px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);border:1px solid var(--dsw-alias-border-l1)}
652
658
  .dsm-quickConfirm>span{flex:1 1 100%;color:var(--dsw-alias-label-secondary)}
@@ -662,7 +668,10 @@ window.__ModuleLoader__.load({
662
668
  .dsm-quickDot{width:7px;height:7px;border-radius:50%;flex:none}
663
669
  .dsm-quickRecentText{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
664
670
  /* [[ 便签候选 */
665
- .dsm-wikiList{position:absolute;left:8px;right:8px;bottom:calc(100% - 4px);z-index:30;max-height:220px;overflow-y:auto;padding:5px;border-radius:10px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 18px 40px -18px rgba(0,0,0,.4);display:flex;flex-direction:column;gap:2px}
671
+ .dsm-wikiList{position:absolute;left:8px;right:8px;bottom:calc(100% - 4px);z-index:2147482500;max-height:220px;overflow-y:auto;padding:5px;border-radius:10px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 18px 40px -18px rgba(0,0,0,.4);display:flex;flex-direction:column;gap:2px}
672
+ .dsm-wikiInline .dsm-wikiList{position:static;left:auto;right:auto;top:auto;bottom:auto;max-height:200px}
673
+ .dsm-viewerEdit{position:relative;overflow:visible}
674
+ .dsm-viewerEdit .dsm-wikiList{top:calc(100% + 6px);bottom:auto}
666
675
  .dsm-wikiItem{display:block;width:100%;text-align:left;font:inherit;font-size:12.5px;padding:6px 8px;border:none;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
667
676
  .dsm-wikiItem[data-active='true'],.dsm-wikiItem:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}
668
677
  .dsm-wikiEmpty{padding:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}
@@ -1104,7 +1113,7 @@ window.__ModuleLoader__.load({
1104
1113
  if (!query) return true;
1105
1114
  return String(note.text || '').toLowerCase().indexOf(query) >= 0;
1106
1115
  })
1107
- .slice(0, 8);
1116
+ .slice(0, 40);
1108
1117
  if (!items.length) {
1109
1118
  return h('div', { className: 'dsm-wikiList' }, h('div', { className: 'dsm-wikiEmpty' }, t('wiki.empty')));
1110
1119
  }
@@ -2331,6 +2340,65 @@ window.__ModuleLoader__.load({
2331
2340
  }
2332
2341
 
2333
2342
  /** 会话消息尾部/操作条的「收进便签」按钮:从 DOM 取该条消息的渲染文本。 */
2343
+ /** [[ 候选浮层:直接挂 body,固定定位在输入框下方,支持高亮跟随。 */
2344
+ function showWikiPop(items, rect, onPick) {
2345
+ const el = document.createElement('div');
2346
+ el.setAttribute('style', 'position:fixed;z-index:2147483400;left:' + Math.round(rect.left) + 'px;top:'
2347
+ + Math.round(rect.bottom + 6) + 'px;width:' + Math.round(Math.min(Math.max(rect.width, 240), 460)) + 'px;'
2348
+ + 'max-height:240px;overflow-y:auto;padding:5px;display:flex;flex-direction:column;gap:2px;'
2349
+ + 'background:var(--dsw-alias-bg-base,#fff);border:1px solid var(--dsw-alias-border-l1,#ddd);'
2350
+ + 'border-radius:10px;box-shadow:0 18px 40px -18px rgba(0,0,0,.45)');
2351
+ const buttons = items.map((item) => {
2352
+ const button = document.createElement('button');
2353
+ button.type = 'button';
2354
+ button.textContent = item.label;
2355
+ button.setAttribute('style', 'display:flex!important;align-items:center;width:100%;box-sizing:border-box;'
2356
+ + 'text-align:left;font-family:inherit;font-size:12.5px!important;line-height:1.5!important;'
2357
+ + 'height:auto!important;min-height:26px!important;max-height:none!important;'
2358
+ + 'padding:6px 9px!important;margin:0!important;border:none;border-radius:6px;background:transparent;'
2359
+ + 'cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'
2360
+ + 'color:var(--dsw-alias-label-primary,#222)');
2361
+ button.onmousedown = (event) => { event.preventDefault(); onPick(item.note); };
2362
+ el.appendChild(button);
2363
+ return button;
2364
+ });
2365
+ document.body.appendChild(el);
2366
+ const setActive = (index) => {
2367
+ buttons.forEach((button, position) => {
2368
+ button.style.background = position === index
2369
+ ? 'color-mix(in srgb, var(--dsw-alias-brand-primary,#4c6ef5) 16%, transparent)'
2370
+ : 'transparent';
2371
+ });
2372
+ const active = buttons[index];
2373
+ if (active && typeof active.scrollIntoView === 'function') {
2374
+ try { active.scrollIntoView({ block: 'nearest' }); } catch (error) { /* 忽略 */ }
2375
+ }
2376
+ };
2377
+ setActive(0);
2378
+ return {
2379
+ remove: () => { try { el.remove(); } catch (error) { /* 忽略 */ } },
2380
+ setActive,
2381
+ };
2382
+ }
2383
+
2384
+ /** 悬浮 tooltip:挂到 body 上,黑底白字,显示在目标下方(不会被容器裁切)。 */
2385
+ function showTip(text, target) {
2386
+ try {
2387
+ if (!target || typeof document === 'undefined') return () => {};
2388
+ const box = target.getBoundingClientRect();
2389
+ const tip = document.createElement('div');
2390
+ tip.textContent = text;
2391
+ tip.setAttribute('style', 'position:fixed;z-index:2147483600;left:' + Math.round(box.left + box.width / 2)
2392
+ + 'px;top:' + Math.round(box.bottom + 6) + 'px;transform:translateX(-50%);white-space:nowrap;'
2393
+ + 'background:#1b1b1f;color:#fff;font-size:13.5px;line-height:1;padding:7px 10px;border-radius:7px;'
2394
+ + 'pointer-events:none;box-shadow:0 8px 20px -8px rgba(0,0,0,.5)');
2395
+ document.body.appendChild(tip);
2396
+ return () => { try { tip.remove(); } catch (error) { /* 忽略 */ } };
2397
+ } catch (error) {
2398
+ return () => {};
2399
+ }
2400
+ }
2401
+
2334
2402
  function makeCaptureButton(t) {
2335
2403
  return function MemoCaptureButton(props) {
2336
2404
  const collect = (event) => {
@@ -2355,13 +2423,15 @@ window.__ModuleLoader__.load({
2355
2423
  console.error('dsh-memo: 收集消息失败', error);
2356
2424
  }
2357
2425
  };
2426
+ let hideTip = () => {};
2358
2427
  return h('button', {
2359
2428
  type: 'button',
2360
2429
  className: 'dsm-captureBtn',
2361
- title: t('capture.button'),
2362
2430
  'aria-label': t('capture.button'),
2363
2431
  'data-message-id': props && props.messageId ? props.messageId : undefined,
2364
2432
  onClick: collect,
2433
+ onMouseEnter: (event) => { hideTip = showTip(t('capture.button'), event.currentTarget); },
2434
+ onMouseLeave: () => { hideTip(); hideTip = () => {}; },
2365
2435
  }, h(Icon, { name: 'pin', size: 16 }));
2366
2436
  };
2367
2437
  }
@@ -2652,6 +2722,7 @@ window.__ModuleLoader__.load({
2652
2722
  const [draft, setDraft] = useState(() => readDraft().text);
2653
2723
  const [color, setColor] = useState(() => readDraft().color);
2654
2724
  const [recent, setRecent] = useState([]);
2725
+ const [allNotes, setAllNotes] = useState([]);
2655
2726
  const [busy, setBusy] = useState(false);
2656
2727
  const [flash, setFlash] = useState('');
2657
2728
  const [remindAt, setRemindAt] = useState(() => readDraft().remindAt);
@@ -2673,9 +2744,10 @@ window.__ModuleLoader__.load({
2673
2744
  const loadRecent = useCallback(async () => {
2674
2745
  try {
2675
2746
  const result = await apiCall('list');
2676
- setRecent((result && Array.isArray(result.notes) ? result.notes : [])
2677
- .filter((note) => note.archived !== true)
2678
- .slice(0, 5));
2747
+ const list = (result && Array.isArray(result.notes) ? result.notes : [])
2748
+ .filter((note) => note.archived !== true);
2749
+ setRecent(list.slice(0, 5));
2750
+ setAllNotes(list);
2679
2751
  setCategories(result && Array.isArray(result.categories) ? result.categories : []);
2680
2752
  } catch (error) {
2681
2753
  setRecent([]);
@@ -2705,6 +2777,36 @@ window.__ModuleLoader__.load({
2705
2777
  loadRecent();
2706
2778
  }, [loadRecent, open]);
2707
2779
 
2780
+ const wikiPopRef = useRef(null);
2781
+
2782
+ useEffect(() => {
2783
+ if (wikiPopRef.current) { wikiPopRef.current.remove(); wikiPopRef.current = null; }
2784
+ if (!wiki || !open) return undefined;
2785
+ const node = areaRef.current;
2786
+ if (!node || !node.getBoundingClientRect) return undefined;
2787
+ const rect = node.getBoundingClientRect();
2788
+ const path = String(wiki.query || '').trim().toLowerCase();
2789
+ const items = allNotes
2790
+ .filter((item) => (!path ? true : String(item.text || '').toLowerCase().indexOf(path) >= 0))
2791
+ .slice(0, 40)
2792
+ .map((item) => ({ note: item, label: noteTitleOf(item) || item.id.slice(0, 8) }));
2793
+ if (!items.length) return undefined;
2794
+ wikiPopRef.current = showWikiPop(items, rect, (picked) => {
2795
+ const title = noteTitleOf(picked) || picked.id.slice(0, 8);
2796
+ const next = applyWikiLink(draft, wiki.start, title);
2797
+ setDraft(next.text);
2798
+ setWiki(null);
2799
+ setWikiIndex(0);
2800
+ });
2801
+ return () => {
2802
+ if (wikiPopRef.current) { wikiPopRef.current.remove(); wikiPopRef.current = null; }
2803
+ };
2804
+ }, [wiki, open, allNotes, draft]);
2805
+
2806
+ useEffect(() => {
2807
+ if (wikiPopRef.current) wikiPopRef.current.setActive(wikiIndex);
2808
+ }, [wikiIndex, wiki]);
2809
+
2708
2810
  const hide = useCallback(() => {
2709
2811
  setOpen(false);
2710
2812
  setConfirmClose(false);
@@ -3090,11 +3192,12 @@ window.__ModuleLoader__.load({
3090
3192
  onClick: requestClose,
3091
3193
  }, h(Icon, { name: 'close', size: 13 }))),
3092
3194
  confirmClose
3093
- ? h('div', { className: 'dsm-quickConfirm' },
3094
- h('span', null, t('quick.keepDraft')),
3095
- h('button', { type: 'button', className: 'dsm-btn dsm-btnPrimary', onClick: () => hide() }, t('quick.saveDraft')),
3096
- h('button', { type: 'button', className: 'dsm-btn', onClick: () => { resetAll(); hide(); } }, t('quick.discard')),
3097
- h('button', { type: 'button', className: 'dsm-btn', onClick: () => setConfirmClose(false) }, t('quick.cancel')))
3195
+ ? h('div', { className: 'dsm-confirmPop', role: 'dialog', 'aria-label': t('quick.keepDraft') },
3196
+ h('div', { className: 'dsm-confirmText' }, t('quick.keepDraft')),
3197
+ h('div', { className: 'dsm-confirmActions' },
3198
+ h('button', { type: 'button', className: 'dsm-btn dsm-btnPrimary', onClick: () => hide() }, t('quick.saveDraft')),
3199
+ h('button', { type: 'button', className: 'dsm-btn', onClick: () => { resetAll(); hide(); } }, t('quick.discard')),
3200
+ h('button', { type: 'button', className: 'dsm-btn', onClick: () => setConfirmClose(false) }, t('quick.cancel'))))
3098
3201
  : null,
3099
3202
  h('textarea', {
3100
3203
  ref: areaRef,
@@ -3112,7 +3215,7 @@ window.__ModuleLoader__.load({
3112
3215
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
3113
3216
  event.preventDefault();
3114
3217
  const step = event.key === 'ArrowDown' ? 1 : -1;
3115
- const list = recent.filter((item) => (!String(wiki.query || '').trim()
3218
+ const list = allNotes.filter((item) => (!String(wiki.query || '').trim()
3116
3219
  ? true
3117
3220
  : String(item.text || '').toLowerCase().indexOf(String(wiki.query).trim().toLowerCase()) >= 0));
3118
3221
  setWikiIndex((current) => Math.max(0, Math.min(current + step, Math.max(0, list.length - 1))));
@@ -3121,7 +3224,7 @@ window.__ModuleLoader__.load({
3121
3224
  if (event.key === 'Enter' || event.key === 'Tab') {
3122
3225
  event.preventDefault();
3123
3226
  const needle = String(wiki.query || '').trim().toLowerCase();
3124
- const list = recent.filter((item) => (!needle ? true : String(item.text || '').toLowerCase().indexOf(needle) >= 0));
3227
+ const list = allNotes.filter((item) => (!needle ? true : String(item.text || '').toLowerCase().indexOf(needle) >= 0));
3125
3228
  const picked = list[Math.min(wikiIndex, list.length - 1)];
3126
3229
  if (picked) {
3127
3230
  const title = noteTitleOf(picked) || picked.id.slice(0, 8);
@@ -3158,6 +3261,7 @@ window.__ModuleLoader__.load({
3158
3261
  }
3159
3262
  },
3160
3263
  }),
3264
+
3161
3265
  h('div', { className: 'dsm-quickCats' },
3162
3266
  h('div', { className: 'dsm-paletteWrap' },
3163
3267
  h('button', {
@@ -3776,9 +3880,34 @@ window.__ModuleLoader__.load({
3776
3880
  return (props.allNotes || [])
3777
3881
  .filter((item) => item.archived !== true && item.id !== note.id)
3778
3882
  .filter((item) => (!needle ? true : String(item.text || '').toLowerCase().indexOf(needle) >= 0))
3779
- .slice(0, 8);
3883
+ .slice(0, 40);
3780
3884
  }, [wiki, props.allNotes, note.id]);
3781
3885
 
3886
+ const popRef = useRef(null);
3887
+
3888
+ useEffect(() => {
3889
+ if (popRef.current) { popRef.current.remove(); popRef.current = null; }
3890
+ if (!wiki || !editing) return undefined;
3891
+ const node = inputRef.current;
3892
+ if (!node || !node.getBoundingClientRect) return undefined;
3893
+ const rect = node.getBoundingClientRect();
3894
+ const items = (props.allNotes || [])
3895
+ .filter((item) => (!String(wiki.query || '').trim()
3896
+ ? true
3897
+ : String(item.text || '').toLowerCase().indexOf(String(wiki.query).trim().toLowerCase()) >= 0))
3898
+ .slice(0, 40)
3899
+ .map((item) => ({ note: item, label: noteTitleOf(item) || item.id.slice(0, 8) }));
3900
+ if (!items.length) return undefined;
3901
+ popRef.current = showWikiPop(items, rect, (picked) => pickWiki(picked));
3902
+ return () => {
3903
+ if (popRef.current) { popRef.current.remove(); popRef.current = null; }
3904
+ };
3905
+ }, [wiki, editing, props.allNotes]);
3906
+
3907
+ useEffect(() => {
3908
+ if (popRef.current) popRef.current.setActive(wikiIndex);
3909
+ }, [wikiIndex, wiki]);
3910
+
3782
3911
  const pickWiki = (target) => {
3783
3912
  if (!wiki) return;
3784
3913
  const title = noteTitleOf(target) || target.id.slice(0, 8);
@@ -3952,9 +4081,7 @@ window.__ModuleLoader__.load({
3952
4081
  }
3953
4082
  },
3954
4083
  }),
3955
- wiki
3956
- ? h(WikiSuggest, { notes: props.allNotes || [], query: wiki.query, index: wikiIndex, onPick: pickWiki })
3957
- : null)
4084
+ )
3958
4085
  : h('div', { className: 'dsm-viewerBody' },
3959
4086
  h('div', { className: 'dsm-noteText' }, withWikiContext(
3960
4087
  { notes: props.allNotes || [], onOpen: props.onOpenWiki },
@@ -4118,7 +4245,7 @@ window.__ModuleLoader__.load({
4118
4245
  return (props.allNotes || [])
4119
4246
  .filter((item) => item.archived !== true && item.id !== note.id)
4120
4247
  .filter((item) => (!needle ? true : String(item.text || '').toLowerCase().indexOf(needle) >= 0))
4121
- .slice(0, 8);
4248
+ .slice(0, 40);
4122
4249
  }, [wiki, props.allNotes, note.id]);
4123
4250
 
4124
4251
  const insertWikiLink = useCallback((target) => {
Binary file
package/docs/quick.png ADDED
Binary file
Binary file
package/docs/wall.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ez-dsh-memo",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "A sticky-note wall for DeepSeek Harness: markdown notes, categories, reminders, wiki links, images, and fast-note-sync mirroring.",
6
6
  "icon": "./icon.svg",
@@ -21,7 +21,8 @@
21
21
  "locale/*.json",
22
22
  "LICENSE",
23
23
  "README.md",
24
- "README.en.md"
24
+ "README.en.md",
25
+ "docs/*.png"
25
26
  ],
26
27
  "dsh": {
27
28
  "bundle": {