dsh-settings-nav-manager
ziye1208/dsh-settings-nav-manager
为 DSH 设置页导航加编辑模式:拖拽排序、逐个显隐、用命名可折叠抽屉分组,排版存在插件自己的设置命名空间;还能把最多 5 个分区钉到主界面侧栏,一键直达。
安装
dsh plugin --profile web add github:ziye1208/dsh-settings-nav-managerREADME
dsh-settings-nav-manager
DSH 设置页导航管理插件 —— 给设置页左侧分区导航加上「拖拽排序 · 显隐 · 抽屉收纳」,一键进入编辑模式随手整理,自动持久保存;还能把常用分区钉到主界面侧栏,点一下直达。
适用于 DeepSeek Harness(DSH) 的 web(网页版)与 desktop(桌面版)两个 profile。纯 JavaScript / ESM,无需构建。
产品演示
总览:编辑模式 · 拖拽排序 · 显隐 · 抽屉收纳

1.3.0 新增:分区固定到侧栏

上图是 1.0 – 1.2 的整理能力,下图是 1.3.0 新增的「固定到侧栏」。
附:DSH 全局导航折叠功能由 dsh-nav-fold 插件实现。
🆕 1.4.4 修复(数据安全)
| 问题 | 说明 |
|---|---|
| 读不到就不许写 | 旧版 describe() 一失败就把内存当「空排版」,随后点一下眼睛/星标/拖拽就会把空态写回磁盘——一次网关抖动 = 一次静默清档。现在只有成功读到过才允许保存,读失败期间写操作全部短路,并在齿轮旁提示「已暂停保存以免覆盖」;之后按 400/800/1600ms 退避重试,恢复后自动解除。 |
| 写盘不再裁剪「此刻缺席」的分区 | 旧版写盘按本次 sync 的分区列表过滤 order / hidden / pinned / 抽屉成员。某插件被禁用、HMR 重载、apply 抛错时它就会缺席,用户给那个分区设的排版会被永久删掉。现在写盘只做类型清洗与上限:暂时不存在的 id 留着无害(渲染会跳过),少一个才是丢数据。 |
🆕 1.4.x 更新的功能
| 功能 | 说明 |
|---|---|
| ⭐ 编辑模式里直接打星标 | 编辑模式下每行右端多一颗星,和眼睛并排:空心点一下变实心,这个分区立刻钉到主界面插件栏(最多 5 个)。整理排版时顺手就固定了,不用再进入口页找 ☆。 |
| 两处 ☆ 同源 | 导航行的星标和入口页的 ☆ 读写同一份 pinned:任一处点,另一处立刻同步(含已固定态、禁用态、上限与隐藏分区的提示)。 |
| 操作说明改成浮层 | 齿轮旁那句说明重写成三行(星标/眼睛 → 改名/分组 → 侧栏开关),放在齿轮左侧的浮层里:进编辑模式显示 8 秒后自动淡出,鼠标移回齿轮区随时再看——不再参与标题栏排版,既不会挤占设置内容,也不会被红字告警推到一边。 |
| 星标与眼睛永远齐平 | 两颗图标画在同一个伪元素的两层遮罩里(位置同源),纵向用上下各 −9px 的对称外扩锚定行中心——宿主给某些行换了定位容器也不会一高一低。 |
| 不抢别家插件的图标位 | 图标画在 ::after 而不是 ::before:dshmarket / dsh-pet / dsh-better-sidebar 这类插件用 ::before 给分区画自己的图标,抢同一个伪元素就会互相裁掉(1.4.3 修掉的真机问题)。 |
1.3.0 更新的功能
| 功能 | 说明 |
|---|---|
| 📌 分区固定到侧栏 | 入口页每行右侧的 ☆ 一点变 ★,该分区就钉到主界面插件栏(最多 5 个,排在「设置导航」入口之后)。点那一行直接打开设置并选中该分区,而且当前主面板不会被切走——关掉设置还停在你原来的页面。 |
| 兜底页 | 每个固定行都配一个同名 main 页。哪怕布局恢复 / 深链 / 键盘激活绕过了点击拦截,也不会抛 layout.selectPanel: panel "..." is not registered,而是看到一个有「打开设置 / 返回」的页。 |
| 隐藏即取消固定 | 已隐藏的分区不能钉(☆ 置灰);反过来,隐藏一个已钉的分区会自动取消它的固定——入口藏在设置里,钉着也点不到。 |
| 语言无关 | 固定行的标题走实时解析,切到英文界面后标签跟着变,不需要重建注册。 |
功能特性
| 功能 | 说明 |
|---|---|
| ⚙ 编辑模式 | 设置弹窗标题栏「打开配置文件」左侧新增齿轮。点击进入编辑模式,再点或 Escape 退出。齿轮常驻防死锁;编辑模式是临时态,不持久化。齿轮旁的「侧栏入口」开关只在编辑模式出现。每行右端此时多一颗星标(固定到侧栏),与眼睛并排。 |
| ⇣ 拖拽排序 | 编辑模式下按住条目整行拖动即可排序,实时预览插入位置,松手定序并立即保存。位移阈值 > 6px 判定为拖拽,低于阈值按普通点击放行,不误触发导航跳转。拖拽被中断(窗口失焦、触屏手势被系统接管)会自动回滚,不会卡住。 |
| 👁 显隐开关 | 点行右端的眼睛切换。隐藏条目在编辑模式内仍可见(置灰 + 划线眼,方便一眼找到恢复入口),非编辑模式下 display:none。允许全部隐藏。若隐藏的正是当前浏览的分区,会自动切到第一个可见分区。 |
| 🗄 抽屉收纳 | 新建命名抽屉把条目收进去:拖入 / 拖出 / 组内排序,双击抽屉名改名(1–32 字符,可重名),点抽屉头任意位置折叠 / 展开,一键解散(成员回到未分组、保留顺序)。支持空抽屉。 |
| └ L 型归属线 | 抽屉内条目左侧有 L 型树线,一眼看清条目挂在哪个抽屉下。 |
| ↺ 恢复默认 | 编辑模式底部一键清空抽屉与隐藏、回到宿主默认顺序。整理乱了不用怕。侧栏入口若被隐藏,也会一并恢复显示。 |
| ⌨ 键盘操作 | 编辑模式下:Alt+↑/↓ 移动条目(或抽屉),h 切换显隐,抽屉头上 Enter / Space 折叠、F2 改名,Escape 退出(在改名输入框内只取消改名)。 |
| 💬 即时说明 | 编辑模式下齿轮左侧浮出一张三行说明(拖拽/星标/眼睛、双击改名与新建抽屉、齿轮旁侧栏开关),8 秒后自动淡出,鼠标回到齿轮区可再看。浮层不参与标题栏排版、不吃点击,所以不会挤占设置内容,也不会被红字告警推歪。 |
| 💾 持久化 | 排序 / 显隐 / 抽屉结构写入 settings-nav-manager 命名空间,重启保留、切换语言重建后仍正确。读取失败不会被当成「没有排版」:此时暂停保存并在齿轮旁提示,退避重试成功后再自动恢复(1.4.4)。写入失败同样会在齿轮旁提示并带上宿主给出的原因,不再静默丢数据。 |
| 🧭 侧栏入口 | 主界面左侧插件栏新增一行「设置导航」(排在插件市场 / 自动化任务之后)。点进去是一页全部分区清单,点任意一行直接打开设置并选中该分区;列表跟随你整理的顺序与抽屉分组,已隐藏的分区折在底部,可展开查看或点进去恢复。深链会回 DOM 核对是否真的选中:失败时页面里显示原因与「重试」,不会点了没反应。 |
| 🙈 入口开关 | 编辑模式下齿轮旁多一个「侧栏入口 显示中 / 已隐藏」开关,点一下就把侧栏那一行收起来(main 页面保留在册,不会让宿主指向空面板);状态随排版一起持久化,重启仍是隐藏,启动瞬间也不会先闪一下。想找回来:打开设置 → 齿轮进入编辑模式 → 再点一下开关。切换失败时按钮、磁盘、侧栏三方一起回滚并提示原因。 |
| 📌 固定分区 | 入口页每行右侧的 ☆ 一点,这个分区就钉到主界面插件栏(最多 5 个,排在「设置导航」入口之后)。编辑模式下每行右端的星标是同一个开关,两处状态同源。以后点那一行直接打开设置并选中该分区,而且当前主面板不会被切走——关掉设置还停在你原来的页面上。已隐藏的分区不能固定(按钮置灰/星标压暗):入口藏在设置里,钉上去只会更难找。固定行若是被布局恢复、深链或键盘激活切过去,也有兜底页兜住,不会抛「面板未注册」。 |
安装
安装命令参考社区插件(如 dsh-purge、dsh-market)的通用写法。包名:
dsh-settings-nav-manager(已发布到 npm)。
从 npm 安装(推荐)
网页版(dsh web):
dsh plugin --profile web add dsh-settings-nav-manager
桌面版(Desktop 桌面应用): desktop profile 由桌面应用独占管理,需先让应用初始化它:
- 启动一次桌面应用(初始化 profile),然后完全退出;
- 执行安装:
dsh plugin --profile desktop add dsh-settings-nav-manager
- 重新打开桌面应用,进入 设置 使用。
安装后重启(网页版 dsh web / 桌面版重开应用),打开 设置 即可看到齿轮按钮。
从 GitHub 安装(tarball)
# 网页版
dsh plugin --profile web add github:ZIye1208/dsh-settings-nav-manager
# 桌面版
dsh plugin --profile desktop add github:ZIye1208/dsh-settings-nav-manager
或用压缩包直链:
dsh plugin --profile <web|desktop> add https://github.com/ZIye1208/dsh-settings-nav-manager/archive/refs/heads/main.tar.gz
本地 / 开发安装
git clone https://github.com/ZIye1208/dsh-settings-nav-manager.git
cd dsh-settings-nav-manager
dsh plugin --profile web add . # 桌面版把 web 换成 desktop
开发时用 link: 软链指向本地目录,改 client.js 后重启 DSH 即生效,无需重装。
卸载
dsh plugin --profile <web|desktop> remove dsh-settings-nav-manager
使用说明
- 打开 设置,点击标题栏「打开配置文件」左侧的 ⚙ 齿轮进入编辑模式(齿轮高亮,齿轮旁出现操作提示)。
- 排序:按住任意条目整行上下拖动,实时预览位置,松手保存。也可聚焦某行后按
Alt+↑/↓。 - 隐藏:点条目右端的 👁 眼睛(或聚焦该行按
h);隐藏后条目置灰、眼睛变划线眼,再点恢复;非编辑模式下直接不显示。 - 固定(1.4.0):点条目最右端的 ☆ 星标(和眼睛并排)→ 变 ★,该分区立刻钉到主界面插件栏;再点取消。与入口页的 ☆ 是同一个开关、同一份数据。已隐藏条目的星标压暗,点了只给提示。
- 抽屉:
- 点底部「+ 新建抽屉」创建并立即改名;
- 拖条目到抽屉名入组,拖到抽屉外出组,组内可继续排序;
- 点抽屉头任意位置折叠 / 展开收纳(
▾/▸); - 双击抽屉名改名,点
×解散(成员回未分组、保序)。
- 恢复默认:点底部「恢复默认」清空所有整理,回到宿主原始顺序。
- 按
Escape或再点齿轮退出编辑模式。所有改动即时持久化,重启保留。 - 钉到侧栏(1.3.0):主界面插件栏点「设置导航」进入入口页 → 点某行右侧的 ☆(变 ★)→ 侧栏插件栏立刻多一行,点它直接打开设置并选中该分区;再点 ★ 取消。最多 5 个。
- 收起入口行:编辑模式下齿轮旁「侧栏入口」开关可把「设置导航」那行藏起来(页面保留在册,不会指向空面板);想找回就再进设置点一次。
数据结构与持久化
数据存放在 DSH 设置命名空间 settings-nav-manager,形状如下:
{
"version": 1,
"order": ["general", "g-1", "models", "plugins"],
"groups": [
{ "id": "g-1", "name": "常用", "collapsed": false, "items": ["account", "skills"] }
],
"hidden": ["bili-settings"],
"hideEntry": false,
"pinned": ["models", "account"]
}
order:顶层相对顺序,元素为未分组的分区 id 或抽屉 id(g-前缀)。groups[]:抽屉(命名分组):id/name/collapsed(收纳折叠)/items(组内成员 id,有序)。hidden:被隐藏的分区 id 数组。hideEntry:是否隐藏主界面插件栏的「设置导航」入口(缺省false,只有严格true才算隐藏)。pinned:固定到插件栏的分区 id 数组(有序,最多 5 个;隐藏的分区会在归一化时自动移出)。
写入前过滤非法元素;读取任何异常一律回退默认值,不报错。
命名空间按条目 id 精确匹配。若宿主把条目 id 改写成别名(@local/... 之类),插件会尝试认领唯一一个「schema 指纹 + 值形状都吻合」的命名空间;只要候选不唯一或值形状不符,就一个字都不写并提示——宁可这次没保存,也不赌一个命名空间去覆盖别人的配置。
命名空间必须等于插件条目 id。 宿主的
settings.update()用entries().find(row => row.options.id === ns)定位配置项,两者不一致会抛No configurable plugin entry。本仓库的cordis.patch.yml把条目 id 固定为settings-nav-manager,与client.js的NS常量一致;改动任一处都会被test/repo.test.mjs拦住。
分区识别
分区 id 取 settings.section 槽条目的 options.id。导航行(navCell)本身不带 id,靠标题文本与 options.label 匹配映射(DOM 序号为主、文本为兜底),因此切换界面语言后仍能正确认行。
识别采用双向校验:序号映射要求文本一致(或尚未渲染),并对重复 id 做保护;一旦发现行数与分区数不一致、且文本无法唯一对应,本次不重排并在齿轮旁提示「导航结构变化」,而不是硬认错行把顺序写坏。
实现约束(React 安全)
- 不改写 React 管理的 nav DOM 结构(
replaceWith/removeChild会抛NotFoundError);只加data-*属性 + 注入 CSS(图标用伪元素 / 描线 SVG mask 绘制,随主题取色)。 - 排序用 CSS
order重排,不动 React 节点(nav 容器为 flex column)。 - 抽屉头是插件自有 DOM 节点,配合
MutationObserver自愈重建。 - 观察范围刻意收窄:面板是
createPortal到document.body的直接子节点,因此只监听 body 的直接childList(感知开关)+ nav 自身(感知行变化),并用requestAnimationFrame合并同帧回调;不订阅整个文档子树,避免流式输出时被 DOM 变更拖慢。 - 拖拽基于起手快照重算(纯函数),
pointercancel/ 失焦 / 页面隐藏都会回滚,不留半截状态。 - 选择器收敛到
client.js头部的SEL/DATA常量表,宿主升级断链只改那一处;不把 id 拼进选择器(id 里的引号会让querySelector抛错),改用属性遍历比对。
开发与测试
npm test # 纯逻辑单测 + 仓库校验(秒级,无需浏览器)
npm run test:e2e # 无头 Chrome 端到端(真实 DOM + 真实 PointerEvent)
npm run locale # 由 client.js 的字典重新生成 locale/*.json
# 改完源码要让它在本机桌面端生效:装好的那份是 node_modules 下的拷贝,不是软链
node tools/sync-profile.mjs desktop # 同步进已安装副本(会先整份备份)
node tools/sync-profile.mjs desktop -n # 只演练,看会改哪些文件
dsh plugin add装的是拷贝:不重新安装/同步,桌面端跑的还是旧代码。同步脚本按package.json的files白名单走,跳过test/、tools/、assets/;备份落在~/.dsh/profiles/<profile>/.snm-sync-backup/<时间戳>/。同步后必须完全退出并重开应用。
test/unit.test.mjs:normalize/serialize/applyDropTo/nudgeState等纯函数,包含 1.0.1 那个数据丢失缺陷的回归用例(「分区未知时不得裁剪」)。test/repo.test.mjs:清单声明(platform必须是web、命名空间必须等于条目 id、兼容范围要有上界)、文案单一来源、源码卫生(无innerHTML/eval、无选择器拼接)。test/e2e.mjs:用无头 Chrome 复刻宿主设置页 DOM + 最小 React 运行时,按出厂方式加载client.js,真实驱动指针 / 键盘事件;同时跑git HEAD的旧版本做 A/B 对照——标注「旧版应失败」的断言若在旧版通过,测试会判定为无效并失败。
排障
| 现象 | 处理 |
|---|---|
| 打开设置看不到齿轮 | 确认插件装在当前 profile(web 还是 desktop),装完需重启/重开应用;齿轮挂在 settings.action 槽,若被其他插件挤占可临时停用对方排查。 |
| 齿轮旁出现「未能保存排版(…)」 | 括号里是宿主的原始拒绝原因。写入先走 update;只有宿主报「配置条目/命名空间不存在」时才退到路径式 mutate,其它拒绝(校验、配额、只读、权限)原样上报、不再换路重试——刻意不用整份替换的 replace,避免认错命名空间时清掉别人的配置。控制台 [settings-nav-manager] 有每一条的完整错误。 |
| 齿轮旁出现「导航结构变化,暂未应用整理」 | 认行失配(行数与 settings.section 条目对不上)。通常是刚装了新插件、分区还在注册中;关掉重开设置即可。若持续出现,请在 issue 里附上你的分区列表。 |
| 整理后重启又变回去了 | 1.0.0 的已知缺陷,升级到 1.0.1 即可。 |
| 侧栏「设置导航」那行不见了 | 编辑模式里齿轮旁的开关被关掉了。打开设置 → 点齿轮 → 再点一下「侧栏入口」开关即可恢复(磁盘里的 hideEntry 会被一起复位)。 |
| 点入口页的某一行,设置开了但没选中 | 1.2.1 起页面里会直接显示「未能打开该分区」+「重试」。重试仍失败通常说明宿主改了设置外壳的实现(sidebar.settings 的 openSection 不在或行为变了),请附上版本号提 issue;临时可先按 Ctrl+, 打开设置手动点。 |
| 固定行点了没反应,或设置没打开 | 1.3.0 起点击走「document 捕获拦截 + 深链」:拦不到就退化成普通切面板(会停在兜底页,页里有「打开设置 / 返回」)。若看到兜底页,说明拦截或深链有一层没生效,请附版本号提 issue。 |
| 侧栏固定行个数对不上 | 上限 5 个;隐藏某个分区时它的固定会被自动取消(入口藏在设置里,钉着也点不到)。想恢复:入口页里再点一次 ☆。 |
| 想彻底重置 | 编辑模式底部点「恢复默认」,或删除设置文档里 settings-nav-manager 命名空间的 user 段。 |
兼容性
| 项 | 值 |
|---|---|
| DSH | >=0.2.1-rc.2 <0.3.0(web / desktop profile,实测 0.2.1-rc.2 / 0.2.1-rc.3) |
| 运行时 | Node ≥ 20.18 |
| 构建 | 无(纯 JS / ESM) |
package.json里的dsh.client.platform必须写"web"——宿主的模块系统据此判定「这是浏览器侧包」,桌面版同样读这个值(platform !== "web"会被直接忽略)。别改成"desktop"。
项目结构
dsh-settings-nav-manager/
├── index.js # Host 侧:settings 命名空间 Config(volatile)声明
├── client.js # Client 侧:齿轮 / 编辑模式 / DOM 干预 / 拖拽 / 显隐 / 抽屉 / 持久化 / 侧栏入口页
├── cordis.patch.yml # 插件注册(id = settings-nav-manager)
├── locale/ # zh.json / en.json(由 client.js 字典生成)
├── icon.svg # 插件图标
├── assets/ # demo-1.3.gif(分区固定演示)/ demo.gif(总览演示)
├── tools/gen-locale.mjs
└── test/ # unit / repo / e2e(含 harness)
许可
MIT