dsh-kp-notes
DeepSeek Harness 知识点(Knowledge Point)笔记插件:把 Markdown 笔记目录变成卡片式学习画布,章节/小节/知识点/题目分层,支持 KaTeX 公式、Mermaid 流程图、思维导图与面板内编辑
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 3, 2026
- Updated
- Oct 6, 2026
Introduction
学习画布 · dsh-kp-notes
把一整个目录的 Markdown 笔记变成一张可以平移、缩放的卡片画布 —— 一个目录 = 一章,一章 = 一张卡;知识点是卡片,题目自带答案遮挡,随手自测。
写给 DeepSeek Harness 的插件(宿主半 + 浏览器半)。笔记就是普通的 markdown 文件,留在你自己的目录里:插件不建数据库、不改你的文件格式,删掉插件笔记照样能读。渲染引擎与编辑器随插件分发,不联网、不依赖 CDN、不需要构建;界面中英双语,跟随宿主语言。
界面预览
| 画布:章节卡 → 小节 → 知识点卡片墙 | 知识点详情:真表格 + LaTeX 公式(KaTeX) |
|---|---|
![]() | ![]() |
| 题目:答案默认折成「▸ 答案已遮挡」 | 点开自测:答案 + 解析 |
![]() | ![]() |
| 思维导图:章 → 小节 → 知识点 | 正文里的 mermaid 流程图(本地渲染) |
![]() | ![]() |
| 编辑知识点:笔记就是普通 markdown | 多画布:一个目录 = 一块画布 |
![]() | ![]() |
特性一览
| 画布 | 章节卡排成一行、不换行,可自由平移 / 缩放(20% ~ 240%);点小节在右侧展开知识点卡片墙 |
| 思维导图 | 同一份笔记切换成「章 → 小节 → 知识点 → 正文」的左→右树;正文按小标题拆成小知识点卡片,同一时刻只展开一个(手风琴) |
| 多画布 | 一级画布上每张卡 = 磁盘上一个笔记根目录(软考、别的考试、工作笔记各放一个,互不干扰);画布清单 / 移出记录 / 字号配色主题跨浏览器同步 |
| 题目与自测 | 一个知识点配一个题目文件,一个文件可放任意多道(每道一个 ## 题目 N);答案默认折叠成「▸ 答案已遮挡」,点开自测;选择题 / 案例题两种题型可视化录入 |
| 富文本编辑 | 编辑器抽屉默认所见即所得(Milkdown:/ 斜杠菜单、选区工具条),一键切回「markdown 源码 + 实时预览」;文件开头的 YAML 头不会被富文本碰坏;Milkdown 回吐 markdown 时会防御性转义(把正文里的 _ ~ & [ \ |、行首标点写成 _ ~ …),插件写回前会逐字验证「去掉后渲染完全一样」再去掉,a*b*c、行首 - 这类真在当语法用的原样保留;$A_i$ 这种成对转义的美元符号会还原成内联公式($A_i$`),题目 / 知识点弹窗与编辑器抽屉走同一套判官 |
| 公式与流程图 | 正文里直接写 LaTeX 与 mermaid,本地渲染;语法写错只在原地报错,不弹引擎那层关不掉的遮罩 |
| 输入助手 | 44 条公式模板 + 8 条结构 / 笔记 / 题目模板(模板本身就是两个可编辑的 markdown 文件),工具栏按钮 + 快捷键,外加一套自己实现的编辑键 |
| 配色与主题 | 插件配色(默认,自带 15 套皮肤:科技蓝 + 14 套 Material Design)/ 跟随主题(直接吃当前 DeepSeek Harness 主题的底色 / 文字 / 强调色,不覆盖平台主题)两档;还能让每张卡各用一色;整体字号 85% ~ 160%;全屏专注模式 |
| Git | ⎇ Git 提交 一次提交整个学习库(提交前自动 git pull --no-rebase),可 ✦ AI 生成 提交信息;绝不 reset / checkout / rebase,冲突了就停下来交给你 |
安装
需要 DeepSeek Harness(桌面版或 Web 版;开发环境为 0.2.0-rc.2)。插件没有 npm 依赖、也不需要构建 —— KaTeX / mermaid / zt-react-milkdown 都在 plugin/dsh-kp-notes/vendor/ 里随包分发。
从本仓库安装(现在就能用):先把仓库克隆到本地,然后
dsh plugin --profile <profile 名> add link:/绝对路径/rk-study/plugin/dsh-kp-notes
从 GitHub 直装(不用克隆):
dsh plugin --profile <profile 名> add github:UnitySirx/dsh-kp-notes#path:plugin/dsh-kp-notes
pnpm ≥10 会提示
The git-hosted package … has to be built but the build scripts were ignored—— 这个包没有构建步骤(没声明prepare/build,也没装任何依赖),忽略这条警告即可。
从 npm 安装(包尚未发布;发布后把 link: 那段换成包名即可):
dsh plugin --profile <profile 名> add dsh-kp-notes
装好后在「插件」面板里把 dsh-kp-notes(内部名 rk-study)打开 —— plugin/dsh-kp-notes/package.json 里声明了 dsh.bundle.patch(Node 半)与 dsh.client(浏览器半),宿主会把两半一起挂上;入口是左侧边栏面板列表里的「知识点笔记」(英文界面下是 Knowledge Notes,图标 plugin/dsh-kp-notes/icon.svg)。三种装法装完都一样。
改完代码怎么生效:改 plugin/dsh-kp-notes/client/ 或 client.js 后刷新页面(⌘R)即可;改 host.js / lib/ 下任何文件时把 ?v=N +1(碰了客户端模块的行为就再把 client.js 的 MODULE_VERSION +1),见「五、插件实现」开头。cordis.patch.yml 现在是发布形态的包名 dsh-kp-notes(没有 ?entry=N 缓存戳),关开 bundle 不会靠换 URL 重新 import 宿主模块;但实测保存后宿主半仍会被重新 import,隔几秒就生效(撞 ?v=N 是必须的:模块按完整 URL 缓存,只改文件内容不换 URL 会一直用缓存里的旧模块)。客户端那半关开一次 bundle(或改个按钮文案)就生效;不放心时重启 DeepSeek Harness 最稳。
快速开始
- 准备一个目录当画布(放插件目录外面,例如
~/Notes/系统架构师/)。面板右上角的+ 新建学习画布会帮你建好notes/与questions/两个子目录,也可以自己手工建。- 想一次管多张画布:先建一个空目录(比如
~/Notes/),用面板右上的⌂ 根目录把它设成根目录(列表里就是它下面那一层子目录),之后新建的画布都挂在它下面。
- 想一次管多张画布:先建一个空目录(比如
- 写笔记:在
notes/下按约定建目录与 markdown 文件(见下一节),或者直接在面板里点+ 新建章节/+ 新建小节/+ 新建知识点/+ 添加题目。 - 看画布:章节卡 → 点小节 → 右侧知识点卡片墙 → 点知识点看说明与题目;答案默认遮挡,点一下展开。
笔记目录(
notes//questions/)里不写任何配置文件:字号、配色、视野这类状态只存在浏览器、学习库那一级的.config/rk-study.json,以及每张画布自己目录下那份.config/rk-study.json(只记这张画布的视野与字号),笔记文件永远是纯粹的 markdown。
目录
下面开始,就是把「磁盘上的文件」和「画布上的卡片」一一对应起来的最完整说明。
一、目录结构与命名约定
| 文件系统 | 插件里 |
|---|---|
notes/ 下的一个目录 | 一个大章节(画布上的一张章节卡);目录名 = 章节名,卡上点「改名」即可重命名(目录本身跟着改) |
notes/<章>/<NN>-标题.md | 该章的第 NN 个小节(只放标题 + 小节导读) |
notes/<章>/<NN>-<MM>-标题.md | 小节 NN 下的第 MM 个知识点(一个文件 = 一张知识点卡片) |
questions/<章>/<同名文件>.md | 上面那个知识点的题目(一个文件可放多道,每道题一个 ## 题目 N) |
<画布根目录>/ ← 例如 ~/Notes/系统架构师/
├─ notes/
│ ├─ 01-架构设计基础/
│ │ ├─ 01-架构风格与架构视图.md ← 小节 01(标题 + 导读)
│ │ ├─ 01-01-五大类架构风格.md ← 知识点(只有说明,不写题)
│ │ ├─ 01-02-易混淆对比.md ← 知识点
│ │ └─ 02-架构评估方法与ATAM.md ← 小节 02
│ └─ 02-质量属性与战术/…
└─ questions/
└─ 01-架构设计基础/
├─ 01-01-五大类架构风格.md ← 知识点 01-01 的题目
└─ 02-04-敏感点-权衡点-风险点.md ← 知识点 02-04 的题目
挂在学习库下的画布(父目录里还有别的画布,例如
~/Notes/系统架构师/)自己不会有.templates/:模板统一放学习库根目录那一份。它自己的目录下会有一份.config/,但只放这张画布自己的东西 —— 视野与字号(rk-study.json)、发号计数器(同一个文件里的seq)与号池(rk-study-uids.json);画布清单、库级字号配色这类库级状态仍在库根目录那份里(见下面「学习库」与「设置存在哪」)。只有自带根目录的画布(不挂在任何学习库下)才会在它自己目录下建.templates/。
notes/、questions/ 是相对画布根目录的路径(不是相对插件仓库)。插件仓库里没有这两个目录:一个画布一个根目录,各自独立、也各自独立 git。
配对规则:题目文件与知识点文件路径同名(notes/X/Y.md ⇄ questions/X/Y.md)即自动配对;也可以在题目文件的 frontmatter 里写 point: notes/X/Y.md 显式指定。配不上的题目文件不会丢,会挂到对应章节目录下并在目录 JSON 的 orphanQuestions 里列出。
命名规则(不满足时会退化,不会丢文件):
- 第一个数字 = 小节号,决定这个文件挂在哪一小节下;第二个数字 = 小节内的知识点序号。
- 兼容旧写法:文件名里出现
例题/题目/案例/错题/真题/习题/已做等片段也判为题目文件,所以历史文件不用改名。 - 都不是、且没有第二个数字 → 判为小节文件(自己一行)。
- 想在 frontmatter 里显式指定,可写
type: point(知识点)/type: question(题目)/type: section(小节);不写就按文件名推断。 - 章节名 = 目录名去掉
01-这类序号前缀;章节序号也来自目录名,所以「第 2 章 · 质量属性与战术」= 目录notes/02-质量属性与战术/。像本仓库README.md这样直接放在画布根目录下的文件,属于「根目录」那一章,章名取其中第一个小节的标题。 - 没有对应小节文件的独立知识点不会消失:会作为一个小节单独列出来。
二、三类文件长什么样
小节文件(notes/<章>/<NN>-标题.md)
---
title: 架构评估方法与 ATAM
type: section
order: 2
tags: [ATAM, 架构评估]
---
# 架构评估方法与 ATAM
考点集中区:SAAM / ATAM / CBAM / SAEM 的区别、ATAM 的四阶段九步骤、效用树的三层结构。
小节文件只放标题 + 导读,内容都拆到知识点文件里。
知识点文件(notes/<章>/<NN>-<MM>-标题.md)
---
title: 敏感点 / 权衡点 / 风险点
type: point
order: 4
tags: [ATAM, 质量属性]
---
# 敏感点 / 权衡点 / 风险点
| 类型 | 含义 | 影响范围 |
| --- | --- | --- |
| 敏感点 | 为实现某一个质量属性,对某架构决策特别敏感的点 | 影响 1 个属性 |
- 按目录定性质:
notes/下的内容一律是知识点,questions/下的内容一律是题目 —— 不会从笔记里「抽」出题目,也不会把笔记切成子卡片。 - 知识点卡片上显示的是提取出来的说明文字,不是 markdown 源码;表格会转成文字摘要。
- 一个知识点文件(
# 标题或## 标题开头)就是一张知识点卡片:里面出现的所有标题(##、###、####……)都留在正文里,不会再拆成画布上的子卡片。 - 正文里的标题按级别分大小与颜色:
#20px/800 渐变白蓝、##17px/700 + 左侧 3px 色条、###15px/700 + 2px 色条、####13.6px、#####12.8px + 字距、######12px + 字距(色条颜色 = 标题色)。2~6 级各给一个鲜艳的颜色、互不撞色:青 → 紫 → 粉 → 琥珀 → 绿,默认皮肤写死在--rk-t2 … --rk-t6五个变量里;换「配色」时按主色色相旋转+0 / +62 / +128 / +200 / +268重算这五个值,所以既鲜艳又跟整体配色协调。正文、实时预览、详情页走同一套;小知识点的标题、左侧色条、序号块也按它自己的级别取同一组颜色(另有 16 / 14.8 / 13.4 / 12.8 / 12.3px 五档字号),右侧角标写着它是几级。 - 但详情页会把正文里的小标题显示成小知识点:取正文中最浅的那一级标题(只有
###就按###,同时有##与###就按##)当分界,标题前的内容是引子,每个标题带自己下面那段正文,上面加一行 小知识点 N;编号行(1.2.…)不算分界,留在它所属的小知识点里。代码块里的#不会被当成标题。写笔记时想在详情里分几小点,就用小标题分段。 - 知识点详情页里的题目来自
questions/目录(用题目文件 frontmatter 的point:指到这个知识点),或者你在详情页点 + 添加题目 加进去的;两者都会写进questions/。
题目文件(questions/<章>/<同名文件>.md)
---
title: 敏感点 / 权衡点 / 风险点 · 题目
type: question
order: 4
tags: [ATAM, 质量属性]
point: notes/01-架构设计基础/02-04-敏感点-权衡点-风险点.md
---
# 敏感点 / 权衡点 / 风险点 · 题目
## 题目 1
在 ATAM 评估中,若某个架构决策同时影响到系统的安全性和性能,则该决策点被称为( )。
A. 敏感点
B. 权衡点
C. 风险点
D. 非风险点
**答案**:B
**解析**:敏感点影响一个质量属性,权衡点影响多个质量属性。
## 题目 2
同一道题里还可以带 `### 问题 1`、`### 问题 2` 这样的子问题(案例题常用)。
**答案**:……
- 一个题目文件可以放任意多道题,每道题一个
## 题目 N,卡片样式与遮挡规则完全一样 —— 没有「例题 / 已做题目」之分,一律叫题目。 - 题目标题写在几级都可以(
## 题目 1最常用,# 题目 1/### 题目 1也认),检测与删除用的是同一套判定:界面上一道题,就一定能删掉。文件里没有任何题目标题时不会凭空造题,界面显示「题目 0 · 这个知识点还没有题目」,点+ 添加题目补一道标准的即可。 - 题目只出现在知识点详情页里,紧跟在知识点说明下面 —— 题目只属于知识点,不属于小节。
- 题目下面自带
### 问题 1这类子标题也可以,题干与答案会被正确切开。 - 两种题型自动识别,也决定
+ 添加题目打开哪个表单:- 选择题 —— 题干下面有
A. …到D. …若干行选项,**答案**:B只写字母,**解析**:…另起一行; - 案例题 —— 没有选项行,
**答案**:后面就是答案正文(可以带### 问题 N子标题、列表、表格)。 - 表单只管这些约定好的位置(题干 / 选项 / 答案行 / 解析行),答案正文里的 markdown 原样保留,不会被插件改写。
- 选择题 —— 题干下面有
答案遮挡
- 答案写在题干后面即可,
**答案**:B、答案:B、或另起一个## 答案小节都认。 - 从「答案」那一行往后(含解析)默认折叠成
▸ 答案已遮挡 · 点击展开,点一下展开。 - 知识点详情页的**「题目」那一行**(和
+ 添加题目并排)有「显示全部答案」,一次展开本知识点的全部答案。 - 选择题的 A/B/C/D 选项自动识别、按行排版。
公式与流程图
正文(知识点说明、答案、导读……)里可以直接写 LaTeX 公式和 mermaid 流程图,插件会渲染出来:

| 写法 | 渲染成 |
|---|---|
$A = \frac{MTBF}{MTBF + MTTR}$ | 行内公式 |
\(x^2 + y^2 = z^2\) | 行内公式(另一种写法) |
单独成段的 $$ … $$ | 居中的块级公式 |
```math / ```latex 围栏 | 块级公式 |
```mermaid 围栏 | 流程图 / 时序图 / 状态图等 mermaid 图 |
- 渲染引擎(KaTeX 0.16.47 + mermaid 11.17.2)作为静态资源随插件分发(
plugin/dsh-kp-notes/vendor/,约 7.1 MB:KaTeX 292 KB + mermaid 3.4 MB + zt-milkdown 3.1 MB),由插件自己的路由/rk-study/vendor/*直出;页面第一次遇到公式/图时才去取,之后走浏览器缓存 —— 不联网、不依赖 CDN、不需要构建。 - 只认识「像公式的」
$…$:价格 $100, 折扣 $20、$5 到 $10这类货币写法会原样显示,不会被当成公式吃掉。 - 引擎取不到时不会白屏:公式退回显示原始
$…$,流程图显示红框提示 + 原始代码,正文照常可读。 - 语法写错时不弹 mermaid 自带的那个红框(它是引擎直接挂到页面
body上的,React 管不到,会一直挡着关不掉):只在原地显示「流程图渲染失败:Parse error on line N」加出错的源码,改好语法自动重画。做法是mermaid.initialize({ suppressErrorRendering: true })让引擎先自己清理再抛错,再加一层按 id 兜底删临时节点的保险。 - 边写边看:编辑器抽屉默认是富文本所见即所得(Milkdown,见下条),点右上角「源码」切回「markdown 源码 + 实时预览」的左右分栏;编辑知识点 / 题目的弹窗仍是左右分栏,右边跟着左边实时渲染 —— 写公式和流程图不用先保存再刷新。
- YAML 头不会被富文本碰到:笔记文件开头那段
---…---元数据(title/type/tags/order)只在源码模式里显示和编辑,富文本里看不到也改不到,保存时再原样拼回去(抽屉头会亮一个「YAML ✓」小标)—— 所以打开富文本改正文、随手保存,不会把元数据变成一段普通文字。富文本回吐的正文还会顺手清掉空段落留下的<br />和行尾空格,头后面固定留一个空行。 - 编辑器也是随插件分发的静态资源:
plugin/dsh-kp-notes/vendor/zt-milkdown/里放的是zt-react-milkdown@0.1.32(MIT,Milkdown 内核;zt-milkdown.js1.6 MB +zt-milkdown.css1.6 MB,含内联字体)。加载方式是自己写的小加载器client/milkdown.js:注入样式 →fetch回 CJS 文本 → 用new Function('require','module','exports', code)执行,require只映射react/react-dom/react-dom/client/react/jsx-runtime(都用宿主那一份,react-dom/server给个空壳)—— 所以不需要 npm 安装、不联网、不打包。编辑器脚本取不到(文件缺失、浏览器拦截)时自动停在源码模式,不会白屏。
三、直接在插件里写

| 位置 | 操作 | 结果 |
|---|---|---|
| 画布右上角 | + 新建章节 | 弹出输入框,只输入章节名;创建目录 <父目录>/<NN>-<章节名>/(不生成任何文件,空章节也会立刻出现在画布上) |
| 章节卡右上角 | 改名 | 改章节名 = 改目录名(序号前缀保留);questions/ 下的同名题目目录一起改名,题目文件里的 point: 前缀同步更新 |
| 章节卡底部 | + 新建小节 | 创建 <章节目录>/<NN>-<标题>.md(type: section + 导读) |
| 小节页右上角 | + 新建知识点 | 创建 <章节目录>/<NN>-<MM>-<标题>.md(type: point + 说明骨架) |
| 知识点页 · 题目分组 | + 添加题目 | 弹出可视化表单(选择题 / 案例题两种),不用写 markdown |
| 每道题右上角 | 编辑此题 | 同一个表单,字段已经从文件里解析出来,改完保存即写回该题目文件 |
| 题目表单 · 选择题 | 题干 / 选项 A–D / 正确答案(点圆点)/ 解析 | 选项可增删(+ 加选项、每行 删除);留空的选项保存时丢掉,字母按顺序重排 |
| 题目表单 · 案例题 | 题干 + 答案正文(markdown) | 多问案例题就按「问题 1 / 答案 / 问题 2 / 答案」往下写,原样保存、原样读回 |
| 题目表单 / 知识点表单 | 左栏写、右栏实时预览 | 两个弹窗都是大号左右分栏:左边是表单,右边实时渲染 —— 题干/正文里的 $公式$ 与 ```mermaid 流程图边写边出,选项与答案按写入文件的样子预览。里面的 markdown 字段(题干 / 答案 / 解析 / 正文)默认就是 Milkdown 所见即所得(/ 斜杠菜单、选区工具条都在),标题行右侧 Markdown / 源码 一键切换(源码模式就是 markdown 源码框 + 输入助手工具栏);知识点弹窗的 Markdown 模式不占实时预览(源码框整栏通铺、编辑器更宽),切到 源码 才并排显示实时预览 |
| 题目表单底部 | 保存并继续添加(新建时) | 存下这一题、清空表单、继续录下一题 |
| 知识点页 | 编辑 | 弹出可视化表单:标题 + 标签 + 内容(正文 markdown),不用面对 frontmatter;改标题会同时改写 frontmatter 的 title: 与正文里的 # 标题,标签只在「整篇文件就是一个知识点」时出现(多知识点的小节文件里标签属于文件,不在单个知识点上) |
| 小节页 | 编辑 | 抽屉里直接改该小节文件的 markdown |
| 编辑器抽屉 | 插入题目模板(只在题目文件里) | 在当前光标处插入 ## 题目 N 骨架;其它文件没有这个按钮 |
| 编辑器抽屉 | Markdown / 源码 | 默认是所见即所得(Milkdown:标题、加粗、列表、表格、公式、代码块写的时候就是最终样子;/ 斜杠菜单、选区工具条、⌘B / ⌘I 等由编辑器自己管);点「源码」切回「markdown 源码 + 实时预览」左右分栏、抽屉收窄。编辑器自带查找替换面板、图片(链接)弹窗、表格操作、代码块语言选择;标题按层级上色(# 亮白、## 青、### 紫、#### 粉、##### 黄、###### 绿 —— 取的是「配色」那套 --rk-t2..t6,换配色跟着变,和预览 / 思维导图一致,## / ### 还带左侧色条);笔记的 YAML 头在富文本里不显示(抽屉头有「YAML ✓」),要改就在源码模式里改。脚本取不到时直接停在源码模式 |
| 新建小节 / 新建知识点 | 保存 | 抽屉里只有「目录(只读)+ 标题」两个输入框,回车或点保存即创建,随即关掉抽屉(不会自动打开编辑器);新建后可在卡片上点 编辑 再写内容 |
| 编辑器抽屉(源码时)/ 题目表单 / 知识点表单(源码时) | 输入框上方那一排小按钮 | markdown 输入助手:加粗、斜体、行内公式、块级公式、表格、流程图、列表、引用、链接、代码,最右边两个下拉 —— 公式 ▾(公式 / 符号两组)与 模板 ▾(结构 / 笔记 / 题目),点一条插进正文(详见下面「输入助手」) |
| 同上 | 快捷键 | ⌘B 粗体、⌘I 斜体、⌘M 行内公式、⇧⌘M 块级公式、⌘K 链接、⌘E 行内代码、Tab / ⇧Tab 整行缩进反缩进、⌘S 保存;另有一组插件自己实现的编辑键:⌘/Ctrl + Z 撤销、⇧⌘Z / Ctrl + Y 重做、⌘/Ctrl + C 复制、⌘/Ctrl + X 剪切、Ctrl + V 粘贴(⌘V 仍交给系统) |
三个弹窗(新建 / 改名章节、题目表单、知识点表单)只有点「取消」或按
Esc才关闭:点到弹窗外面的灰底不会误关,正在写的内容不会因为手滑点空而丢掉。弹窗里⌘S/Ctrl+S就是保存。
插入图片(落盘,不写 Base64)
编辑器(富文本 / 题目表单 / 知识点表单)里插的图不再写成一长串 Base64,而是交给插件落成真文件:
- 放哪:这篇笔记所属小节旁边,统一收在正文目录下的
.media/里 ——.media/<小节编号>.assestfiles/(例:notes/01-计算机硬件/.media/s0001.assestfiles/)。编号就是插件给每个小节发的s0001号(见「编号(uid)」一节)。中间那层.media/是为了让正文目录里只多一个素材目录,不和.md混在一起;点开头这一条还顺手解决了「素材目录被当成一章」—— 扫描本来就不看点开头的目录,它连判据都不用走。 - 叫什么:
<小节编号>-<序号>.<扩展名>——s0001-1.png、s0001-2.png…… 序号自动往后排,同名不会互相覆盖。 - 笔记里写什么:只写相对路径,例如
;在题目文件(questions/那一侧)里插的图落到同一个素材目录,路径写成../../notes/01-计算机硬件/.media/s0001.assestfiles/s0001-1.png—— 因为相对的是这篇笔记自己的位置。老布局里写的media/…也照旧认得到(认图按后缀找,见「路由」一节),所以旧笔记不改也能显示。 - 显示:卡片 / 思维导图 / 实时预览 / 编辑器都会把这段相对路径换成
/rk-study/media?path=…去取字节,所以写进去立刻能看到图;插件之外(别的 markdown 编辑器、导出、搬走整个库)也照样认,因为它就是普通相对路径。渲染器(client/md.js)把![]()渲染成真的<img>,相对路径→可显示地址的换算挂在面板根节点上统一的那个watchImages里(新插进来的节点也会被就地改写),所以源码模式的实时预览、卡片、详情里都能看到图。 - 入口是 ⌘⇧I:vendor(zt-milkdown)的斜杠菜单在打进 vendor 的这份 build 里没注册语言 / 菜单 spec,
/菜单是关的;插图只有「⌘⇧I 上传文件」和「粘贴」两个口子。 - 插图是两步(选文件 → 插入):选文件那一步一个字节都不写盘 ——
media.upload()同步交回一条blob:本地引用(URL.createObjectURL(file),浏览器自己的临时地址,弹窗拿它new Image()探得到、预览也看得见),文件对象先记在内存里;只有点了「插入图片」(或直接粘贴)之后,图片真进了正文,onChange交回的 markdown 里带着那条blob:时,才由media.settleText()把字节交给 host 落到.media/<小节编号>.assestfiles/,落完把正文里那段换成相对路径。选完就取消、或重新选一张,都不留孤儿图。 imageUpload上有两个必传项(vendor 的默认值都不合用):maxFileSize(默认只让传 5 MB,超限只console.error,粘贴路连提示都没有;现在给 16 MB —— host 那侧/rk-study/media收 24 MB 的 JSON body,base64 要膨胀 4/3)与allowedProtocols: ['blob:'](vendor 在 image 节点上装了协议白名单,只放行http:/https:/mailto:/tel:/data:,名单外的地址会被换成空串 ⇒ 图片节点被 markdown 序列化器静默跳过:文档里看得见图、markdown 里一个字都没有、onChange也永远不触发,落盘那步就永远等不到。vendor 允许调用方追加协议,所以把blob:补进去)。- 写盘收口:
media.toMarkdownSrc(text)把/rk-study/media?…显示地址与还没落盘的blob:换成相对路径,编辑器 / 弹窗的清洗函数各调一次,编辑器保存前再对整段跑一次 strict 收口(源码框里手打的地址也跑不掉;落盘失败时 strict 收口直接把那段图去掉,绝不把blob:写进文件)。 - 删图不删文件:从正文里删掉图片只删 markdown 里那一行,盘上的文件留着(想删就去那个目录删)。名字带序号、只增不改,所以图片可以放心长缓存。
- 支持的格式:
png/jpg/jpeg/gif/webp/avif/bmp/svg,单张上限 24 MB。 - 新建时例外:新建小节 / 知识点时文件还没落盘,此时插图仍是编辑器内置的 Base64;保存之后再插图就走上面这套了(把之前的 Base64 图删掉重插一次即可)。
- 素材目录不会被扫描成章节:新布局的
.media/是点开头的,扫描根本不看它;老布局的media/仍走判据 ——「里面除点开头的东西外全是*.assestfiles目录」(isMediaHome,所以 Finder 顺手丢进来的.DS_Store不会让它变成一章;你真建一个叫media的章节、里面有.md,它照样是章节)。两种布局都不会在画布上多出奇怪的卡片。 - 老布局的
media/会自己搬过来:往一个老小节里第一次插图时,插件顺手把<小节目录>/media/整层搬成<小节目录>/.media/(migrateLegacyMedia),并把notes/、questions/两侧正文里的media/<小节编号>.assestfiles/改成.media/…(rewriteLegacyMediaRefs,只动这一层,字节一个不碰)。目标位置已经存在同名素材目录(那个小节在新布局下已经插过图)时改成逐个文件并过去,同名文件留在原地不动 —— 名字一样就是同一张图,正文里那段路径改成.media/之后照旧点得开。搬完空掉的旧目录摘掉(只删空目录:里面还剩 Finder 的.DS_Store这类文件就留着,扫描反正不看它)。搬不动也不拦着插图。 - 删笔记时素材跟着走:删小节(含连带删掉的知识点 / 题目)⇒ 它那份
.media/<小节编号>.assestfiles/整份跟着进.remove/<桶>/;只删某个知识点 / 题目 ⇒ 只搬「这篇笔记引用、而别处已经不再引用」的那几张(别的笔记还在用的留着),搬完空掉的素材目录顺手清掉(老布局的media/一样认)。详见「删除」一节。
输入助手(快捷键 + 公式模板)
每个 markdown 源码框(编辑器抽屉切到「源码」、题目 / 知识点表单切到「源码」)上方都有一排小按钮,写笔记时不用手打符号 —— 它按光标位置插入,所以富文本模式里不显示(富文本用 / 菜单和选区工具条):
| 按钮 | 插入什么 | 快捷键 |
|---|---|---|
| B / I | 选中文字加粗 / 斜体(没选中就插占位符,光标停在中间) | ⌘B / ⌘I |
| ∑ | 行内公式 $…$ | ⌘M |
| ∫ | 块级公式 $$…$$(单独一段) | ⇧⌘M |
| 表格 | 3×3 markdown 表格骨架 | — |
| 流程图 | ```mermaid + flowchart TD 骨架 | — |
| 列表 / 引用 / 链接 / 代码 | 无序清单 / > 引用 / [文字](url) / 行内代码 | — |
| 公式 ▾ | 公式库菜单,四组:公式(行内 / 块级 / 分式 / 求和 / 积分 / 极限 / 根号 / 上标 / 下标 / 方程组 / 矩阵)、希腊字母、关系与运算符、箭头与集合 —— 一条就是一条,点哪个插哪个 | — |
| 模板 ▾ | 其余模板菜单:结构 / 笔记 / 题目 三组,点一条插进正文 | — |
编辑键(撤销 / 重做 / 复制 / 剪切 / 粘贴)也由插件自己实现,因为有些环境(Tauri 打包后的 WKWebView)不把 ⌘C / ⌘V / ⌘X / ⌘Z 交给网页、应用自己的原生菜单又不接管,这四个键就整个失效:
⌘/Ctrl + Z撤销、⇧⌘Z(或Ctrl + Y)重做 —— 走document.execCommand('undo' / 'redo'),用的是浏览器自己那份撤销栈,所以打字、粘贴、输入法上屏都能一步步退。⌘/Ctrl + C复制选中、⌘/Ctrl + X剪切选中 —— 走execCommand('copy' / 'cut'):剪贴板、撤销栈、input事件都和原生一致(受控 textarea 也能同步到 React 状态)。Ctrl + V粘贴 ——execCommand('paste')被浏览器禁掉,只能读navigator.clipboard.readText(),所以只有 Ctrl+V 走插件;⌘V仍交给系统(避免 WKWebView 的权限弹框把本来好用的⌘V弄坏)。- 三条都只在
execCommand返回true时才preventDefault:命令不被支持就原样交回系统,本来能用的环境不受影响。
模板放在两个单独的 markdown 文件里:.templates/公式模板.md(公式 ▾ 菜单)和 .templates/Markdown模板.md(模板 ▾ 菜单)—— 一个文件对应一个菜单,各改各的。模板统一放在学习库根目录的 .templates/(库里所有画布共用这一份,各画布自己不再存);只有自带根目录、不挂在学习库下的画布才放在它自己的 .templates/。老画布原来放在 notes/.templates/ 的继续认(那里有文件就用那里的)。
- 目录名以
.开头,所以它们不会出现在画布上(扫描时跳过点开头的目录),也不会被当成一章。它们在学习库根目录(不在任何画布里,也不在notes/里);面板的⎇ Git 提交是在一级画布(根节点)上按整个学习库提交的(git add -A),所以它们会一起进版本库 —— 除非你自己的.gitignore把它们排除掉。 - 文件格式只有三条规则:
##是分组名,###是模板名,模板名下面紧跟的第一个代码块里的内容就是插入到正文的文本;模板内容本身想带代码块(比如流程图),外面用四个反引号包起来。 - 一条只放一个公式或一个符号:菜单里每条的名字都带着它的 LaTeX 命令(
积分 \int、α \alpha、≤ \le),鼠标停在上面会显示要插入的完整代码,点下去只插这一条 —— 不会一次插一串。 - 想加自己的公式就写进
公式模板.md(照## 公式/## 希腊字母/## 关系与运算符/## 箭头与集合分组放,一条一个###),想加结构骨架就写进Markdown模板.md;保存后回到编辑器点对应菜单里的 重新读取 就能看到(工具栏每次打开时也会重新读一次)。要加第三类菜单得改代码(宿主lib/templates.js的TEMPLATE_FILES)。 - 加一条自己的公式:在文件里照着写
### 名字+ 代码块并保存,回到编辑器点 模板 ▾ → 重新读取 就能看到(工具栏每次打开时也会重新读一次)。 - 某个文件被删了也没关系:那个菜单底部会出现 建模板文件,点一下按内置默认库重新生成它(公式模板 44 条 —— 公式 11 条:行内 / 块级 / 分式 / 求和 / 积分 / 极限 / 根号 / 上标 / 下标 / 方程组 / 矩阵,希腊字母 17 条,关系与运算符 8 条,箭头与集合 8 条;Markdown模板 8 条 —— 结构 4 条:表格 / 流程图 / 无序清单 / 引用块,笔记 2 条:知识点骨架 / 小节骨架,题目 2 条:选择题骨架 / 案例题骨架)。
- 编辑模板文件 在菜单底部,点它直接在抽屉编辑器里打开这个菜单对应的那个文件,改完
⌘S保存(换文件时编辑器会重新挂载,不会残留上一个文件的内容)。
删除(不真删:一律移到当前画布的 .remove/,都有二次确认)
插件里的每个「删除」都是移动:目标被 rename 到当前一级画布根目录下的 .remove/,一次删除 = 在这里新建一个日期桶目录(名字就是当天的 YYYYMMDD,例如 20261004),桶里保留原来的相对路径结构(删掉章节 notes/01-第一章,桶里就是 notes/01-第一章/ 与 questions/01-第一章/)。桶与桶互不相干,所以删两次就留两份,谁也不覆盖谁(同一个章节删两次,第二次进的是新桶,第一次那份原样还在),也不会把不同次的删除混进同一条路径(先删整个章节、之后又删重建章节里的某个知识点 —— 后者进它自己那个新桶,不会落进上次那份章节备份里)。同一天里再删一次,名字往后排成 20261004-2、20261004-3(桶名只到「天」,一天里删几次就有几个桶;文件本身的修改时间由 rename 原样保留,想知道具体几点几分看它就行)。.remove 以点开头,扫描时跳过,所以它不会出现在画布、思维导图或搜索里。要找回:在画布根目录下打开 .remove/,挑日期对得上的那个桶,把东西移回原位即可;rm -rf .remove 就等于真正清空回收站。
编号(uid):每个一级画布与章节都有一个自己的、永不复用的编号,形如 c0001(画布)/ h0007(章节)/ s0012(小节)/ p0031(知识点)。编号不写进文件名或目录名(文件怎么排、卡片长什么样一个字都没变),只当身份用:目录类实体(画布 / 章节)的号记在学习库的 .config/rk-study-uids.json 里(号池,就一张表 { "version": 1, "uids": { 绝对路径: 编号 } }),发号计数器 seq 仍跟界面设置一起放在 .config/rk-study.json,跟画布放在哪个目录、叫什么名字无关。删掉之后号不回收:把同名的章节/画布在原地重建,拿到的是新号(旧号只留在它自己的备份里),所以「先删 02 再建一个」不会让两个不同时期的东西共用一个身份。号会跟着实体走 —— 章节改名、画布改名(重命名目录)都是搬号,身份不变;章节被删除或画布被「移出列表」时,被带走的号会连同文件一起写进那个桶里的 .rk-uids.json({"at": …, "uids": {绝对路径: 编号}}),想恢复时认得回来:把目录搬回原位(被移出的画布还要在插件里重新导入一次),下次扫描就会按路径把原来那个号认回来 —— 恢复的是原身份,不是新号;同一个路径在多个桶里都留过号时,以最近一次删除留下的为准。老笔记不用迁移:第一次扫描到没有号的章节就会自动补上;小节与知识点另走一条更省事的路 —— 号就写在 markdown 的 frontmatter 里(uid: s0012),跟着文件内容走,所以改标题、挪位置、连文件名一起改都不丢。没有号的老笔记也不会被改文件:扫描时先把号按路径登记进号池(界面上立刻就有号可用),等这个文件下次被插件保存,号才写进它的 frontmatter,号池里那条按路径记的账同时摘掉(号从此跟着内容走,路径再变也不丢)。插件保存时取号的顺序是:文件里已有的 → 盘上旧文件里的(前端重建头部把 uid 弄丢也能认回来)→ 扫描时登记的号池 → 都没有才发新号;题目文件本身不发号,但每一道题有自己的号 —— 挂在它那个块的标题下一行(<!-- rk-uid: q0001 -->,渲染出来的笔记里看不见;单独一行是必须的,因为重排 ## 题目 N 会重写标题行);改题干、换选项、整段重排都不动它,删掉一道题时标记跟着那段原文一起进 .remove,号同时记进计数器(不再发放)。扫盘结果和接口里也带着号:/rk-study/notes 返回的 chapters / sections / points 每一项都有 uid 字段(章节的号在扫描时补上,小节 / 知识点直接读它文件里的),所以客户端想显示编号可以直接用。号池为什么单独一个文件:rk-study.json 从此只留界面设置(字号、配色、画布清单)与发号计数器 seq,这张随时在长、只有插件自己用得到的号表放旁边 rk-study-uids.json;老版本把 uids 混在 rk-study.json 里的那些库不用手动改 —— 插件照读老键,并在下一次发号 / 认号时把整张表写进号池文件、再把老键从 rk-study.json 里摘掉(一次自动迁移,号一个都不变)。移出列表的墓碑同样待遇:它按库里移出过的画布条数长,也住在同目录自己的文件 rk-study-removed.json 里({ "version": 1, "removed": [绝对路径] }),老版本混在 rk-study.json 里的 removed 照读、第一次写时自动搬过去并把老键摘掉。
| 位置 | 按钮 | 移到 .remove/<桶>/ 里的是什么 |
|---|---|---|
| 章节卡右上角 | 删除整章 | notes/<章>/ 整个目录 + questions/<章>/ 同名题目目录(含其下所有小节 / 知识点 / 题目文件),根目录章节不可删 |
| 小节行 | 删除小节 | 该小节文件 + 该小节下的全部知识点文件(NN-MM-*.md)+ 这些知识点在 questions/ 下的题目文件 |
| 知识点卡片底部 | 删除知识点 | 该知识点文件 + questions/<章>/<同名文件>.md(两侧同步移动) |
| 题目卡片右上角 | 删除此题 | 题目文件本身不动:被删掉的这一段 markdown 另存成 .remove/<桶>/<题目文件路径>.removed-<题号>.md(首行注释写明从哪个文件、第几题删的;同一题删两次就是两个桶里各一份,不覆盖),再重写剩下的题目 |
| 小节页 / 知识点页 / 编辑器 | 删除本文件 | 当前打开的这个文件(笔记文件同样连带移动配对题目文件) |
图也跟着走:笔记里引用的图片素材(.media/<小节编号>.assestfiles/…,老布局的 media/… 一样认)不会留在原地空着 ——
- 删小节(含它底下连带删掉的知识点 / 题目):这一份素材目录属于这个小节,整份跟着进同一个桶,桶里路径照旧(
<桶>/notes/01-第一章/.media/s0001.assestfiles/),恢复时图就在正文旁边; - 只删某个知识点 / 题目:那份素材目录是整个小节共用的,所以只搬「这篇笔记引用、而画布里别的笔记已经不再引用」的那几个文件 —— 还被别处引用的留着(判断就是拿文件名在
notes/与questions/的 markdown 正文里再找一遍,扫不完就一律留着,宁可少搬不多搬); - 搬走之后空掉的
<小节编号>.assestfiles/与它上面那层.media/(老布局是media/)顺手清掉(只删空目录)。 - 删除接口的返回里因此多一个
media字段(这次跟着搬走的素材路径),界面用不到,排查时看得见。
点一次按钮会在原处浮出「确认删除? / 取消 / 删除」,再点「删除」才真正执行。确认条是绝对定位浮层,不占布局 —— 所以列表里的按钮不会因为多出这行字而换行或位移,同一个位置连点两下第二次点到的仍然是「删除」(早先的写法会把按钮挤到下一行、让第二次点空或误点「取消」)。执行后自动回到章节图并刷新,提示条写「已移到 .remove/<桶>/ · <路径>」,并写明连带处理了几个关联文件。
配对规则:
notes/X/Y.md(知识点)与questions/X/Y.md(该知识点的题目)是同一实体的两侧,动任何一侧都会连带处理另一侧;直接操作题目文件(API 的{action:'delete', path:'questions/…'})只动题目文件本身,知识点文件保留。 画布自己也有同类操作:一级画布卡上的 移出列表 是把这个画布目录移到它同层的.remove/里(见下一章)。
回收站(工具条上的 ♻ 回收站:把删掉的东西搬回来)
上面那些「删除」都不真删,东西都在 .remove/<日期桶>/ 里 —— 工具条上的 ♻ 回收站 就是它的界面,而且只有两层有它:
- 根画布(还没进任何画布):列的是被「移出列表」的整只画布。画布目录躺在它上一层目录的
.remove里(学习画布可以散在不同目录),所以这层不是一个目录一只,而是把当前根、它上一层、以及画布列表里每张画布的目录各自上一层都算上(listRootBins),把这些.remove里的桶合起来按日期排;当来源不止一只目录时,桶头会多一行来自 <目录>,免得两个目录下的同名桶看混。恢复时按这条记录自己那只桶所属的目录落回原位(号也认那只目录的号池);恢复好的画布还会自动从「移出列表」的墓碑里放出来、重新登记进画布列表。 - 一级画布(走进某张画布之后):只看这张画布自己的
.remove,并把里面的记录按章并成一条(listChapterBin)——01-第一章一条,点↩ 恢复整章就把这一章(含里面被删过的小节 / 知识点 / 题目片段)整段合并回去(restoreChapter)。理由是你删东西时想找回来的通常是「那一章」本身,而不是散在几个日期桶里的同几个文件;同一章在不同日期删过几次,也都并到这一条上。
面板里每一条显示它原来的路径(按章聚合时就是章名)和它自己的编号(h0007 / s0012 / p0031…),右边一个 ↩ 恢复。恢复就是把它 rename 回原位,编号跟着回去(恢复的是原身份,不是新号)。
题目是内容级删除,桶里存的是一段片段(questions/01-第一章/01-01-知识点.md.removed-2.md,带一行注释记着它从哪个文件、什么时候删的)。题目仓库 questions/<章>/ 与笔记目录 notes/<章>/ 是平行结构,所以在面板上它们并进同一章,点「恢复整章」时一起搬回来;搬回来只是把这个片段文件放回原位旁边(原文件不动),要回到题库里还得自己把内容贴回知识点文件 —— 插件不替你把题目插回去,免得序号 / 编号对不上。片段也带着它那道题的号,面板上看得见 q0007。
原位已经有同名的文件时一律拒绝,绝不覆盖(提示「原位已经有同名的东西,先给它改名或删掉再恢复」),桶里的东西原样不动。按章恢复(以及根画布那层恢复整只画布)走的是合并:原位缺什么就搬回什么,原位已经有的目录往下钻着补,只有原位存在同名文件时才跳过(跳过几条会写在提示里)。恢复干净之后空掉的桶会自己从回收站里消失(.remove 里就剩一个 .rk-uids.json 的话,连空掉的 .remove 一并收掉)。要彻底不要了,rm -rf .remove 就是清空回收站。
老版本留下的 .remove(2025 年那阵子)也认得:那会儿的写法是 .remove/<那条记录当时的相对路径>(例如 .remove/senior-software-architect-review、.remove/notes/01-第一章),目录自己就是被搬走的那条记录,没有日期桶这一层。现在它会在面板上单独归成一只 旧格式 分组(桶名是空的,日期那栏显示「旧格式」),里面的每条记录跟新格式一样能 ↩ 恢复;原位还在的那一层只当路径容器(往下钻着找真正被搬走的那些),所以一整个目录不会因为被误当成桶而拆成一堆假条目。老格式没有「桶」可以整桶合并,所以那一组上的 ↩ 整桶恢复 是一条一条搬回去。
四、画布操作
章节图是一块可以自由平移、缩放的画布:所有章节卡排成一行、不换行(顺序就是章节顺序),所以「复位」/双击空白会把这一整行缩放到刚好铺满可视区;当前视野同时记两处 —— 浏览器本地(
localStorage的rk-canvas:<工作区路径>,刷新后立刻保持)与配置文件:一级画布的视野写进<库>/.config/rk-study.json的zoom.roots,某张画布的视野写进它自己的<画布>/.config/rk-study.json的zoom(键就是这张画布的绝对路径)。换浏览器 / 换机器都回到同一处;画布那份还会跟着画布目录一起被拷到别处(里面只有一条视野时,换个路径也认得出它)。视图缩放是纯视觉的:
.rk-plane上用transform: translate(x, y) scale(s),排版完全不动 —— 同一篇正文在任何缩放下,换行位置和节点高度都逐字节一样(实测 4 档缩放一致)。曾经为了「文字更锐利」改用过 CSSzoom,但zoom会让浏览器按缩放后的字号重新排版,盒子边缘还要向像素栅格取整(缩放越小取整误差越大,1/zoom 个布局像素)⇒ 宽度抖动几个像素 ⇒ 长段落偶尔多折一行、代码块的横向滚动条槽变高 ⇒ 越缩小节点越高。整数设备像素比(Retina dpr 2)下transform: scale的静态清晰度与zoom目视无差别,所以缩小时选排版稳定。但小数设备像素比(系统「显示缩放」1.5 倍之类)下transform的位图重采样会让文字发虚 —— 实测 dpr 1.5、放大 2 倍时zoom的笔画明显更实(1:1 裁剪对比/tmp/rk186-A-transform.pngvs/tmp/rk186-B-zoom.png)⇒ 导图的平面在放大到 1 倍以上时改用 CSSzoom(一屏只有一片正文,重排可以接受);1 倍及以下仍用transform,缩小时的排版稳定不变。画布用同一套规则(放大到 1 倍以上也改用zoom,实测 1.146→2.262 时卡片宽度 428→490→561→643→736→845=374×缩放比、间距 18→22→26→29→34→37=16×缩放比,不重叠不跳变;zoom不影响卡片的布局宽度,只让盒内文字按新字号重排,画布「量一次真实卡片高度」的逻辑会自动跟上)。平移量仍按设备像素对齐,画布只在平移的那一瞬间提升为合成层(will-change用完即撤)。在这个基础上再提清晰度(都不改变排版尺寸,实测四档缩放下节点/段落尺寸逐字节一致):缩放过的平面用
text-rendering: geometricPrecision渲染(不做整数栅格取整,笔画更准);导图正文(展开后会缩到 9px 上下,中文 Regular 笔画太细显糊)用 Medium 字重,代码块与行内代码保持等宽常规字重(合成加粗反而更糊)。画布里的卡片/小节行不做任何 hover 重绘:被缩放的元素一旦因为 hover 被浏览器单独提升成合成层,就会按自己的比例栅格化再被
scale重采样,鼠标划过时会出现「一下清晰一下模糊」的闪烁。hover 高亮改由一个画在缩放之外(屏幕空间)的浮层.rk-hover-ring承担 —— 它跟随鼠标下的卡片/小节行,只动自己,不动卡片本体。
| 操作 | 说明 |
|---|---|
左上 ← 全部画布 | 只在某个学习画布里出现:回到一级画布(多个学习画布的总览)。一级画布上一张卡片 = 一个笔记根目录(磁盘上任意目录),点卡片上的 进入画布 › 进入它的画布(卡片空白处是穿透的:按住可以直接拖动画布,也不会选中卡片上的文字);+ 新建学习画布 建目录骨架,卡片上可改名 / 移出列表(移出 = 把画布目录移到同一层的 .remove/ 里存着,想恢复就手工移回上一层;同时记一个墓碑,重新扫描 / 设根目录都不会再把它加回来,在同一个路径新建画布或把它设回根目录时解除)。列表与墓碑记在 localStorage 的 rk-study:roots / rk-study:removed-roots(「当前在哪张画布」不落盘:打开面板 / 刷新都停在「全部画布」这一级,想进哪张自己点进去),并同步写一份到学习库的 <库>/.config/rk-study.json |
右上 ⌂ 根目录 | 就一个输入框 + 一个 📁 选择… 按钮:输入绝对路径,或点按钮弹宿主的目录选择窗体(跟宿主「打开文件夹」用的是同一个 —— 桌面版是系统「选择文件夹」框,网页版是内置目录浏览器);输入时防抖 350ms 预览这个目录 —— 认出几张画布(子目录里带 notes/ 或 questions/ 的)、还有哪些别的子目录、目录在不在,自己就是一张画布时会提示「根目录得选它的上一层」并给一个 用上一层 按钮。点 设为根目录 就把它定成根目录:画布列表替换成它下面那一层的子目录(扫盘得到的清单,同时写进库里的 .config/rk-study.json),库里缺的 .templates 内置模板补上;不在根目录下的旧画布就不再列出来(磁盘上一个字节都不动),第一次扫盘时会提示一句「有 N 张画布不在根目录下」。之后这个目录就是新画布的默认父目录(+ 新建学习画布 有根目录时只填名字,建在它下面),记在 localStorage 的 rk-study:default-root;已手动删掉的画布目录会在点右上 重新扫描 时从列表里自动移出 |
左上 画布 / 思维导图 | 两种看同一份笔记的方式:画布是章节卡 + 小节行 + 知识点卡(默认);思维导图把「章节 → 小节 → 知识点」画成一棵从左往右的树,知识点的正文放在它下面默认收起的子节点里,进导图会自动缩放到整棵树可见。切换记在 localStorage 的 rk-study:mode,刷新后保持 |
导图模式下的 ↻ 刷新导图 | 只在某张画布的思维导图里出现(一级画布永远是卡片,不显示它):重新扫描笔记、把量到的高度清空重算,再按新尺寸铺满一次 |
| 在空白处按住拖动 | 平移画布(光标变抓手) |
| 滚轮 | 以鼠标位置为锚点缩放(20% ~ 240%) |
| 在空白处 / 章节卡 / 小节行上点右键 | 弹出右键菜单:空白 = +新建章节 / 重新扫描 / 复位;章节卡 = +新建小节 / 改名 / 删除整章;小节行 = 打开 / 编辑小节 / 删除小节。删除类条目点第一下只是展开「确认删除?」,再点一次才真的删;Esc、点菜单外面、缩放或平移都会收起菜单 |
| 双击空白处 / 「复位」 | 把所有卡片缩放平移到刚好铺满可视区 |
点 100% | 缩放回到原始大小(位置不动) |
A- / 字号 N% / A+ | 改整个插件的字号(85% ~ 160%,六档,等比放大,等价于在根节点上设 CSS zoom)。点中间的 字号 N% 恢复 100%;选择存在浏览器本地(localStorage 的 rk-study:font-scale),刷新后保持;在一级画布上改还会同步进 <库>/.config/rk-study.json 的 ui.fontScale,换浏览器也保持。注意:本机存的那份要是「没设过」的出厂默认 100,就不算「存过」—— 这时以配置文件里那份为准(否则 localStorage 被清过一次之后,这个默认值会把库 / 画布里真正的字号永久顶掉,「离开插件再进来字号被重置」就是这个病);同理,库配置还没读回来之前一个字都不往本机 / 文件里写。因为根节点整体缩放,画布的屏幕坐标换算都除以了缩放比,右键落点、滚轮锚点、拖动跟手都仍然精确 |
◍ 配色 | 换整套配色(顶栏最右)。点开是 15 个色块:科技蓝(默认) + 14 个 Material Design 色(蓝 / 青 / 蓝绿 / 绿 / 柠檬 / 琥珀 / 橙 / 深橙 / 红 / 粉 / 紫 / 深紫 / 靛蓝 / 蓝灰),点一下立刻整块换色(菜单不关,方便一个个点着对比)。底色、描边、正文色都跟着主色色相算出来,保证层次与对比度一致。选择存在 localStorage 的 rk-study:skin,刷新后保持(一级画布上改还会同步进 <库>/.config/rk-study.json 的 ui.skin);跟字号一样,全屏专注时也生效。弹层下面还有一个 卡片各用一色 开关(默认开):让每个章节 / 小节 / 知识点带自己的颜色,一屏里不会全是同一个色,记在 localStorage 的 rk-study:card-colors(0 = 关)。弹层顶部还有 主题 一档(两档):插件配色(默认,就是这套黑蓝科技皮肤,一个字没改)/ 跟随主题。选「跟随主题」时插件不再用自己的底色,而是直接吃当前 DeepSeek Harness 主题的 --dsw-alias-* token —— 根节点背景透明、卡片 / 面板 / 输入框 / 弹层取宿主 bg-layer-1/2/3,文字取 label-primary/secondary,线条取 border-l1/l2/l3,强调色取宿主 brand-primary(换算成 --rk-a1/--rk-a2/--rk-a3 三个色相三元组),画布网格与径向光晕整个撤掉,配色色块变半透明并禁用(跟随主题时颜色由宿主决定),所以不会覆盖平台主题。宿主在 html[data-ds-theme-source] / body[data-ds-dark-theme] 上标了明暗,client.js 用 MutationObserver 盯着它们,宿主一换明暗插件立刻跟着换(不用刷新页面)。主题记在 localStorage 的 rk-study:theme(plugin / follow,老值 light / auto 自动迁到 follow),一级画布上改也会同步进 <库>/.config/rk-study.json 的 ui.theme;正文里的流程图(mermaid)与富文本编辑器(Milkdown)也跟着一起换 |
⤢ 全屏 | 把左侧边栏与右侧栏一起藏掉,画布占满整个窗口(宿主的壳是「侧栏 | 中列 | 右栏」的网格,插件给 <html> 挂 rk-focus、把两侧列压成 0px 并把中列显式放到第二列;不改宿主代码,宿主结构变了顶多全屏不生效)。按钮随即变成 ⤡ 退出全屏,按 Esc 也能退出。开关记在 localStorage 的 rk-study:focus,刷新后保持(老版本 rk-study:window 里 floating:true 的会自动升级成全屏) |
| 拉窗口 / 藏侧栏 / 全屏 | 界面按插件自己那一块的宽度自适应:一级画布的列数在 3 / 2 / 1 列之间自动切换(卡片跟着重排,不再硬撑三列),缩到装不下时自动重新铺满一次(自己调好的视野不会被动),顶栏标题、路径与工具条提示超长一律省略号;容器窄于 1000px 时右侧详情面板改成整块盖住画布(不再把画布挤成一条缝)、顶栏统计与工具条提示收起,窄于 860px 按钮更紧凑、弹窗铺满容器、编辑抽屉的左右分栏改上下;窄于 700px 藏掉缩放 -/+(⌘+滚轮、「复位」仍在)。用的是 CSS @container rk (...) 容器查询(只跟插件容器宽度走,宿主侧栏开合也算),不是 @media |
| 搜索框 | 搜章节名、小节名、知识点标题与说明,命中后直接列知识点卡片 |
| 章节卡右上角「改名」 | 弹出输入框改章节名(改的就是目录名) |
| 点章节卡里的小节行 | 在右侧展开详情面板:知识点卡片墙(题目在各知识点的详情页里);左侧画布照旧可以平移、缩放 |
| 点知识点卡片 | 右侧面板里进入知识点详情:说明 → 题目列表(答案默认遮挡);卡片底部显示它来自哪个文件 |
右侧面板右上角 ✕ 关闭 | 收起详情面板,画布恢复整宽 |
面板里的 ‹ 返回 | 从知识点详情退回到所在小节 |
| 重新扫描 | 立刻重新读取文件(平时每 10 秒自动刷新一次);在一级画布上则是按磁盘刷新画布列表——已手动删掉的画布目录会被移出并提示移出了几个;手动「移出列表」过的那几张不会再被加回来(它们的目录已经在同层的 .remove/ 里,墓碑记在 localStorage 的 rk-study:removed-roots,重新导入这个库或在同一路径新建画布才解除) |
顶栏 ⎇ Git 提交 | 只在一级画布(根节点)上出现:打开 Git 提交弹窗,一次提交整个学习库(所有画布的笔记 + 库根目录的其它改动),按钮上的数字 = 未提交文件数;弹窗里看分支 / 领先落后 / 上次提交与逐条改动,写一句提交信息后「仅推送」/「仅提交」/「提交并推送」(本地已有提交但还没推上去时,可以直接点「仅推送」;想手动更新远程内容就点「拉取」;提交前会先自动拉取合并)。细节见「七、Git 提交」 |
思维导图模式(左 → 右)
左上角的 思维导图 把同一份笔记画成一棵从左往右的树,五层:

| 层 | 节点 | 角标 |
|---|---|---|
| 0 | 根:学习画布 | N 章节 |
| 1 | 每个章节 | N 小节 |
| 2 | 每个小节 | N 知识点 |
| 3 | 每个知识点(只放标题) | 有题目时显示 N 题目 |
| 4 | 知识点的内容子节点(默认收起) | 里面渲染这一篇的整篇 markdown 正文(标题 / 列表 / 引用 / 代码块 / 公式 / 表格 / 流程图),高度按内容自适应;正文里的小标题(##/###)会像详情面板那样拆成一张张「小知识点」卡片(序号 + 标题 + H<级别> 角标 + 各自的内容,左边一条按级别配色的色条)—— 一眼能看出这一篇分了几小点 |
- 连线按层着色(主色 →
--rk-t2→--rk-t3→--rk-t4→--rk-t5),节点左侧也有一条同色的边条;节点宽度按层递减,每个有子节点的节点左侧有个+ / −圆按钮。 - 折叠状态分两处记:章节 / 小节的折叠记
localStorage的rk-study:map-fold,知识点正文的展开记rk-study:map-open(默认全部收起,只登记被展开的那一个),刷新后都保持。 - 同一时刻只展开一个知识点的正文(手风琴):点了别的知识点的
+,之前展开的那个会自动收起 —— 地图上永远只有一段正文铺开;再点同一个−就全部收起。 - 导图是只读视图:点节点、点正文、右键都不会打开任何面板或菜单(要看某个知识点的详情,回画布点它的卡片)。搜索框在导图模式下照常可用(有匹配时显示卡片墙)。
- 只有
+ / −是可点的:知识点节点上的+展开它下面的正文子节点,章节 / 小节节点上的+ / −折叠整段子树;顶部复位也是「缩放到整棵树可见」。 - 节点里的文字不能选中:
user-select:none,在正文上按住拖动也不会框住文字(拖出来的是平移)。反过来,节点里的链接照旧可以点。 - 鼠标可以穿透节点:在节点上按住左键拖动同样能平移地图,不必非得从空白处拖;光标落在节点上是
grab、拖动时变grabbing。画布里的章节卡保持原样(在卡片上按住不会平移,因为卡片自己可点);但一级画布的画布卡片同样是穿透的(卡片空白处按住就能拖动画布,也不会选中文字),只有进入画布 ›、改名、移出列表这些要执行事件的部件才接收鼠标。 - 内容子节点的高度是量出来的:先把正文照常渲染一遍,量到每个节点的真实高度后再重新排版(公式 / 流程图是异步画出来的,画完还会再量一次),所以整篇正文多长、节点就有多高。
- 展开 / 收起知识点正文不会动你的视野:当前缩放与平移原样保留(不跳回 100%、也不自动居中到那块正文),被点的那个节点在屏幕上原地不动 —— 展开后想看清楚,自己滚轮放大或点
100%。只有在你还没自己缩放过 / 拖过时,进入导图才会自动铺满一次(整棵树可见,等量到的高度稳定后再校准一次);一旦你滚轮缩放过、拖动过、或点过- / 100% / +,自动铺满就彻底让位(换画布、切模式、点↻ 刷新导图时会重新给一次自动铺满)。从存档恢复出来的视野同样算「你自己定的」,不会被自动铺满顶掉。长笔记(比如「使用说明」那一章)展开后很高,直接折叠整章、或点↻ 刷新导图就回到紧凑视图。 - 内容节点的盒子高度直接跟着内容(
height:auto),量出来的高度只用来算其它节点的位置。因为导图与画布在 100% 及以下(见上)视图缩放是纯视觉的,同一篇正文在任何缩放下换行与高度都一样 —— 缩到 20% 时节点不会变高、正文也不会溢出盒子。导图放大到 1 倍以上会换用 CSSzoom(见上),正文本就会按新字号重排,所以缩放比一变就重新量一轮高度(mindmap.js的scaleRef)。 - 平移 / 缩放 / 复位与画布完全一致(同一套
view与.rk-plane),切模式时视图回到左上角、回画布时自动重新铺满。 - 导图节点同样不做 hover 重绘,鼠标高亮走屏幕空间的
.rk-hover-ring.rk-mm。
一级画布(多个学习画布 / 多根目录)
插件不再只服务一个笔记目录:一级画布上每张卡片就是一个「学习画布」,也就是磁盘上一个笔记根目录 —— 软考笔记、别的考试、工作笔记可以各放一个目录,互不干扰。
- 点卡片上的
进入画布 ›进入它的画布(章节卡、小节、思维导图都在里面),左上角← 全部画布回到总览。 + 新建学习画布:设过根目录时只填名字(就建在<根目录>/<名字>);还没设根目录时才多出一行路径输入(默认<桌面上级目录>/<名字>,名字一改默认路径就跟着变,也可以自己改成任何绝对路径)。创建时在该目录建好notes/、questions/,再把内置的「公式模板」「Markdown模板」写进模板目录:只要这个画布建在导入的学习库里(或父目录下已经有别的画布 / 已经有.templates),就写进库里那一份<库>/.templates/,库里的画布共用、自己不再各存一份;只有自带根目录的画布才在它自己的.templates/里存。同一层目录里不允许有同名画布:名字撞上已有目录会被拦下(提示「已经有同名的画布了,换个名字」),既不覆盖也不合并。- 根目录(新画布的父目录):右上
⌂ 根目录设的那个目录就是它 —— 画布列表 = 它下面那一层的子目录(扫盘得到),+ 新建学习画布不填路径就建在它下面(它自己不会被当成画布;列表为空时也只是空着等你新建,不会拿它顶上)。记在localStorage的rk-study:default-root,同时在这个库目录下维护一个.config/rk-study.json(画布清单 / 移出记录 / 字号配色 / 一级画布视野,见「设置存在哪」)。没有单独的「设置」弹窗(已移除),想换根目录就再点一次⌂ 根目录。把库设到插件目录以外,插件的笔记就和插件本身分开了。 - 右上
重新扫描:按磁盘实际状态刷新列表 —— 你已经手动删掉的画布目录会自动从列表里移出(提示「已移出 N 个磁盘上已经不存在的画布」),其余卡片重新统计;没有任何变化时提示「已重新扫描」。探测请求失败的目录不会误删。 - 右上
⌂ 根目录:一个输入框 + 一个📁 选择…按钮 —— 点按钮弹宿主的目录选择窗体(桌面版 = 系统「选择文件夹」框,网页版 = 宿主内置的目录浏览器)选好目录,路径自动填进输入框(也可以手打/粘贴绝对路径,按 Enter 或点设为根目录)。这个目录就成了根目录:例如/Users/你/Desktop/RootNotes下面有xxx学习笔记/、软考学习笔记/(各自带notes/、questions/),设好之后这两张卡就进了一级画布列表(列的就是这个根目录下那一层的子目录),同时在该库里建好共用的.templates/(以及库级的.config/rk-study.json)。空目录也能导入(只是暂时没有画布);目录本身已经是一张画布时(有notes/或questions/)会被弹窗拦下并建议「用上一层」,磁盘上什么都不写,也不会把它的子目录(notes/)当成画布。 - 模板共用规则(一个画布的模板目录按顺序找):① 父目录是学习库就用库根目录的
.templates(判据:那一层已经有.templates,或者父目录下还有别的画布)—— 库里所有画布共用这一份,画布自己不再各建一份;库里那份被删掉后,再从画布改模板也只会重建在库根目录,不会在画布里冒出来;② 自带根目录的画布才用画布根目录的.templates;③ 老画布原来放在<画布>/notes/.templates的继续认(那里有文件就用那里)。所以现在新建的画布,notes/里不会再出现.templates。 - 列表就是根目录的子目录:设了根目录之后,一级画布列的那一层 = 扫盘结果(只列根目录下、且没被手动移出的),本机
localStorage只用来记住名字;扫不动(宿主刚起来 / 目录不在)时保留本机与配置文件里那份,绝不清空。一级画布列表与根目录的<库>/.config/rk-study.json双向同步 —— 插件配置(cordis.patch.yml)里的root只是「没带?root=时的兜底根」,不会自动变成一张卡片(那张带「内置」标签的卡已取消)。所以插件仓库里不需要notes/、questions/:笔记放在你自己的目录里(例如~/Notes/系统架构师/),仓库只管插件代码。 - 卡片上还能改名与移出列表。改名 = 直接重命名磁盘上的那个目录(目录名就是画布名,笔记与题目跟着目录一起走);改成同层已有的名字会被拦下(
name-taken),弹窗不关、磁盘不动。移出列表会把画布目录移到同一层的.remove/里(排布跟画布内删除章节 / 知识点是同一套:同一层的.remove/下按天开桶 ——<同层>/.remove/20261004/<画布名>,同一天里移出多个就往后排20261004-2、20261004-3,从不覆盖;想恢复就把目录从桶里移回上一层)。移出还会记一个墓碑:之后再重新扫描、刷新、重新导入同一个库都不会把它加回来 —— 只有在同一个路径+ 新建学习画布、或重新⌂ 根目录设回这个库时才解除。 - 画布卡片是穿透的:卡片空白处按住鼠标 = 直接拖动画布(
pointer-events:none,指针事件落到 canvas 上),卡片上的文字也不可选中(user-select:none);只有真正要执行事件的部件(进入画布 ›与改名/移出列表按钮)照旧接收鼠标(pointer-events:auto)。+ 新建学习画布那张虚线卡本身就是按钮,仍然整块可点。 - 列表、手动移出的墓碑、以及一级画布上的字号 / 配色,都记在
localStorage(rk-study:roots/rk-study:removed-roots/rk-study:font-scale/rk-study:skin);「当前在哪张画布」只活在这一个页面会话里,打开 / 刷新都从一级画布(全部画布总览)开始;学习库那一级的这些状态还会写进<库>/.config/rk-study.json(canvases/ui/zoom;移出记录单独放同目录的.config/rk-study-removed.json),所以换浏览器、换机器、重装插件,导入同一个库就能把画布列表与移出记录读回来;每个画布的统计与 Git 范围都只算当前这个根目录。
实现上,客户端把当前根目录加在所有请求上(?root=<绝对路径>),Host 半按 root 落配置文件分两级 —— 学习库那一级是 <库>/.config/rk-study.json(画布清单 / 移出记录 / 库级字号配色 / 一级画布视野),每张画布自己那一级是 <画布>/.config/rk-study.json(只放这张画布的视野与字号 + 发号计数器;笔记目录 notes/ 里一个字节都不写,见「设置存在哪」一节):plugin/dsh-kp-notes/lib/routes.js 用 node:async_hooks 的 AsyncLocalStorage 做请求级 root —— 每个请求进来先算出它自己的 {...基础配置, root},配置对象与 1 秒缓存都按 root 分桶(同一秒里读两个画布不会串),其余逻辑一行没改。新增四个接口:GET /rk-study/roots(默认根、建议父目录 suggestParent、宿主常用的 home / desktop、某个目录自己是不是画布 isCanvas,以及 notes/questions/模板目录约定)与 POST /rk-study/roots(建目录骨架 / 设根目录(导入)/ 改名,返回 created / templates;?path=&uids=0 只预览不发号),以及 GET / POST /rk-study/config(读写那一级的 .config/rk-study.json:GET 除了配置本身还回四个探针 exists / isCanvas / hasConfig / hasTemplates(客户端靠它判断这个库目录还在不在),POST 在目标根目录不存在时回 400 no-such-root 且不会把目录建出来;学习库那一级照单全收;请求的 root 自己有 notes/ 时按画布对待,只收 ui / zoom 两个键,canvases / seq 这类库级键一律丢掉,免得画布那份把库级状态写歪 —— 见 routes.js 的 canvasConfigPatch;removed 移出列表另存同目录的 rk-study-removed.json,读写仍走这个接口),还有 GET / POST /rk-study/state(插件级状态的镜像文件 <DSH_HOME>/rk-study/state.json:GET 读回 roots / defaultRoot / removed,POST 清洗这三项后与旧文件合并再落盘(老版本存过的 activeRoot 一律忽略,并在写盘时删掉),返回写进去的 keys)。路径会校验:必须是绝对路径、不含 ..、长度受限,不合法的 root 参数回退到默认根。
设置存在哪
单张画布的笔记目录(notes/ 那一层)里不写任何配置文件。设置分四处存:即时状态在浏览器 localStorage(打开就立刻生效);画布清单 / 移出记录 / 库级字号配色 / 一级画布视野写一份到学习库目录下的 .config/rk-study.json;某张画布自己的视野与字号(跟着画布走)写进这张画布目录下的 .config/rk-study.json;「画布列表 / 学习库(新画布的父目录)/ 移出墓碑」三项再由插件镜像到宿主侧的 <DSH_HOME>/rk-study/state.json(DSH_HOME 默认 ~/.dsh)。
第三份(state.json)解决的是「重启系统后要重新导入目录」:浏览器 localStorage 是空的(清过缓存 / 换了浏览器 / 宿主换了端口),客户端启动时会先读这份文件,只补齐 localStorage 里缺的键(画布列表与墓碑取并集),补齐后再走原来的恢复链,所以画布列表会自己回来,不用手动再设一次根目录。之后客户端每 900ms 比对一次这三项的快照,变了才 POST /rk-study/state 写回(Host 端是合并写:只覆盖这三个键 + updatedAt,不动文件里别的字段)。
学习库目录找不到时怎么办(目录被删 / 移动盘没挂上 / /tmp 被系统清掉):客户端每次读配置都会看宿主回的上面那四个探针 —— 库目录不在了就先把它认回画布列表的共同上一层(该层必须真的在、不是画布自己、且像学习库:有 .config/rk-study.json 或 .templates/),认回来提示一句「学习库目录已经不在了,按画布的上一层认回 <路径>」并改写本机那份 rk-study:default-root,画布列表原样不动;认不回来只提示「学习库目录 <路径> 已经不在了:视野和字号先只留在本机」,而且不会把记着的路径清掉(目录哪天回来还能接着用)。这一轮往库级的写入会被宿主挡成 400 no-such-root,所以不会再把配置写进一个幽灵目录、把它重新造出来(老版本会)。
先看 localStorage 里的:
localStorage 键 | 内容 | 说明 |
|---|---|---|
rk-study:roots | 画布列表 | 这台机器上要看哪些画布 |
rk-study:removed-roots | 移出列表的墓碑 | 移出的目录已经在同层 .remove/ 里,墓碑保证重新扫描 / 导入也不会再加回来 |
rk-study:default-root | 根目录(新画布的父目录,画布列表 = 它下面那一层的子目录) | ⌂ 根目录 写入 |
rk-study:font-scale / rk-study:skin / rk-study:card-colors / rk-study:theme | 字号 / 配色 / 卡片各用一色 / 主题(plugin 插件配色 / follow 跟随 DeepSeek Harness 主题) | 全局一套,所有画布共用 |
rk-study:mode / rk-study:map-fold / rk-study:map-open | 画布还是导图 / 导图折叠 / 知识点展开 | 同样全局一套 |
rk-study:focus | 是否全屏专注(1 / 0) | —— |
根目录那一级(⌂ 根目录 设的那个目录,例如 ~/Notes)还有一个插件维护的配置文件:
<学习库>/
├─ .config/
│ └─ rk-study.json ← 画布清单 / 移出记录 / 库级字号配色 / 一级画布视野
├─ .templates/ ← 库里共用的模板(导入 / 新建画布时建)
├─ .remove/20261004/ ← 「移出列表」的画布目录整份移到这里(按天开桶;想恢复就移回上一层)
└─ <各张画布>/ ← 每张画布 = 一个目录
└─ .config/
├─ rk-study.json ← 这张画布自己的视野 / 字号 + 发号计数器 seq
└─ rk-study-uids.json ← 这张画布自己的号池(画布与它下面章节的号)
.config/rk-study.json 键 | 内容 | 说明 |
|---|---|---|
version | 格式版本 | 目前是 1 |
ui.fontScale / ui.skin / ui.cardColors / ui.theme | 库级的字号 / 配色 / 卡片各用一色 / 主题(plugin / follow) | 在一级画布(学习库这一级)上改就写这里;进某张画布后改的字号写它自己那份 .config(下面那张表),配色 / 主题仍走库级 |
zoom.roots | 一级画布的视野 | { "x": …, "y": …, "scale": … };某张画布自己的视野写进它那份 <画布>/.config(键是这张画布的绝对路径),浏览器里的 rk-canvas:<工作区路径> 只是本机的即时副本 |
canvases | 画布清单 | [{ "path": "…", "name": "…" }],name 只是副本,真正的名字仍是磁盘目录名 |
uid | 这个库自己的编号 | 给「一张画布自成一库」时记下这张画布的号 |
seq | 发号计数器 | { "canvas": …, "chapter": …, "section": …, "point": …, "question": … },只增不减(号不复用的根据) |
每张画布自己那一份(<画布>/.config/rk-study.json)只有它自己的三样东西 —— 视野、字号、发号计数器:
<画布>/.config/rk-study.json 键 | 内容 | 说明 |
|---|---|---|
version | 格式版本 | 目前是 1 |
zoom | 这张画布的视野 | { "<画布绝对路径>": { "x": …, "y": …, "scale": … } };键就是画布自己的绝对路径,所以画布被整个拷到别处后仍认得出(那份配置里只有一条视野时,换个路径也认它) |
ui.fontScale | 这张画布的字号 | 在画布里改字号写这里;配色 / 主题 / 卡片各用一色仍只走库级那份 |
seq | 发号计数器 | 与库级那份同名同义,各记各的 |
号池是单独一个文件(跟 rk-study.json 同一个 .config/):rk-study-uids.json,内容 { "version": 1, "uids": { 绝对路径: 编号 } } —— 画布与章节的号都在这儿;小节 / 知识点的号平时写在 markdown 的 frontmatter 里,只有「老笔记还没被保存过」时先在号池里按路径挂一个(保存后就摘掉)。老版本混在 rk-study.json 里的 uids 会被自动搬到这个文件(见上面「编号」一段),rk-study.json 里的老键随之摘掉。
移出列表(墓碑)同样是单独一个文件(同一个 .config/):rk-study-removed.json,内容 { "version": 1, "removed": [绝对路径] } —— 客户端每次送的是完整名单,所以写就是整份替换(空数组 = 全部解除)。这个文件一旦建出来它就是权威:即使 rk-study.json 里还留着老键也不再并进来(不然「取消墓碑」会被那条老记录重新加回来);老版本混在 rk-study.json 里的 removed 照读,第一次写时自动搬过去并把老键摘掉。
写入是深合并:POST /rk-study/config 只带要改的键(例如 {"ui":{"fontScale":130}}),值给 null 就删掉那个键;文件不存在会自动连 .config/ 一起建。写哪一级由目标根决定:目标根自己有 notes/(说明它是张画布)时就只收 ui / zoom 两类键 —— routes.js 的 canvasConfigPatch 会把 canvases / removed / seq 这些库级键滤掉,所以画布那份 .config 只会长它自己的视野与字号,发号器写在里面的 seq 与号池文件 rk-study-uids.json 原样保留(老版本对画布回 400 not-a-library,现在放开了)。目标根目录本身不存在时直接回 400 no-such-root(body 的 error 就是这个字符串,message 是 目录不存在: <路径>):writeLibConfig 会顺带把不存在的目录 mkdir 出来,不挡的话「库已经删了、老客户端还拿着老路径」就会把那个幽灵目录重新造出来。
- 为什么库级与画布级各写一份:库目录是你自己选的项目根,「这个库有哪些画布 / 移出过哪些 / 库这一级习惯多大多小」记在它下面最自然 —— 换浏览器、换机器、重装插件,导入同一个库就全都回来了。而某张画布的视野与字号记在它自己目录下:把这张画布拷到别的库 / 别的机器上,视野与字号跟着它走(
localStorage的键名带着工作区路径,换台机器就对不上,所以配置文件才是它的「真身」)。无论哪一级,笔记目录(notes/那一层)与笔记文件本身一个字节都不写:笔记进 git 时不会因为字号、视野这类状态变化而互相冲突。 - 画布名就是目录名,不写进配置:名字只有一个来源,就是磁盘上的目录名。卡片上「改名」走的是
POST /rk-study/roots {action:"rename"}:Host 半直接把目录rename掉再返回新路径,客户端把列表里的路径换成新的。所以换浏览器 / 换机器 / 重新⌂ 根目录,显示的都是目录名;直接改目录名也一样有效。名字会校验(不能为空、不能含/、不能以.开头)与查重(同一层已有同名目录就报name-taken,弹窗不关、磁盘不动)。 - 画布目录里除了
notes/与questions/,只有一个.config/(老画布可能还有notes/.templates/):模板是给人编辑的 markdown(见上面「模板共用规则」),统一放学习库根目录的.templates/;画布自己的.config/里就是上面那两张表里的画布那份 ——rk-study.json(这张画布的视野 / 字号 /seq)与rk-study-uids.json(号池)。库根目录下另有一份.config/:rk-study.json(画布清单 / 库级字号配色 / 一级画布视野)、rk-study-uids.json(库这一级与各画布的号池)、rk-study-removed.json(移出列表的墓碑)。点开头的目录扫描会跳过、也不会被当成一章。⎇ Git 提交现在只在一级画布上、范围是整个学习库,所以这两样(以及库根目录下别的改动)都会在「仅提交」时一起进版本库 —— 除非.gitignore排除。 - 想覆盖插件配置(目录名 / 排除 / 扫描深度 / AI 模型):改插件自己的
plugin/dsh-kp-notes/cordis.patch.yml,不再支持「按画布覆盖」。 - 视野(平移 / 缩放)两级各写一份:一级画布的视野写进库的
.config(zoom.roots,跨浏览器保持),单张画布内的视野写进这张画布自己的<画布>/.config/rk-study.json(键是画布路径,跟着画布走);两者在浏览器里都还有一份即时的localStorage(rk-canvas:roots/rk-canvas:<画布路径>,刷新先用本机这份)。本机那条只有在v对得上、且真能解析出view时才作数,否则退回配置文件里那份(老版本条目 / 坏条目不再当成「本机有」)。重进面板不会把视野洗掉:视野只要是「本机存过的 / 配置里的 / 用户自己拖过缩过的」(userMoved),面板挂载时的尺寸测量与切模式的复位都不再触发自动铺满,「离开插件再进来画布缩放被重置」就是这个病。只有全屏开关不落盘。 - 「配置读不到」不等于「这个库没有配置」:
GET /rk-study/config的失败(宿主刚重启、插件半还没激活完、网络抖一下)以前被当成空的配置 —— 于是字号退回本机默认100、视野退回「没存过」,接着自动铺满一次 + 把这一版写进文件,库里真正那份字号与一级画布视野就被盖掉了(现象正是「重启 DeepSeek Harness 后根画布的缩放与字号被重置,进到某张画布里的却没事」)。现在:①readConfig只有真读到(宿主的成功响应里必带exists/isCanvas/hasConfig/hasTemplates探针字段,所以「对象非空」= 读到了)才算数,否则给null并一路重试(0.4s / 0.8s / 1.6s 起步、之后每 2.5s 一次,最多 50 次≈2 分钟,靠踢libRev重跑读配置的 effect;早期只给 6 次 × 700 ms≈4.2 秒,宿主慢启动时会先耗尽、之后没人再读 —— 于是「重启后第一次打开面板」时一级画布停在默认视野,直到进出一张画布(rootPath变了)才重读,表现成「第一次打开缩放不对,进出一趟就正常了」);②读不到时这一轮什么都不认:不改字号、不覆盖视野、不自动铺满、不设libReadyRef;③libReadyRef(= 真正的库配置读回来了)是自动铺满与库级写盘的前提,本机还没认下学习库目录时也不再硬置真;画布那一级的「首次进入画布自动铺满」同样等它。④宿主回的exists是三态:只有确定不在(FS_NOT_FOUND/ENOENT/ENOTDIR)才回false,探不动回null;客户端只在exists === true时才算「读到了配置」,null/false都继续重读 —— 早期宿主把「探不动」(capability 还没就绪、宿主半刚被重新 import 那几秒)也报成exists:false,于是被当成「读到了空配置」,正好复现「重启后第一次打开插件根画布没读配置,第二次才正常」。⑤「学习库目录已经不在了」这个提示与「按画布的上一层认回目录」的自动修复,都要求连续 ≥2 次确定不在、且持续 ≥2.5 秒(CONFIRM_MS)才动手,认回还要求候选目录给出正面证据(exists === true且带配置 / 模板)—— 于是宿主刚起来那几下瞬时失败只是「问不出来」,不再误弹「学习库目录已经不在了,按画的上一层认回 …」也不再误认路。⑥宿主侧POST /rk-study/config同样只在确定目录不在时才回 400no-such-root(问不出来就照写),免得把用户刚调好的视野 / 字号丢掉。⑦宿主每次「确定不在 / 问不出来」的探测与原因会记一行到/tmp/rk-probe.log(只有这两种情况才写、加 try 兜着),再遇到这种症状先看它。代价:连学习库都没有的极少数情形下不再自动铺满(宁可不动,也不拿没读到的配置去盖文件),手动拖 / 缩照旧、复位按钮照旧。
五、插件实现
| 文件 | 作用 |
|---|---|
plugin/dsh-kp-notes/host.js | Host 半入口:只有 16 行,把 lib/* 里的东西重新导出(name / inject / apply) |
plugin/dsh-kp-notes/lib/*.js | Host 半的实现,按职责拆成 14 个 ESM 模块(见下表) |
plugin/dsh-kp-notes/client.js | Client 半入口:模块注册 + apply(加载 client/ 下的模块、注入依赖)+ 面板本体(1525 行,含回收站面板与那块 panelRoot JSX);数据层 / 持久化 / 配色主题 / Git 面板 / 画布目录与根目录 / 编辑动作 / 画布视口与导图几何 / 渲染层 / 宽高配置分别搬到了 client/api.js / client/store.js / client/theme.js / client/git.js / client/roots.js / client/editing.js / client/canvas.js / client/view.js / client/sizes.js |
plugin/dsh-kp-notes/client/*.js | Client 半的实现,按职责拆成 21 个原生 ESM 模块(见下表),由入口用 import() 经插件自己的 /rk-study/client/ 路由取回;其中 client/sizes.js 是全插件唯一的「宽高配置」入口 |
plugin/dsh-kp-notes/vendor/ | 渲染引擎 + 编辑器静态资源:katex.min.js / katex.min.css / fonts/*.woff2(KaTeX 0.16.47)、mermaid.min.js(mermaid 11.17.2)、zt-milkdown/zt-milkdown.js + zt-milkdown.css(zt-react-milkdown 0.1.32,MIT) |
plugin/dsh-kp-notes/package.json | 包清单(dsh.bundle.patch、dsh.client、图标) |
plugin/dsh-kp-notes/cordis.patch.yml | 安装补丁与配置:root(扫描根目录)、exclude(忽略目录) |
host 半边(plugin/dsh-kp-notes/lib/,按依赖从下往上):
| 模块 | 行数 | 职责 |
|---|---|---|
constants.js | 131 | 路由/资源路径、默认值与上限、路径与标题的正则(图片素材那几项:MEDIA_ROUTE = '/rk-study/media'、MEDIA_PARENT_DIR = '.media'、老布局 MEDIA_LEGACY_PARENT_DIR = 'media'、MEDIA_DIR_SUFFIX = '.assestfiles'、MEDIA_MAX_BYTES = 24 MB、MEDIA_TYPES 后缀白名单) |
util.js | 283 | 目录名归一化、路径换算、标题与标签清洗、frontmatter、摘要、模板/库路径(templateDirOf / sharedTemplateDirOf / libraryDirOf)、validateRoot |
libconfig.js | 346 | 那一级的 .config/rk-study.json 读写(学习库那份与画布自己那份共用同一套代码,写哪一级由调用方给的 root 决定):键清洗(ui / zoom / canvases / uid / seq;画布那级由 routes.js 的 canvasConfigPatch 只放行 ui / zoom)、深合并(null 删键)、1 秒缓存,/rk-study/config 与 uid 发号器共用;两份「机器账本」各自另立文件 —— 号池 uidConfigPathOf(<库>/.config/rk-study-uids.json)/ readUidStore(号池优先,老配置里的 uids 只用来补缺、并被照读)/ writeUidStore(只写号池,写完顺手把老配置里那份 uids 摘掉,完成迁移),移出列表 removedConfigPathOf(<库>/.config/rk-study-removed.json)/ readRemovedStore(独立文件是权威,老键只在文件还没建出来时才认)/ writeRemovedStore(整份替换,写完把老配置里的 removed 摘掉),各有自己的 1 秒缓存 |
uid.js | 262 | 编号发号器:ensureUids(按路径批量发号,一次调用只写一次配置)/ uidsFor(只读)/ moveUid(改名搬号)/ dropUids(摘号,计数器不回退)/ adoptUids(恢复时认回)/ takeUid(只发号不记路径,给小节 / 知识点写进 frontmatter)/ adoptUid(认下文件里已有的号并把计数器抬上去)/ uidFromText 与 withUidText(从 markdown 里读号 / 把号写进 frontmatter)/ formatUid / isUid;号表读写的都是号池文件(readUidStore / writeUidStore),只有 seq 计数器还留在 .config/rk-study.json 里 |
headings.js | 156 | 扫标题(跳过代码围栏)、建标题树、子树范围、节点正文 |
parse.js | 224 | 一个 markdown 文件 → 小节/知识点/题目 的结构 |
questions.js | 105 | ## 题目 N 的识别、定位、替换、重排;每道题的身份号就挂在标题下一行的 <!-- rk-uid: q0001 --> 里(blockUidOf 读、withBlockUid 写、saveQuestionBlock 换块时自动沿用原来的号),removeQuestionBlock 把被删掉那题的号一并带出来 |
points.js | 118 | 单个知识点的读写(file 模式重建头部、node 模式只换那一段) |
templates.js | 547 | 小节 / 知识点 / 题目文件 / 题目的模板与题目计数 + 内置的「公式与结构模板」默认库 |
fsguard.js | 69 | 路径边界:insideRoot(经 ctx.fs.resolve 复核,符号链接指向外面也挡得住)、writePolicyOf(写操作的沙箱策略)、denyOutsideRoot;mkdir 的边界由调用方给 —— 新建画布 / 导入学习库 / 移出列表的目标本来就在请求 root 之外(父目录才是这几件事的边界)。取舍见文件头 |
write.js | 103 | 路径校验与写文件(经 ctx.fs.writeText,必要时 mkdir 兜底);写盘前给小节 / 知识点补 frontmatter 里的 uid:先认文件里 / 盘上已有的号,再认扫描时在号池里按路径登记的号(认到就把它摘出号池、写进 frontmatter),都没有才发新号;失败只当这次没补,不挡写盘 |
delete.js | 456 | 删除一律移到 .remove/:REMOVE_DIR / bucketNameIn / removeBucketFor(挑一个日期桶 YYYYMMDD,同一天再来就排 -2、-3;「移出列表」用同一个 bucketNameIn 在同层 .remove/ 下开桶)/ removeBucketName / moveIntoRemove(文件与目录都靠 renameSync 搬进桶,桶内保留原相对路径;一次删除的东西全落在同一个桶里)/ saveRemovedText(内容级删除另存片段)/ stashUids 与 readStashedUids(把这次删掉的目录带走了哪个编号记进桶里的 .rk-uids.json;readAllStashedUids 把一个 .remove/ 下所有桶记着的号合并读出来,恢复时按路径认回,名字(= 时间)晚的桶覆盖早的);素材跟着走:mediaRefsOf(从被删笔记的正文里挑出 …/*.assestfiles/* 引用,MEDIA_REF_RE + joinRel 拼成 root 相对路径)/ mediaStillReferenced(拿文件名在 notes/、questions/ 的 markdown 里再找一遍,扫不完返回 true = 留着)/ moveMediaIntoBucket(删小节 ⇒ 整个 <uid>.assestfiles/ 目录进同一个桶;只删知识点 / 题目 ⇒ 只搬没人再引用的那几个文件;mediaDirsOf 列出可能空掉的目录(新老两种布局的 media/ 都认),只 rmdirSync 空目录),以及排除判断 |
scan.js | 357 | 扫工作区、按目录聚章、把题目文件配回知识点、算统计、1 秒缓存(*.assestfiles 图片素材目录跳过;新布局的 .media/ 那层是点开头的,扫描本来就不看,直接进 skipped;老布局的 media/ 那层也跳过,不然会被当成一章 —— 判据 isMediaHome 是「里面除点开头的东西外全是 *.assestfiles 目录」,.DS_Store 不算数,而真有 .md 的 media 目录照旧是章节);顺带把号带出来 —— 文件记录读 frontmatter 里的 uid(小节 / 知识点),知识点文件自己的号挂到它那条知识点上 |
bin.js | 507 | 回收站(只有两层有它):根画布那层 listRootBins(把当前根、它上一层、画布列表里每张画布父目录的 .remove 合起来列,只留「顶层整条」= 整只画布,notes / questions 这类画布内部结构不算),一级画布那层 listChapterBin(只看这张画布自己的 .remove,按章把 notes/<章>/… 与平行的 questions/<章>/… 聚成一条);恢复是 restoreItem / restoreBucket / restoreChapter(按记录所在的 box 落回对应目录、原位已有同名文件时跳过或拒绝、章级恢复是「原位缺什么补什么」的合并、号按那只桶里记的认回、空桶与空掉的 .remove 一起收掉);listBin 是底座 —— 只列「影子树的根」(原位已经没有、父目录还在的那一层,所以恢复它就是把整棵子树搬回去),并且按 isBucketName(YYYYMMDD / YYYYMMDD-2 / YYYY-MM-DD_HHmmss)区分新旧:不是日期桶的目录是老格式(.remove/<相对路径> 就是那条记录本身),交给 collectLegacy 收进一只 at=旧格式、桶名为空字符串的分组,restoreItem 也支持空的 bucket(记录直接躺在 .remove 下) |
git.js | 551 | git 状态(分支 / 领先落后 / 改动分组)与拉取合并、暂存提交、推送、AI 生成提交信息:execFile 直调 git,关掉交互式凭据提示,只做 add -A / commit / push,绝不 reset / checkout / add -f |
routes.js | 2116 | apply:注册 8 个路由(/rk-study/notes 的全部 GET/POST 动作(含回收站 ?bin=1(mode=roots 走根画布那层、默认按章)与 restore / restoreBucket / restoreChapter)、/rk-study/roots 建画布 / 改名 / 浏览 / 设根目录(导入;?path=&uids=0 只预览不发号)、/rk-study/templates 模板读写、/rk-study/git、/rk-study/state 插件级状态镜像、/rk-study/media 图片素材(GET 取字节(按后缀在根 + 一二级子目录里兜底找,所以正文里老布局的 media/… 也认得到)、POST 落盘到 .media/<小节编号>.assestfiles/,落盘前先 migrateLegacyMedia 把老布局那层搬过来并 rewriteLegacyMediaRefs 改正文里的路径)、/rk-study/client 与 /rk-study/asset 静态资源)、用 AsyncLocalStorage 做请求级 root;画布与章节的编号在这里发放(uidLibOf 找号池、withChapterUids 给章节补号、withNoteUids 给还没号的小节 / 知识点在号池里挂号、建 / 改名 / 删除时发号 / 搬号 / 摘号;题目在 addQuestion 发新号、saveQuestion 沿用原来那道题的号、deleteQuestion 把号记进计数器;恢复时按桶里记的号认回 —— 导入搬回来的画布、扫描时搬回来的章节 / 笔记) |
改完怎么让它生效:改
client/(或client.js)保存后按⌘R即可,但如果按钮 / 文案这类东西没变,就把 bundle 关一次再开一次(客户端也是经打包端点带rev哈希下发的,缓存的rev不变就还是老脚本)。改host.js、lib/下的文件时必须把?v=N+1(碰了客户端模块的行为再一起 +1client.js的MODULE_VERSION),不然会出现「改了文件却还是老代码」的静默错配:# 1) host 半跨模块 import 的 ?v=N(当前 ?v=91) sed -i '' 's/?v=90/?v=91/g' plugin/dsh-kp-notes/host.js plugin/dsh-kp-notes/lib/*.js # 2) client.js 里加载 client/ 各模块的 MODULE_VERSION(当前 182;只改注释 / 只动宿主时不必动) sed -i '' 's/MODULE_VERSION = 181;/MODULE_VERSION = 182;/' plugin/dsh-kp-notes/client.js # 3) 已废弃:cordis.patch.yml 现在写的是包名 dsh-kp-notes,没有 ?entry=N 这个缓存戳宿主是按完整 URL(含 query)缓存模块的:只改
lib/的文件内容而不换 URL,兄弟模块照旧从缓存里拿 ⇒ 必须撞?v=N。 至于什么时候加载:实测宿主半保存后会被重新 import,隔几秒就生效,不用重启 Harness(cordis.patch.yml是发布形态的name: 'dsh-kp-notes',没有?entry=N,所以别再指望关开 bundle 换 URL);紧跟保存的那次请求可能还是旧代码,等几秒再试。客户端那半仍然是⌘R或关开一次 bundle 就生效。
client 半边(plugin/dsh-kp-notes/client/,按依赖从下往上;每个模块导出的是一个 createX(deps) 工厂 —— 模块之间不互相 import,依赖由入口按拓扑顺序注入,deps 里包含 React 与它需要的兄弟模块成员):
| 模块 | 行数 | 职责 | 依赖 |
|---|---|---|---|
client/dict.js | 664 | 中英文词典(zh / en) | — |
client/sizes.js | 135 | 全插件的「宽高」都在这一个文件里(createSizes() → {SIZES, px}):根画布(还没进任何画布)的学习画布卡片与舞台 rootCanvas 与 画布内(进了某张画布)的章节卡片与舞台 canvas —— 两套互不影响(改一个不会动另一个)—— 加两级共用的缩放上下限 stage、思维导图各层列位置/列宽/行高(mindmap)、右键菜单与浮层(menu)、弹窗 / 抽屉 / 详情面板宽度(dialog)、编辑器与预览高度(editor)、git 面板等零散值(misc)、容器宽度断点(breakpoints);纯数字 = px(px(n) 拼单位),vh / % 写成字符串;想调大小改这里一处,CSS 与画布几何一起变 | — |
client/css.js | 762 | 全部样式(const CSS + 末尾的 HOST_CSS 宿主主题映射层:.rk-root.rk-follow 把 --rk-* 指到宿主 --dsw-alias-*);所有尺寸都从注入的 sizes 拼出来(${px(S.dialog.pcardW)}),文件里不再写死宽高;舞台留白按级拆成 .rk-stage(根画布那份)与 .rk-stage.rk-stage-inner:not(.rk-canvas)(画布内那份,避开可拖动舞台自己的 padding:0) | sizes |
client/util.js | 94 | 缩放取整、字数、路径标签、小节 / 知识点查找、编辑器状态 | — |
client/api.js | 453 | 数据层:宿主路由的 fetch/post 包装(画布目录、roots、库配置、state、git;fetchLibConfig 会把宿主回的 exists / isCanvas / hasConfig / hasTemplates 四个探针挂到配置对象上,其中 exists 是三态(true / false / null,null = 问不出来,客户端只有 true 才算「读到了配置」),isCanvas / hasConfig / hasTemplates 仍按 === true 认;老宿主不带这几个字段时按「在」处理,免得被误判成库目录丢了)+ useCatalog / useGit 两个轮询 hook + activeRoot(只活在本页面会话的当前根目录,面板通过 getActiveRoot() / setActiveRoot() 读写) | React |
client/media.js | 230 | 图片素材:插图两步(upload(file, notePath) 同步交回一条 blob: 本地引用(URL.createObjectURL(file))并把文件记进内存 —— 选文件阶段不写盘;settleText(text) 在图片真进正文(markdown 里带 blob:)之后才把字节 POST 到 /rk-study/media 落到 .media/<小节uid>.assestfiles/,flush() 供保存前等一等)+ 显示换算(mediaUrl(src) / watchImages(rootEl):用 MutationObserver 盯住面板里所有 <img>,把相对 src 就地换成路由地址;只改 DOM,markdown 里那份相对路径原样不动)+ 写盘收口(toMarkdownSrc(text, strict):把显示地址与没落盘的 blob: 换回相对路径,strict 时把落盘失败的图整段去掉)+ maxFileSize(16 MB)与 allowedProtocols(['blob:'],两个都喂给 vendor 的 imageUpload,不传它默认只让 5 MB、且不认 blob: 会把图从 markdown 里静默丢掉) | api |
client/store.js | 268 | 持久化 + 本机状态:画布列表与统计(roots / rootStats)、「移出列表」墓碑(isRemoved / markRemoved / unmarkRemoved,外加把墓碑 ref 本身导出的 removedRef —— 面板合并「库配置里的 removed」与「落盘镜像里的 removed」时要直接改这份内存副本,只写 localStorage 会让 isRemoved() 在本页面里一直说「没移出过」)、配置的 500ms 合并写盘(saveLib / saveRoots / flushLib —— 库级那份收 canvases / `ui.skin | cardColors |
client/theme.js | 176 | 配色与主题:配色皮肤(skin / skinList / skinHex)、主题(theme:插件配色 / 跟随宿主明暗,follow / hostDark / mdTheme)、卡片各一色(cardColors);跟随主题时把宿主 brand 色搬进 --rk-a1..a3,主题变化用 MutationObserver 跟住,三样都同时写 localStorage 与库配置 | React、store |
client/git.js | 109 | Git 提交面板的状态与动作:未提交改动的角标数字(gitPending / gitOutside)、提交(可选顺手推送)/ 仅推送 / 拉取、让模型写候选 commit message(gitAi) | React、api |
client/roots.js | 342 | 画布目录与根目录管理:新建 / 改名(= 重命名磁盘目录)/ 移出列表(搬进同层 .remove/ + 记墓碑)、扫盘核对 probeRoot、⌂ 根目录 弹窗(350ms 防抖预览 + 用上一层 + 设为根目录,列表按扫盘结果替换)以及两个弹窗的状态 | React、api |
client/editing.js | 401 | 打开 / 编辑 / 新建 / 删除:小节与知识点的打开与编辑、新建小节 / 章节、章节改名、题目片段的新增与编辑、删除小节 / 章节 / 题目、舞台与卡片的右键菜单项(写盘一律 postAction 后 reload(true)) | React、api |
client/canvas.js | 514 | 画布视口与导图几何:视野(缩放 / 拖拽 / 自动铺满)、舞台尺寸与测量、导图树布局 mind / positions / rootCards / extent、指针与检索命中 hits、右键菜单状态;几何助手由入口注入,几何常量从 sizes 拿,并且按级取:const G = level1 ? S.rootCanvas : S.canvas(卡片宽 / 间距),卡高与每行列数只属于根画布(S.rootCanvas.cardHeight / rootColsMax),缩放上下限是共用的 S.stage | React、sizes |
client/view.js | 394 | 渲染层:右键菜单、一级画布卡片、导图(含思维导图模式)、小节、知识点详情、正文 body / detailBody;只读面板状态与各域动作,生成 vdom | React |
client/vendor.js | 282 | KaTeX / mermaid 按需加载、公式与流程图组件(流程图配色跟着主题走,换主题自动重画) | React |
client/md.js | 461 | markdown 渲染器(表格 / 引用 / 代码 / 公式 / 流程图 / 图片:![]() 渲染成真的 <img>,相对 src 由面板根那层 watchImages 换成路由地址)+ 实时预览;\X 按字面量渲染(代码 / 公式里的反斜杠原样保留;定界符自己被转义的 \$A_i\$ 不算公式段),另导出 unescapeRedundant / renderFingerprint 给编辑器与弹窗做「去冗余转义」($ 另有宽松指纹:只放过「字面量 $tex$ → 真公式」这一种变化) | React、vendor |
client/cards.js | 451 | 章节卡 / 知识点卡 / 答案遮挡 / 删除按钮 / 侧栏图标;renderPointBody(引子 + 小知识点分组,详情面板与思维导图共用);卡片默认宽度 `width | |
client/dialogs.js | 753 | 章节名、题目表单、知识点表单、Git 提交四个弹窗(题目 / 知识点表单里的 markdown 字段默认是 Milkdown 富文本,切「源码」回落到 markdown 源码框 + 输入助手) | React、md、snippets、milkdown |
client/milkdown.js | 95 | zt-react-milkdown 的小加载器(注入样式 → fetch CJS → 自写 require 只映射 react 家族 → 导出 useMilkdown();失败返回 failed,调用方回退源码模式) | React |
client/editor.js | 317 | 编辑器抽屉(默认 Milkdown 富文本,可切「源码」回落到 markdown 源码框 + 实时预览 + 输入助手工具栏;富文本只编辑正文,文件开头的 YAML 头单独摘出来按原样拼回;写回前用 unescapeRedundant 清掉 Milkdown 防御性多加的 \) | React、md、cards、snippets、milkdown |
client/mindmap.js | 268 | 思维导图模式:buildMindmapTree(建树,知识点 = 标题节点 + 默认收起的内容子节点)/ layoutMindmap(左→右分层排布,支持实测高度;每层的列位置 / 列宽 / 行高 / 行间距与画布留白都来自 sizes.mindmap)/ MindMap(svg 连线 + 绝对定位节点 + 渲染整篇 markdown(小知识点成卡片)+ 量高回填) | React、md、cards、sizes |
client/snippets.js | 479 | markdown 输入助手:工具栏按钮 / 快捷键(加粗、公式、表格、流程图…)、自己实现的编辑键(撤销 / 重做 / 复制 / 剪切 / Ctrl+V)、模板库解析与插入、公式 / 模板两个下拉菜单的分组归类、模板文件读写 | React |
为什么不用 loader 的 chunk 协议:它只认插件根目录下文件名匹配
client.<名字>.js的文件(正则^client\.[A-Za-z0-9][A-Za-z0-9._-]*\.js$不含/,chunkUrl()也是把入口 URL 的目录与文件名直接拼起来),装不下子目录。所以换成原生 ESM + 插件自己的一条只读静态路由/rk-study/client/<名字>.js(host 半lib/routes.js的clientHandler:只放行 GET/HEAD、只吐.js、拒绝越级路径、cache-control: no-store)。入口是普通<script>注入的经典脚本,可以直接用import()。
改 client 后怎么让它生效:
⌘R刷新即可 ——client/下的模块由上面那条路由直出且no-store,不参与 loader 的 rev 缓存。新加模块时:文件放进client/、导出create<名字>(deps),并把名字加进入口的const CLIENT_MODULES列表;若它需要别的模块,在入口的调用处把依赖传进去。想调插件的宽高(卡片多大、导图列多宽、弹窗 / 抽屉多宽、编辑器多高、几档响应式断点):只改
plugin/dsh-kp-notes/client/sizes.js一个文件。它导出的SIZES分九组 —— 卡片相关按级分成两套:rootCanvas(根画布,也就是还没进任何画布时那层「学习画布卡片」:cardWidth/cardHeight/cardGap/rootColsMax/ 舞台留白)与canvas(画布内,也就是进了某张画布之后的「章节卡片」:cardWidth/cardGap/ 舞台留白)—— 两者互不影响,改哪个只影响哪一级;另有stage(两级共用的缩放上下限)、mindmap(思维导图各层的列位置、列宽、行高、行间距、组间距、画布留白)、menu(右键菜单 / 浮层 / markdown 菜单 / 肤色面板宽高)、dialog(各种弹窗、抽屉、详情面板、回收站的宽高)、editor(编辑器与预览框的最小 / 最大高度)、misc(git 面板的文件列表 / 输入框 / 输出框、知识点正文框)、breakpoints(容器宽度断点)。单位约定:纯数字就是 px(配px()拼成18px那样),要vh/%就直接写字符串。CSS 与画布几何都从这一份拼出来 ——client/css.js用${px(S.…)}插值(舞台留白按级分别落在.rk-stage与.rk-stage.rk-stage-inner:not(.rk-canvas)上),client/canvas.js按level1取对应那套、client/cards.js/client/mindmap.js直接读数值,所以同一个尺寸只在一个地方定义(例如右键菜单宽度同时是.rk-menu的min-width与 JS 里算菜单位置用的MENU_W)。改完还是⌘R;故意没收进去的东西:装饰性小件(圆角、1-2px 描边、图标与文字的间距)与100%/auto这类由布局决定的值 —— 它们不是「宽高配置」,跟着窗口走才对。两个例外要知道:①面板很窄时(@container rk触发breakpoints.wide/breakpoints.tiny)两级舞台留白都会被收紧成固定值(16/16/48 与 12/12/40,写在css.js的容器查询里);②可拖动的舞台(.rk-stage.rk-canvas,画布/导图模式)本来就是padding:0,那里量不到留白。
路由:
GET /rk-study/notes—— 目录 JSON(?force=1跳过 1 秒缓存)GET /rk-study/notes?file=<相对路径>—— 单文件正文与解析结果GET /rk-study/notes?point=<相对路径>&key=<知识点标题>—— 单个知识点的结构化正文:mode:'file'(整个文件就是一个知识点,带标签)或mode:'node'(多知识点文件里的某一个##段落,key就是它的标题);错误point-key-required/point-not-found/note-not-foundGET /rk-study/notes?template=note|point|question—— 各类模板(question= 一道题目的骨架)GET /rk-study/templates—— 模板库文件表:{ok, dir, files:[{key, label, file, path, exists, markdown}]},key为formula(公式模板)/markdown(Markdown模板),文件不存在时exists:false并给出内置默认库;POST /rk-study/templates{key, markdown}覆盖写入对应文件(目录不存在会建),用于「建模板文件」与在外部改完再存回GET /rk-study/vendor/<文件名>—— 插件自带的渲染引擎与编辑器静态资源(KaTeX / mermaid / 字体 / zt-milkdown),只允许js|css|woff2|svg,单文件上限 16 MB,带一周缓存;越界路径一律 404GET /rk-study/client/<文件名>—— Client 半的 ESM 模块(见下表),只允许js、只允许 GET/HEAD、no-store、拒绝.开头与含../\的路径GET /rk-study/media?path=<笔记里的那段相对路径>[&root=…]—— 图片素材(png|jpg|jpeg|gif|webp|avif|bmp|svg,单张上限 24 MB,私有长缓存):先当「相对 root」的路径认(../前缀剥掉),认不到就把路径按/切成后缀(文件名不动、最多 3 档)到库里的根 + 一二级子目录找同名后缀 —— 所以.media/<uid>.assestfiles/x.png、老布局的media/<uid>.assestfiles/x.png与<uid>.assestfiles/x.png、以及notes/01-硬件/.media/<uid>.assestfiles/x.png都能认(搬迁之前写在正文里的老路径不改也照样显示);卡片 / 导图 / 预览 / 编辑器都只需要把 markdown 里那一段原样丢过来。找不到回404 {error:'image-not-found'}POST /rk-study/media——{path:<正在编辑的那篇笔记的相对路径>, name, type, data:<base64>}(data也接受带data:前缀的 dataURL):按「同目录 + 小节序号相同」定到这篇笔记所属的小节(题目文件先镜像到notes/一侧);落盘之前先看这个小节有没有老布局的media/,有就先整层搬成.media/并把正文里那几段路径改过来(migrateLegacyMedia+rewriteLegacyMediaRefs,搬不动不拦着插图),然后才把字节写进它旁边的.media/<小节编号>.assestfiles/<小节编号>-<序号>.<扩展名>(序号自动往后排),返回{ok, uid, name, path, src, bytes},src是相对那篇笔记自己的路径(题目文件里就是../../notes/…)。校验invalid-upload/empty-image/image-too-large/no-section-for-noteGET /rk-study/git[?scope=notes]—— 工作区 git 状态:{ok, root, branch, upstream, remote, ahead, behind, clean, files:[{path,code,group,tracked}], truncated, scope, scopeTotal, counts:{plugin,notes,other,total}, lastCommit:{hash,subject,date}}(?scope=notes时files/clean/scopeTotal只算notes/+questions/,counts仍是全量;不是 git 仓库时{ok:false,error:'not-a-git-repo'})POST /rk-study/git——{action:'commit', message, scope:'all'|'plugin'|'notes', push?}先按范围git add再git commit(没东西可提交时返回nothing-to-commit;push:true时连着git push,没有新改动就只推送已有提交);{action:'push', dryRun?}只推送(弹窗里的「仅推送」按钮 = 不提交、直接把本地已有的提交推到远程);{action:'message', scope?}让模型读一遍改动内容给 3 条候选 commit message(返回{ok, candidates, provider, model, files, tried});{action:'models'}列出当前可用的 provider / model(排障用)POST /rk-study/notes——{action:'save', path, markdown}/{action:'newSection', dir, title}/{action:'newPoint', dir, section, title}/{action:'addQuestion', path}(题目文件不存在就自动创建)/{action:'saveQuestion', path, order?, fields}(path可以是知识点路径,也可以是题目文件路径;fields = {kind:'choice'|'case', stem, options:[{key,text}], answerKey, answerText, explanation},校验empty-stem/need-two-options/bad-option-key/answer-not-in-options/empty-answer,写完统一重排## 题目 N)/{action:'savePoint', path, key, title, body, tags?}(知识点表单:key决定改的是整篇文件还是文件里的某一个##段落,校验empty-title,tags只在整篇文件模式下生效)/{action:'newChapter', parent, title}(只建目录)/{action:'renameChapter', dir, title}(改目录名,题目镜像目录与point:前缀一起改)/{action:'delete', path}/{action:'deleteDir', dir}/{action:'deleteQuestion', path, order}
扫描规则:忽略 plugin、node_modules、.git、dist、build、.obsidian 与隐藏文件;最多 1500 个文件、8 层目录、单文件 1 MB;图片素材目录 *.assestfiles 也跳过(否则会被当成一章),新布局的 .media/ 那层因为是点开头、直接进 skipped,老布局的 media/ 那层走判据(里面除点开头的 .DS_Store 这类文件外全是 *.assestfiles 目录时才算素材目录,真有 .md 就没被吞掉)。
六、主题与配色
默认是一套黑蓝科技皮肤(插件配色,插件自己的颜色,跟宿主壳的主题无关):深空底色 + 电光青蓝描边 + 玻璃面板,悬停有霓虹描边、卡片右下角有 HUD 角标、顶部标题条有流光。想让插件跟着 DeepSeek Harness 的主题走,用顶栏 ◍ 配色 里的 主题 切成「跟随主题」。
顶栏 ◍ 配色 可以整套换成 Material Design 的 14 个色(外加默认的科技蓝):换皮肤只覆盖 .rk-root 上的一组 CSS 变量,布局规则一行都不动。配色选择器、色块表都在 plugin/dsh-kp-notes/client/css.js(SKINS 表 + 生成的 .rk-root.rk-skin-<id> 规则)。
主题是另一条正交的轴,两档:插件配色(默认,就是上面那套)/ 跟随主题(吃宿主 DeepSeek Harness 的 token)。
-
插件配色:还是原来那套(默认),深色那套样式一行没改;15 套 Material 皮肤只在插件配色下生效。
-
跟随主题:插件不再自己上色,而是把
--rk-*映射到宿主 DeepSeek Harness 的--dsw-alias-*token(bg-layer-1/2/3、label-primary/secondary、border-l1/l2/l3、brand-primary、markdown-code-block…),根节点背景设成透明 ⇒ 平台主题原样透出来,不覆盖平台主题;宿主明暗由html[data-ds-theme-source]/body[data-ds-dark-theme]标记,client.js里的readHostDark()+MutationObserver盯着这两个属性,宿主一换立刻跟着换类名(不用刷新页面)。强调色用hostBrandTriplet()把宿主的brand-primary换成--rk-a1 / --rk-a2 / --rk-a3三个色相三元组,插件里所有rgba(var(--rk-a1), …)的淡色都跟着宿主品牌色走;跟随主题时配色色块半透明并禁用(颜色由宿主决定)。映射层是plugin/dsh-kp-notes/client/css.js末尾的HOST_CSS(.rk-root.rk-follow { … }+ 一组覆盖规则)。 -
主题还会传给两处「自带配色」的组件:Milkdown 富文本编辑器(包本身有
zt-md-dark/zt-md-light两套 token,传theme就行)与 mermaid 流程图(client/vendor.js的mermaidConfig()按当前主题给theme: 'default'+ 一套浅色 themeVariables;MermaidBlock订阅主题,切换后自动重画)。 -
想加一套配色:在
SKINS里加一行{ id, name, hex, light }(hex取 Material 500、light取 300),再在plugin/dsh-kp-notes/client/dict.js里加skin<名字>的中英文,⌘R后就能选 —— 底色 / 描边 / 正文色会按hex的色相自动算出来。 -
强调色不直接写死在规则里:样式里所有
rgba(…,…,…,…)都走三个「色相三元组」变量--rk-a1 / --rk-a2 / --rk-a3,皮肤只改这三个数 + 少量语义色,所以换配色不需要改任何规则。
变量(定义在 .rk-root 上) | 值 | 用途 |
|---|---|---|
--rk-bg-0 / -1 / -2 | #03070f / #060d1c / #0a1426 | 画布底 / 层 1 / 层 2 |
--rk-line-1 / -2 / -3 | 蓝白半透明描边 | 分隔线 / 边框 / 强调边框 |
--rk-a1 / -a2 / -a3 | 34,224,255 / 63,182,255 / 120,150,255 | 「色相三元组」:样式里所有 rgba(var(--rk-aN),…) 的强调色都走它们,皮肤只换这三个数 |
--rk-accent | #3fb6ff | 主色(描边、标题) |
--rk-accent-2 | #22e0ff | 高亮(数字、序号、展开后的答案) |
--rk-text / --rk-text-2 | #e8f2ff / #93a9c9 | 正文 / 次要文字 |
逐项配色(卡片各用一色)
除了整套换皮肤,还能让每个章节卡 / 小节行 / 知识点卡带自己的颜色(顶栏 ◍ 配色 弹层里的「卡片各用一色」,默认开)—— 一屏里不会全是同一个色,看久了不累。
- 实现:卡片自己在
style里覆盖一组 CSS 变量(--rk-a1 / --rk-a2 / --rk-a3+--rk-accent+--rk-accent-2),只影响这张卡和它的子树,所以在整套皮肤之上再叠一层「局部配色」,两者可以叠加。 - 调色板:
SKINS里除tech(默认那套)与blueGrey(灰调不够醒目)之外的 13 个 Material 色,按序号轮流取(client/cards.js的CARD_PALETTE+toneProps),章节、小节、知识点各按自己的序号错开,相邻的一般不同色。 - 开关状态记在
localStorage的rk-study:card-colors(0= 关);关掉后卡片不再加rk-tinted类,样式与原来完全一致。
| --rk-warn | #ffc247 | 遮挡中的答案条 |
| --rk-danger | #ff6b81 | 删除类按钮 |
改主题只动 plugin/dsh-kp-notes/client/css.js 里的 CSS 字符串(const CSS,末尾「黑蓝科技主题」皮肤段落)—— 改完 ⌘R 即可。
开发者提示:改完
host.js/lib//client/后怎么让插件重新加载(三个版本号一起 +1,再关一次开一次 bundle),见「五、插件实现」开头那段。
七、Git 提交
⎇ Git 提交 挂在根节点画布(一级画布)的顶栏上,一次提交整个学习库:git add -A(库根目录 + 下面每张画布的 notes/ / questions/ + 别的任何改动)→ 一条 commit 装完 → 可选 git push(推到该仓库自己的 origin)。提交前会先自动 git pull(合并而非 rebase,绝不改写已有历史),所以每次提交都是「先拉后提」。不用切终端,也不用一张张画布去点。画布里面不再显示这个按钮:只有「自带根目录、不挂在任何学习库下」的独立画布还保留自己的入口,范围仍是它自己的 notes/ + questions/。
| 部位 | 说明 |
|---|---|
| 按钮角标 | 整个学习库里未提交的改动文件数(每 20 秒轮询一次,窗口重新聚焦时也刷一次) |
| 状态行 | 当前分支、与上游是否一致(领先 N / 落后 N)、上游名、上次提交的短 hash 与标题。落后 N 用的是本地 remote-tracking 记录(git 的常规行为):点一次「拉取」(或提交前自动拉取)之后才是远端此刻的真实值 |
| 改动清单 | 列整个学习库的改动(<画布>/notes/… 这类路径照样归到「笔记与题目」分组);每条显示中文状态(新增 / 修改 / 删除 / 重命名 / 冲突),鼠标悬停能看到 git 的原始状态码(??、 M、D …);超过 400 条只显示前 400 |
| 提交范围 | 整个学习库(git add -A,等于在库里 git add -A && git commit);弹窗提示行会写明「整个学习库一起提交: 所有画布与其它改动都在这一次里」 |
| 提交信息 | 打开时的默认值是 更新学习库: 全部改动一次提交(画布上的独立入口仍按笔记改动给 更新笔记 / 更新仓库内容),可随意改;⌘/Ctrl + Enter = 仅提交,再加 Shift = 提交并推送,Esc 取消 |
✦ AI 生成 | 在「提交信息」标签右侧:把这次改动(路径 + 状态 + 每篇笔记的标题/小标题摘要)和最近 8 条 commit message 一起发给模型,回 3 条候选,点一条即填入输入框(第 1 条会先自动填上);生成中按钮变成「生成中…」 |
| 四个按钮 | 「拉取」:把远程更新 git pull --no-rebase 下来合并到本地(不改写本地历史,冲突了会停下来让用户解决;没有配置远程时禁用并把悬停提示写成「没有配置远程, 没法拉取」;拉完自动重扫画布、结果行显示「已拉取远程更新」+ git 原始输出,本来就没新东西时显示「本地已经是最新的」);「仅推送」:不提交,只把本地「已提交、还没推上去」的东西推到 origin(领先 N > 0 时才可点,否则按钮禁用并把鼠标悬停提示写成「当前没有需要推送的提交」;推开之后自动刷新状态、按钮随即变灰);「仅提交」:只做一次本地提交;「提交并推送」:提交完再 git push —— 工作区没有新改动时它会跳过提交、直接把已有提交推上去,并在结果行写明「没有新的改动, 已把本地提交推送上去」。推送的结果行统一是「已推送到远程」+ git 原始输出,失败时变红显示 git 的 stderr(例如远端不存在时的 does not appear to be a git repository) |
| 结果行 | 「拉取」/「提交并推送」在合并过远程之后还会在末尾补一句「(提交时已先合并远程更新)」;冲突时这一行会变红,并以「有 N 个冲突文件没解决: <文件列表> —— 」开头,三个提交/推送按钮同时禁用(悬停提示「有冲突文件没解决, 先处理冲突」),仓库停在合并状态(笔记里留着 <<<<<<<,git status 是 UU),解决冲突后正常提交即可收尾;成功显示 已提交 · 文件数 · 短 hash · 已推送到远程 与 git 的原始输出(工作区本来就干净、只是把已有提交推上去时显示 已推送到远程 · 没有新的改动, 已把本地提交推送上去);失败显示 stderr;没有需要提交的改动 只作提示 |
实现要点(plugin/dsh-kp-notes/lib/git.js):
- 只用
execFile('git', [...])传 argv,不经 shell;环境里GIT_TERMINAL_PROMPT=0、GIT_ASKPASS=''—— 远程要密码时会直接失败而不是把宿主进程挂住(凭据交给系统的osxkeychain)。 - 一定带
-c core.quotepath=false,否则中文文件名会变成\346\226\207…这种八进制转义。 - 只做
git pull --no-rebase --no-edit→git add -A [-- 范围]→git diff --cached --name-only(空了就不提交)→git commit -m→ 可选git push;永远不reset/checkout/add -f/rebase,.gitignore照常生效。 - 拉取/合并的三条保险:① 提交前先查
git rev-parse -q --verify MERGE_HEAD,上一次合并没结束(还有git diff --name-only --diff-filter=U的冲突文件)就拒绝提交——绝不把<<<<<<<写进笔记;② 提交前的拉取如果因为「工作区有改动」被 git 拒(改了远程也改了的文件、且工作区还没提交),会自动改成提交之后再补拉(pull.pulledAfter);③ 真的撞上冲突就不推送,把合并状态留在仓库里让用户自己解决(冲突文件列表一并回给界面),另一处没提交的改动也不会丢。 - 超时:状态/提交 30~60 秒,推送 180 秒。
dryRun参数可以让push走--dry-run(只打印 refspec,不写远程)。 - AI 生成走宿主自己的模型服务(
@deepseek-ai/dsh-llm,插件侧唯一的模型调用路径):故意不把llm写进inject—— 没配模型的 profile 也要能正常打开笔记面板;取而代之的是lib/git.js里的serviceOf(ctx, 'llm'),按ctx.llm(声明过才可用)→ctx.get(name)→ctx.reflect.get(name, false)依次尝试,拿不到就回llm-unavailable(面板上是一句正常提示,不再是 500)。拿到之后用llm.stream({ provider, model, system, messages, maxTokens, signal })逐块收text-delta,不引任何 npm 依赖,超时用内置的AbortSignal.timeout(60000)。 - 路由兜底:
resolveRoutes按「显式指定 > 插件配置gitProvider/gitModel> 注册顺序里每个 provider 的第一个模型(最多 4 个)」排队,前一个失败就试下一个(例如deepseek-official没有DEEPSEEK_API_KEY时会立刻失败,自动落到可用的deepseek-account),返回值里带tried说明试过谁、为什么失败。
第三方资源与许可
plugin/dsh-kp-notes/vendor/ 里放的都是第三方 MIT 项目,随插件原样分发(未改源码):
| 资源 | 版本 | 许可 | 用途 |
|---|---|---|---|
| KaTeX | 0.16.47 | MIT | LaTeX 公式渲染(katex.min.js / katex.min.css / fonts/*.woff2) |
| mermaid | 11.17.2 | MIT | 流程图 / 时序图 / 状态图(mermaid.min.js) |
| zt-react-milkdown | 0.1.32 | MIT | 所见即所得编辑器(zt-milkdown/zt-milkdown.js + .css,Milkdown 内核) |
插件自身以 MIT 许可开源:见仓库根目录的 LICENSE(plugin/dsh-kp-notes/LICENSE 是同一份副本,给发 npm 包时带上)。上面这三份第三方资源同样都是 MIT,各自的版权声明随文件保留在 plugin/dsh-kp-notes/vendor/ 里。
反馈
用着有问题、或者想要什么功能,在 GitHub 上开 Issue 就行;也可以直接找作者 UnitySir(QQ 451991189)。




