DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

HighToward /

HighToward/dsh-plugins-sidebar-manager

Verified

DSH左上角插件侧栏管理,可以把面板入口收进一级分类/二级分类,也可以把某个入口隐藏或显示。

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@7bc7bd97

插件侧栏管理 · Plugin Sidebar

English | 中文

插件侧栏管理图标

一个给 DeepSeek Harness 用的插件。它管的是 DSH 左上角那一列功能入口:把各家插件塞进来的按钮按你自己定的分类摆好,一级分类下面还能再挂二级,用不上的入口直接收起来。所有调整都在 设置 → 插件侧栏管理 里做,改完立刻生效,不用重启。

  • 包名:dsh-plugins-sidebar-manager
  • 版本:0.1.0
  • 许可:MIT
  • 平台:Web(DSH Desktop 与 dsh web 都是 Web 外壳)

侧栏效果

上面这张是实际效果:「常用」「创作」「其他」三个一级分类,「其他」处于收起状态,下面的「更多」是外壳自己的入口,不受影响。


目录

  • 它解决什么问题
  • 功能
  • 安装
  • 使用
  • 配置与数据
  • 宿主接口
  • 实现原理
  • 兼容性与限制
  • 开发
  • 许可

它解决什么问题

DSH 的左上角侧栏是一个开放的插槽,叫 sidebar.panellist。任何插件想在那儿放一个图标入口,就往这个插槽里注册一条记录,并自己定一个 order 数字。外壳按 order 从小到大排成一列,画完就结束。

这套机制简单、够用,但有个副作用:入口多了以后,这一列就变成一堆互不相干的图标。装了十几个插件之后,你很难一眼分清哪个是"看板"、哪个是"画布"、哪个是某个插件的调试面板,因为外壳只认识数字,不认识用途。

这个插件补上的就是这一层:

  • 你可以自己定义分类,比如「创作」「工具」「调试」
  • 分类可以嵌套,一级下面挂二级,视觉上带缩进
  • 每个入口都能单独显示或隐藏,不用卸载插件
  • 分类标题可以点开收起,收起后它的成员一起消失
  • 侧栏折叠成一条图标条时,标题自动隐藏,不会撑破布局

它不改任何别的插件的代码,也不要求别的插件配合。它只在你看到的那一列上做排版。


功能

分类

  • 一级分类:侧栏顶层的分组标题
  • 二级分类:挂在一级分类下面,缩进显示,适合把一大类再拆细
  • 分类可重命名、上移、下移、删除
  • 分类可折叠,折叠状态会被记住
  • 删除分类不会丢入口,成员会退回到「未分类」

入口

  • 每个入口可以放进任意一个分类
  • 分类内可以上下调整顺序
  • 每个入口可以单独隐藏或显示,隐藏后从侧栏消失,但插件本身照常运行
  • 未归类的入口排在所有分类之后,顺序跟插件注册时一致

整体

  • 「接管侧栏」总开关,关掉后侧栏立刻回到各插件注册时的原样,配置不丢
  • 「重新扫描」重新读取当前所有已注册的侧栏入口
  • 「恢复默认」清空全部分类与隐藏设置,需要点两次确认

安装

方式一:从 GitHub 安装

在 DSH 的插件管理页面(侧栏的「插件」页)点安装,填入仓库地址:

github:HighToward/dsh-plugins-sidebar-manager

或者用命令行,把同样的地址交给 profile 的包管理器:

dsh plugin --profile desktop add github:HighToward/dsh-plugins-sidebar-manager

也可以直接用 pnpm:

cd ~/.dsh/profiles/desktop
pnpm add github:HighToward/dsh-plugins-sidebar-manager

方式二:从本地目录链接安装

适合你要改代码的时候。把仓库克隆到任意位置,然后让 profile 以 link: 方式依赖它:

cd ~/.dsh/profiles/desktop
pnpm add link:/absolute/path/to/dsh-plugins-sidebar-manager

再在 profile 的 package.json 里把包名追加到 dsh.profile.bundles 列表末尾:

{
  "dsh": {
    "profile": {
      "bundles": [
        "...",
        "dsh-plugins-sidebar-manager"
      ]
    }
  }
}

