dsh-entry-drawer:用一个开关把侧栏入口列表收起来

前言

DSH 的左侧栏上半部分是一列「全局面板入口」:插件、插件市场、自动化任务、远程窗口、任务看板、记忆系统。每装一个插件,这里就多一行。列表越长,会话历史就被挤得越靠下,而会话历史才是日常真正在用的那块。

dsh-entry-drawer 就是针对这个痛点的插件。维护者是 xingzhen199186,分类为客户端插件,协议 MIT,npm 上版本 0.1.0,仓库星标为 0。它不做任何新功能,只借平台留给第三方插件的唯一座位 sidebar.panellist,在入口列表最前面放一个开关行,把其余入口整块收起,把空间还给会话历史。

这是什么

一句话定位:在 DSH 0.2.x 的侧栏入口列表最前面加一行开关,点击后隐藏除自己以外的所有入口,再点一次恢复。

浏览器端的能力只有这一条,但做得比较完整:收起状态会被记住,重载页面、重启桌面端都保持上次的选择;展开/收起都不会改动 DOM,收起通过一条 CSS 规则表达。

平台侧没有被它修改。座位(sidebar.panellist)是界面上一个固定位置,插件只能往里放自己的东西,改不了别人放的东西;壳层掌管这些行的盒子、行高和点击行为,平台本身也没有「把入口收成一组」的能力。

你会看到什么

展开时(默认状态),新增的一行位于左侧栏入口列表第一行,就在会话历史上方,内容是一对朝上的双箭头图标加文字「入口收纳」(英文界面显示 Entry drawer),下方是原有的全部入口,一个不少。

点击这一行:

  • 其余入口整块隐藏,直接不占位置,不是变灰;
  • 会话历史上移,多出几行空间;
  • 箭头翻成朝下,开关行留在原位。

再点一次,全部入口回来,箭头朝上。

收起状态存两份:浏览器 cookie(键 dsh-entry-drawer,一年有效)和 localStorage(键 dsh-entry-drawer:collapsed)。桌面端每次启动会在 127.0.0.1 上换一个端口,而浏览器按「协议 + 主机 + 端口」分区存 localStorage,cookie 只认主机不认端口,所以跨端口重启后依然记得。

一条已实测的行为:2026-11-05 在桌面端 0.2.2-rc.1 上验证过,先收起再展开正常;之后新装的第三方插件多出来的入口也会被一并收进去,不需要重新配置。

安装

前提:DSH 0.2.x,本机有可用的 Node.js 与 pnpm(dsh plugin 只是 pnpm 的转发器)。三种安装方式按场景选一种。

从 npm 安装(推荐):

dsh plugin --profile web add dsh-entry-drawer

web 换成你实际在用的 profile 名。

跟最新代码、或 npm 还没同步到新版时,从 GitHub 安装:

dsh plugin --profile web add github:xingzhen199186/dsh-entry-drawer

改代码时想一边改一边看效果,用本地目录安装:

dsh plugin --profile web add ./dsh-entry-drawer

也可以在插件管理器里选「从目录安装」并指向该文件夹。注意这种方式 profile 里记的是目录路径,之后移动或删除这个文件夹,插件就加载不起来了。

装完必须重载:Web 端刷新页面即可;桌面端建议直接重启应用,因为跑在浏览器里的那一半(client.js)要等窗口重新加载才会挂上,桌面端默认没有客户端热更新。

另外,桌面端请优先在应用内安装。桌面端的 profile 由应用自己托管,命令行写入一般会被拒绝(提示该 profile 只由桌面应用管理),这不是权限问题。在桌面端「插件」页面填包名、仓库地址或本机目录即可。

卸载:

dsh plugin --profile web remove dsh-entry-drawer

然后重载窗口。收起状态不会跟着删掉——它存在浏览器里,想清掉见后面的常见问题。

它是怎么做到的

一共三件事:占一个座位、拦一次点击、写一条样式规则,外加一个保险面板和一处存偏好的地方。

第一步,占座位。客户端半在侧栏座位上注册一行,order: -1000 让它排在入口列表最前面:

ctx.slots.inject('sidebar.panellist', () =>
  ctx.slots.register(
    { name: 'sidebar.panellist', id: 'entry-drawer', order: -1000, label },
    FoldGlyph,
  ),
)

行里画出来的就是开关本身:一个带 .dsh-entry-drawer-mark 标记的 span,里面是双箭头图标,文字标签「入口收纳」来自插件自带的中英词典。

第二步,拦点击。行盒子和行点击都归壳层,插件改不了,于是在 document 上注册一个捕获阶段的 click 监听(捕获阶段比壳层自己的处理更早看到这次点击)。判据很窄:点到的元素在某个 button 里,且那个 button 含本插件的标记。命中就 preventDefault() 加 stopPropagation(),壳层「切换到某个面板」的逻辑不再执行,插件改为翻转收起状态并把选择存起来。整个插件只注册一个监听,随插件一起挂上和回收,点侧栏其他位置不受影响。

第三步,写样式。收起不改 DOM,由插件自己的一条 <style> 表达,收起时追加:

nav[aria-label="全局面板"] > *:not(:has(.dsh-entry-drawer-mark)) { display: none !important }

读法:在无障碍名为「全局面板」的那个 nav 里,隐藏所有不带本插件标记的直接子项。导航名优先取活的语言词典(sidebar 命名空间的 panels.label),取不到时用已知的两个名字「全局面板」和「Global panels」各生成一条规则,所以中英文界面都能命中;名字拼进选择器前做过程序内转义。每个名字都要单独生成一条规则并写全子项部分,不能合成一个逗号列表——否则前半段会把整个导航(连开关一起)隐藏掉。切换界面语言时样式重算。

保险面板和状态存储:插件还在主内容区(main)注册了一个同 id 的占位面板,渲染结果为空。正常路径用不到它,因为点击已被拦下;若某条旁路仍然选中了这个面板,它会在挂载时翻转收起状态,然后跳回上一个看过的面板,而不是因为没有内容报错。宿主半(index.js)是空实现,apply() 里什么都不做,留着只为让这个包在插件树里有一个可选中、可检查、可卸载的入口——所有逻辑都在客户端。

测试

没有构建步骤,纯 JS,客户端半是 window.__ModuleLoader__.load({ id, factory }) 形式的模块。测试不需要浏览器也不需要装依赖,用 Node 内置的 assert、一个极小的假 React 与假 DOM 直接驱动 client.js:

node test/client.test.mjs

通过时输出 client.test.mjs: all checks passed。覆盖范围:两个座位的注册契约、展开与收起两种样式表、点击拦截(挡住壳层、翻转状态、写入偏好)、点别处不受影响、活词典优先、标签转义、无词典与无存储时的降级、样式表和监听器的归属与回收。

改完 client.js 要重载窗口才生效;桌面端请直接重启,它默认不跟随源码热更新。

已知限制

以下是实际情况,不是免责声明。

  1. 依赖导航的无障碍名。样式规则靠 nav[aria-label=...] 定位,上游哪天改掉这个名字或不再用 nav 元素,插件会安静地失效(收起不生效,也不误藏别的东西)。修法是改 client.js 里的 KNOWN_NAV_LABELS。
  2. 点击拦截在 React 之外。它靠「button 里含本插件标记」这个结构判断,上游若改了行结构,拦截会退化:点击落回壳层并选中这一个面板,保险面板会跳回原面板,体感是「点了没反应」。
  3. 收起是整组隐藏。官方的「插件」「自动化任务」「更多」会一起消失,因为它们和第三方入口在同一个列表里,做不到只藏第三方、留下官方。展开即恢复。
  4. 窄侧栏下没有可收的东西。Windows 桌面端把侧栏收成窄条时,整块入口列表本来就是隐藏的,插件不改变任何东西。
  5. 本地目录安装有脆弱点。用 add ./目录 装时,profile 里记的是目录路径,移动或删除该文件夹客户端半会加载失败,长期用请改用 git 安装,或别动这个目录。
  6. 状态按浏览器存。换浏览器、换主机名(例如从 127.0.0.1 换成 localhost)、清站点数据,都会回到展开。多个窗口共用同一份存储,但插件不监听 storage 事件,一个窗口切换后另一个窗口要重载才跟上。

常见问题

点了这一行入口整块不见了,怎么找回来?再点同一行,这一行始终留着,它就是开关。

收起之后点不回来了?通常是上游改了行结构,这一下点击落回了壳层。收起状态记在 cookie 里,刷新不会把它变回来,可在控制台执行 document.cookie = 'dsh-entry-drawer=0; path=/' 再刷新,或清掉这个站点的数据。

会影响别的侧栏插件吗?不会改它们的代码或状态。装在同一列表里的入口收起时会跟着一起藏起来,展开就回来;侧栏其他区域和主内容区不受影响。

能只收第三方入口、留下官方入口吗?不能。它们在同一个列表里,从这个座位只能整组隐藏。

适用场景与注意

适合 DSH 0.2.x 的 Web 端与桌面端用户:侧栏装了较多插件、会话历史被入口列表挤得要靠滚动找的人。形态为客户端插件(dsh.client 声明 platform: "web"),依赖 @deepseek-ai/dsh-client-ui-sidebar;没有侧栏的形态(例如 headless 命令行)不加载。

状态存放在浏览器本地,不写 profile、不碰任何配置文件——但插件是以当前 dsh 进程权限运行的。安装前应检查源码与许可证(MIT,见仓库 LICENSE)。

目录页与源码:

  • 目录页:https://www.skillhub.cn/plugins/xingzhen199186/dsh-entry-drawer
  • GitHub:https://github.com/xingzhen199186/dsh-entry-drawer

生态背景:DSH 的理念是「一切皆插件」,插件生态由社区维护,目录站点是独立站点,与 DeepSeek / 幻方无官方从属关系。

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