link: 安装的包,改完源码需要重启 DSH 才会重新加载宿主半;浏览器半在下次刷新页面时生效。

装完之后

打开 设置,左侧导航里会多出一项「插件侧栏管理」,位置在「插件」之后。同时左上角侧栏的入口会开始受它管理。

插件列表里这个包会显示中文名「插件侧栏管理」和一句中文说明,这两段文字来自包内的 locale/zh.json。


使用

第一步:建分类

进入 设置 → 插件侧栏管理,点「新建一级分类」。分类名可以留空,留空的分类在侧栏里显示为空标题,建议至少给个名字。

需要再细一层时,在某个一级分类卡片里点「新建二级分类」,它会自动挂到当前分类下。二级分类也可以在卡片里用「上级分类」下拉改挂到别的父级,或者选「未指定」,那样它就自己升成一级分类。

第二步:把入口放进去

每个分类卡片底部有一个下拉框和「加入」按钮。下拉框里只列出还没归类的入口。选中后点「加入」,这个入口就从「未分类」区搬进这个分类。

分类卡片中间是该分类的成员列表,每行有:

控件 作用
↑ ↓ 在分类内上移或下移一位
眼睛图标 隐藏或显示这个入口,隐藏后行会变淡并标上「已隐藏」
× 把这个入口移出分类,退回「未分类」

第三步:调侧栏上的样子

分类卡片头部的 ▾ / ▸ 按钮控制折叠。折叠后,这个分类的成员在侧栏里一起隐藏,标题还在,点一下就能展开。

卡片头部的 ↑ ↓ 控制一级分类之间谁在前。二级分类不参与这个排序,它跟着父级走。

总开关

工具条上的「接管侧栏」是总开关。关掉之后:

  • 侧栏立刻回到各插件注册时的原生顺序
  • 你建好的分类、隐藏设置全部保留
  • 再打开就恢复你调好的样子

适合临时排查"是不是这个插件让侧栏看起来不对"。

恢复默认

「恢复默认」会清空所有分类和隐藏设置。第一次点会变成「再点一次确认」,3 秒内再点一次才真的执行。这个动作只清配置,不会卸载任何插件。


配置与数据

存在哪

配置同时存两份,互为备份:

位置 作用
浏览器 localStorage,键名 dsh.plugins-sidebar-manager.v1 首屏就能按你的分类画出来,不等网络
$DSH_HOME/plugins-sidebar-manager.json 跨浏览器缓存、跨 profile 副本长期保存

$DSH_HOME 默认是 ~/.dsh,被环境变量覆盖时以环境变量为准。

写入策略:每次改动先写 localStorage,再防抖 400 毫秒 POST 给宿主。宿主用「临时文件 + 原子 rename」写盘,不会出现半个文件。

清掉浏览器缓存不会丢配置,下次打开会从宿主那份读回来。反过来,宿主那份不存在也不算错误,插件会当作「还没配置过」,侧栏保持原生顺序。

文档结构

{
  "version": 1,
  "enabled": true,
  "groups": [
    { "id": "g1", "name": "创作", "level": 1, "parent": "", "collapsed": false },
    { "id": "g2", "name": "视频", "level": 2, "parent": "g1", "collapsed": false }
  ],
  "items": {
    "skill-manager": { "group": "g1", "order": 0, "hidden": false },
    "infinite-canvas": { "group": "g1", "order": 1, "hidden": true }
  }
}

字段说明:

字段 类型 含义
version number 文档版本,当前固定 1
enabled boolean 总开关,false 时完全不接管侧栏
groups[].id string 分类 id,插件自动生成,形如 g1
groups[].name string 显示名,可为空
groups[].level 1 | 2 一级或二级
groups[].parent string 二级分类的父级 id,一级分类为空串
groups[].collapsed boolean 是否折叠
items[<入口 id>] object 以侧栏入口 id 为键
items[].group string 所属分类 id,空串表示未分类
items[].order number 在分类内的排序值,小的在前
items[].hidden boolean 是否隐藏

入口 id 就是该插件注册 sidebar.panellist 时用的 options.id,例如 skill-manager、task-board。

文档的容错

配置读进来之后会先做一次规整,任何不合法的地方都被丢掉而不是信任:

  • 顶层不是对象,当作空配置
  • 分类 id 缺失或重复,跳过该条
  • level 不是 2 就按 1 处理
  • 二级分类的父级不存在时,它会被提升成一级分类而不是丢弃。理由很直接:丢掉一个分类会让它下面的入口全部散掉,提升至少保住了归类
  • items 里指向不存在分类的条目,group 被清空,入口回到未分类
  • 所有字段类型不对的值都退回默认值

半截的、手改坏的、旧版本的文档都不会让侧栏崩掉。


宿主接口

宿主半挂在 DSH 的 webServer 上,提供三个方法两个路径:

方法与路径 请求 响应
GET /api/dsh-plugins-sidebar-manager/config 无 { "document": {...} },没有存过就是 { "document": {} }
POST /api/dsh-plugins-sidebar-manager/config { "document": {...} } { "ok": true }
GET /api/dsh-plugins-sidebar-manager/health 无 { "ok": true, "path": "<配置文件绝对路径>" }

约束:

  • 只接受本机回环地址(127.0.0.1、::1、127.*、::ffff:127.*),其他来源一律 403 { "error": "local-only" }
  • 方法不对返回 405
  • 请求体上限 512 KiB,超限或解析失败返回 400
  • 所有响应带 cache-control: no-store

这个接口只读写你自己的布局偏好,不涉及任何敏感数据,但也没有放开给远端:一个远端页面没有理由改你的侧栏。


实现原理

这里值得写清楚,因为侧栏分组看起来"应该很简单",实际是整套实现里唯一有难度的地方。

为什么不能走官方口子

sidebar.panellist 是列表插槽。外壳遍历这个插槽的所有注册项,为每一项渲染一个按钮,顺序由各插件自己声明的 order 决定。

它没有提供:

  • 往这一列里插入"分组标题"的插槽
  • 让 A 插件改 B 插件 order 的接口
  • 一个"分类"概念

也就是说,分组这件事在插槽层面没有位置可放。想让分组存在,只能落在渲染出来的 DOM 上。

于是怎么做的

插件在页面加载后对那一列做后置增强:

  1. 认出那一列。不靠 CSS 类名,因为类名是 CSS Module 的哈希,版本一变就失效。改成按内容识别:遍历页面上所有 nav,用插槽注册表里同一份 aria-label 去数它有多少个子按钮能对上,得分最高的那个就是侧栏列。
  2. 不改 React 的节点。所有由外壳渲染的按钮原地不动,插件只写它们的 style。
  3. 用 CSS order 排视觉顺序。这一列是 flex 容器,视觉顺序由 order 决定,与 DOM 顺序无关。
  4. 用 display: none 隐藏。隐藏的入口仍然留在列表里,这样取消隐藏时它回到原本该在的位置,而不是被追加到末尾。
  5. 标题作为兄弟节点插入。这些 <button class="cem-group"> 是插件自己创建的,从来没进过 React 的元素树,React 重渲染时不会去动它们。

为什么必须是 CSS order,不能直接移动节点

这是整个实现里最关键的一个决定。

React 在每次渲染时按 fiber 的位置重新插入自己的子节点。你把某个按钮 appendChild 到别的地方,下一次渲染它会被弹回原位。切换语言、切换面板、任何一次外壳重渲染都会触发。

而 order 不一样:React 不管这些按钮的 style 属性,谁写的就留在谁身上。所以视觉顺序能扛过重渲染。

怎么保持同步

  • 一个 MutationObserver 盯着 body 的子树变化和 aria-label / aria-current 属性变化,任何变化触发一次重排
  • 重排用 requestAnimationFrame 去重,同一帧只算一次
  • 配置变化直接订阅 store 重排
  • 另有 3 秒一次的兜底轮询,覆盖"外壳很晚才挂载侧栏"和"只换了文字没触发结构性变化"这两种观察不到的情况
  • 所有写 style / setAttribute 的地方都先比较当前值,值相同就不写。否则观察者会把自己写的变化当成外部变化,形成死循环

折叠态(rail)的处理

侧栏可以收起成一条 36 像素宽的图标条。这时每行只剩图标,没有位置放标题。插件通过测量每行实际宽度(<= 40 且大于 0)判断是否处于折叠态,是的话给这一列打上 data-cem-rail="1",CSS 里把标题全部 display: none。

失败时的行为

侧栏增强整段跑在 try / catch 里,出错只打一条 [plugins-sidebar-manager] sidebar pass failed 日志,不会往外抛。原因是 DSH 的 Web 外壳在某个插件 apply 抛异常时会中断整个启动流程。一个负责排版的插件不该有能力把 GUI 弄挂。


兼容性与限制

依赖

  • DSH >= 0.2.1-rc.2
  • React 18 或更新(用了 useSyncExternalStore,React 18 起才有)

插件本身没有运行时依赖。react、@deepseek-ai/* 这些都由 DSH 外壳在加载时注入(client.js 里的 require 拿到的是外壳给的模块表),所以 peerDependencies 只声明了一个 react 并标成 optional,安装时不会顺带装一份 React 或 DSH 本体。

已知边界

  • 侧栏增强依赖外壳渲染出来的 DOM 结构。外壳如果改了侧栏的容器标签(现在是 nav)、或者不再把入口标签写在 aria-label 上,识别会失效。失效的表现是侧栏保持原样,不会报错,也不会弄坏别的功能。
  • 两个入口如果显示名完全相同,识别靠"按注册顺序依次匹配"来区分。注册顺序和渲染顺序不一致时可能配错。这种情况在真实插件里极少。
  • 入口的显示名由各插件自己的 label 决定,会跟随界面语言变化。插件用的是实时读到的标签,所以切语言后仍能对上。
  • 分类标题是插件自己插入的节点,不参与键盘 Tab 焦点循环之外的特殊处理,但它本身是可聚焦按钮,可以用键盘操作。
  • 宿主半的接口只对本机开放。如果你通过远程端口转发访问 DSH,配置读写会被拒绝,但已保存的配置仍从 localStorage 正常生效。

数据安全

  • 插件只读写自己的配置文件,不读别的插件的数据
  • 配置里不含任何凭据
  • 卸载插件不会删除 $DSH_HOME/plugins-sidebar-manager.json,想彻底清理需要手动删掉这个文件和浏览器里那条 localStorage

开发

目录结构

dsh-plugins-sidebar-manager/
├── index.js             宿主半:webServer 路由,读写配置文件
├── client.js            浏览器半:侧栏增强 + 设置页分区
├── cordis.patch.yml     bundle patch,把插件插进 profile 的插件树
├── package.json         包清单,声明 dsh.bundle 与 dsh.client
├── icon.svg             插件图标,同时用于插件列表与设置导航
├── locale/
│   ├── zh.json          插件列表里显示的中文名与说明
│   └── en.json          英文名与说明
├── LICENSE
├── README.md
└── README.en.md

两个半边通过 package.json 的 exports 分别暴露:. 是宿主入口,./client 是浏览器入口。dsh.client.platform 声明为 web。

本地调试

# 语法检查,两个文件都不需要构建步骤
node --check index.js
node --check client.js

# 宿主半接口
curl http://127.0.0.1:<dsh端口>/api/dsh-plugins-sidebar-manager/health
curl http://127.0.0.1:<dsh端口>/api/dsh-plugins-sidebar-manager/config

浏览器半的改动刷新页面即可看到。宿主半的改动需要重启 DSH。

没有构建步骤

client.js 直接用 window.__ModuleLoader__.load 注册模块,require 拿到的 react 等依赖由外壳提供。改代码不需要打包。

样式约定

所有颜色、圆角、间距都走 DSH 的主题变量(--dsw-alias-*、--dsh-sidebar-*、--dsw-radius-*),不写死颜色值。这样跟随 DSH 的浅色/深色主题自动变化。唯一的例外是开关按钮的白色滑块。

分组标题的尺寸、内边距、悬停背景刻意对齐外壳自己的侧栏行规格,目的是让它看起来就是侧栏的一部分,而不是贴上去的一块。


许可

MIT,见 LICENSE。

Copyright (c) 2026 HighToward

—/ 5

No ratings yet

Verified DSH bundle

Commit 7bc7bd97ae14

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout