🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

面板列表管理 qwq1999/dsh-panellist

DeepSeek Harness
qwq19999 天前有提交已提供中文文档扫描:含敏感能力

DSHDeepSeek Harness客户端「面板列表」的管理插件。

30.7/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/26 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add qwq1999/dsh-panellist
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么[目录]—
dsh 原生插件 · market
暂无时间戳·判据 →
装得上吗[实装验证]✓
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
数据截至 2026/11/02·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/03·判据 →
还在维护吗[仓库]✓
活跃:最近一次提交在 9 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件
查看扫描证据(共 15 处,展示前 15 条)
  • 读取本地文件
    src/plugin/routes.ts:215
    await stat(path)
  • 执行外部命令来自配置/变量
    src/plugin/routes.ts:241
    const child = spawn(command, args, { detached: true, stdio: 'ignore' })
  • 读取本地文件
    src/plugin/store.ts:140
    const raw = await readFile(new URL(candidate, import.meta.url), 'utf8')
  • 读取本地文件
    src/plugin/store.ts:258
    raw = await readFile(this.configPath, 'utf8')
  • 读取本地文件
    src/plugin/store.ts:370
    await stat(join(this.home, PROFILES_DIR_NAME, profileId))
  • 写入本地文件
    src/plugin/store.ts:405
    await mkdir(this.home, { recursive: true })
  • 读取本地文件
    src/plugin/store.ts:406
    await access(this.home, constants.W_OK)
  • 读取本地文件
    src/plugin/store.ts:430
    raw = await readFile(this.configPath, 'utf8')
  • 写入本地文件
    src/plugin/store.ts:453
    await mkdir(dirname(this.configPath), { recursive: true })
  • 写入本地文件
    src/plugin/store.ts:455
    await writeFile(tempPath, text, { encoding: 'utf8', mode: 0o600 })
  • 写入本地文件
    src/plugin/store.ts:457
    await rename(tempPath, this.configPath)
  • 删除本地文件
    src/plugin/store.ts:461
    await rm(tempPath, { force: true })
  • 写入本地文件
    src/plugin/store.ts:467
    await writeFile(this.configPath, text, { encoding: 'utf8', mode: 0o600 })
  • 删除本地文件
    src/plugin/store.ts:468
    await rm(tempPath, { force: true })
  • 写入本地文件
    src/plugin/store.ts:488
    await copyFile(this.configPath, target)
扫描于 2026/11/3(1 天前) · 范围 全包扫描(包内未声明入口) · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-store@deepseek-ai/dsh-client-ui-layout@deepseek-ai/dsh-client-ui-primitives@deepseek-ai/dsh-client-ui-renderer@deepseek-ai/dsh-client-ui-settings@deepseek-ai/dsh-client-ui-settings-general@deepseek-ai/dsh-client-ui-sidebar@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-home-paths@deepseek-ai/dsh-host-webserver
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 自定义页签顺序
  • 修改页签名称
  • 页签分组管理

DeepSeek Harness 面板列表管理器

README

由 DeepSeek 最新模型翻译生成
dsh-panellist DSH(DeepSeek Harness)客户端「面板列表」的管理插件。 面板列表(panellist)是会话界面左侧那一列由各个插件贡献的全局入口——插件、自动化任务、技能中心、记忆系统、SSH、MCP 连接器……它们来自不同的插件,顺序由各自声明,图标与名称也由各自决定。 本插件让用户接管这件事:哪些页签出现在轨道上、以什么顺序出现、叫什么名字、用什么图标,以及把若干页签收进一个分组。所有配置都在「设置」里的一项新页签内完成,改动即时生效。 状态:已实现。这套 README(中英各一份)就是完整的规范——不再有单独的设计文档:行为见 §2–§3,配置与接口见 §4,兼容契约见 §5,安装见 §6,实现方式见 §7。配置的权威形状是 docs/schema/config.schema.json。 与最初设计的三处细化(都是实现期间发现的,已回填本文):分组的轨道 id 是 dsh-panellist:<groupId>;未配置页签始终列在整条轨道里,但只有序列非空(即你做过结构性改动)时才写 order;/panellist/api/reveal 这个端点是为了实现「打开配置文件」而新增的。
展开完整文档(README 剩余 1 节)
dsh-panellist

DSH(DeepSeek Harness)客户端「面板列表」的管理插件。

面板列表(panellist)是会话界面左侧那一列由各个插件贡献的全局入口——插件、自动化任务、技能中心、记忆系统、SSH、MCP 连接器……它们来自不同的插件,顺序由各自声明,图标与名称也由各自决定。

本插件让用户接管这件事:哪些页签出现在轨道上、以什么顺序出现、叫什么名字、用什么图标,以及把若干页签收进一个分组。所有配置都在「设置」里的一项新页签内完成,改动即时生效。

状态:已实现。这套 README(中英各一份)就是完整的规范——不再有单独的设计文档:行为见 §2–§3,配置与接口见 §4,兼容契约见 §5,安装见 §6,实现方式见 §7。配置的权威形状是 docs/schema/config.schema.json。

与最初设计的三处细化(都是实现期间发现的,已回填本文):分组的轨道 id 是 dsh-panellist:<groupId>;未配置页签始终列在整条轨道里,但只有序列非空(即你做过结构性改动)时才写 order;/panellist/api/reveal 这个端点是为了实现「打开配置文件」而新增的。

截图

| 设置 → 面板列表 | 轨道上的分组菜单 |
| 设置 → 面板列表 | 轨道上的分组菜单 |

1. 为什么需要它

DSH 的面板列表是只读投影:外壳枚举 sidebar.panellist 槽位里的注册项,按每一项注册时自报的顺序排序渲染。官方没有提供任何改写他人注册的 API:

- 顺序只读——插件只能决定自己那一项的 order;
- 没有「分组」概念——一行就是一个页签;
- 图标由贡献者提供的组件决定,名称由贡献者的 label 决定;
- 「设置」导航页签的图标只能从内核内置的一份清单里挑。

因此本插件必须同时用两个杠杆,并且把 DOM 介入压到最小:

1. 注册表改写——在运行时改写注册项上的顺序、名称与图标组件(注册项对象未被冻结,且由槽位注册表原样交回,所以事后写入对下一次投影可见);本插件的改动不触碰任何插件的页面本身。
2. 最小 DOM/CSS 重投影——只有一件事无法靠注册表完成:把某一行从轨道上「拿掉」。分组行本身也是注册项(由本插件注册),因此它的行框、高亮、提示与收起态行为依旧由外壳负责;弹出菜单是覆盖层,不改动原生行。

2. 行为规范(已确认)

2.1 管理对象

- 面板列表里的所有注册项一视同仁:内核自带项(插件、自动化任务、技能中心、记忆系统、SSH、MCP 连接器)与第三方插件注册的项都可被管理。
- 本插件自己的设置页签不在面板列表内,因此不参与管理。
- 真实页签 id 在运行时发现,不硬编码;本文档示例里的 id 仅是示意。
- 轨道自带的「更多」行也会列出,虽然它由外壳渲染、并非 Panellist 贡献者(§2.12)。

2.2 序列模型

轨道序列是一串有序项,每一项要么是单个页签,要么是一个分组:

- 每个页签在整条序列中恰好出现一次(序列是对页签集合的划分,不重复);
- 分组自身占据序列里的一个位置,并含有一个有序的页签子集;
- 只允许一层嵌套(分组内不能再放分组);
- 空分组非法;只含 1 项的允许,但管理面板会提示。

设计讨论里确认过的写法按此模型解释如下(G{…} = 一个分组):

| 写法 | 解释 |
| a c b d e | [a][c][b][d][e] |
| a b c | [a][b][c] |
| a 下拉(b c d e) | [a][G{b,c,d,e}] |
| a 下拉(b d e c) | [a][G{b,d,e,c}] |
| a 下拉(b e c)d | [a][G{b,e,c}][d] |
| 下拉(a b c)d e | [G{a,b,c}][d][e] |

2.3 显示 / 隐藏

- 只有两态:显示 / 隐藏(没有「禁用」第三态)。
- 隐藏 = 不再出现在轨道上,但页签本身依然存在、依然可用(可从管理面板选中、可被其它插件或快捷键激活),它所属插件的功能不受影响。
- 例外:被隐藏的页签若正处于激活态,它那一行临时显示——避免出现「人还在这个页面,轨道上却没有入口」。
- 分组内的成员也支持隐藏,含义换成「不出现在分组菜单里」:成员的轨道行本来就被分组抑制掉了,所以对成员而言隐藏只作用于下拉菜单。成员仍留在组内——计数、排序、组头高亮都照旧看见它。
- 已缺失的成员自动不出现在分组菜单里(注册项没了,菜单里留一行禁用的「已缺失」只是占地方,用户也无从操作);它仍留在配置里、仍列在设置页的「已缺失的面板」里,等你清理。

2.4 名称覆盖

- 纯显示层覆盖,只影响三处:轨道行的文字、收起状态的提示文字、无障碍名称。
- 不改动 DSH 其它位置(设置、命令面板等)中的同名显示,也不改变贡献者声明的原始名称。
- 首版为单字符串,对所有语言生效。

2.5 图标覆盖

三种来源,可随时切回原图标:

1. 保留原图标(默认);
2. 内置图标集——本插件自带的 24 个内联矢量图形(零依赖,离线可用)。它们按轨道上邻居们所用的规范绘制,即 IconOutlineRegular 那一档:viewBox="0 0 16 16"、fill="none"、stroke="currentColor"、线宽 1px、几何落在 16 网格的半像素坐标上并大致收在 2…14 之内、圆头圆角、aria-hidden、无 id 与外部引用。其中三个直接采用外壳自己那几颗默认图标的美术数据,因此选上去和它要替代的项看起来完全一样:puzzle 是插件风车(IconPluginPinwheelOutline)、clock 是时钟(IconClockOutline)、more 是省略号(IconEllipsisOutline)。这套图形在 16px 下与上述官方美术并排比对,并逐个量过着墨包围盒;
3. 用户上传图标——粘贴 SVG 源码;
- 白名单净化:移除 script、foreignObject、事件属性、外部引用(href/xlink:href 指向外部的用例);
- 单个 SVG 上限 64 KB;
- 内联渲染(不是 ),因此能随主题变色。

设置导航那一行有它自己专用的图形(一个「面板列表」,线宽取那一列所用的 IconOutlineMedium 1.3)——见 §3。

2.6 分组与组头

- 组头有独立的图标与名称,默认预填「组内第一项」的图标与名称(常用名如「更多」),用户可改。没设名称时,组头文字在渲染时按「第一个活着的成员」解析——配置里不烘焙字面量。
- 组头整行可点,点击即展开分组菜单;整行点击一律用于展开菜单,不会跳转页面。菜单里只列该分组的成员:标题行里原本还有一个指向分组自己那一页的「概览」入口,已按用户反馈删除。那一页仍保留为分组 main 的兜底页(这样程序化选中分组 id 时不会报错),但不再有任何自己的入口。
- 组内任一项处于激活态时,组头高亮,用户不展开菜单也能看出当前页面在哪个组里。
- 拖拽新建分组时,分组插到「最靠上的那个成员」原来的位置,即分组「长在」它成员的原地。这条规则由模型保证(createGroup 以「最靠上成员上方那一行」的身份为锚点),因此与具体手势无关。

2.7 分组菜单

- 点击展开(悬停不展开,避免误触)。
- 只列用户能操作的成员:隐藏的(§2.3)与已缺失的都不出现;组内成员全部隐藏时,菜单写明这一点,而不是给一张空卡片。
- 键盘可达:Enter/Space 展开、Esc 关闭、上下方向键在组内移动。
- 侧栏收起成 56 px 轨道时,菜单一律向右弹出(flyout)。
- 组内项数超过可视高度时菜单可滚动;首版不做组内搜索框。

2.8 未配置项与孤儿

- 新装的插件注册了一个你还没动过的页签 → 它直接出现在序列里(在已配置项之后,按它自身的顺序),不需要先「纳入」它。
- 某个页签的贡献者被卸载、而配置里还留着它 → 配置保留,管理面板标注「已缺失」,并提供「清理孤儿」按钮。
- 在你做结构性改动之前(拖动、建组/解散分组),本插件不写任何顺序:轨道保持贡献者自己的顺序。改名、换图标、隐藏只覆盖那一项、不动顺序——这正是「不碰你没让我碰的东西」的落点。

2.9 生效方式

配置改动即时生效:不需要重启 DSH,不需要重载任何插件,也不需要重新加载页面。这对每一次改动都成立,包括那些「什么都不用写」的改动:改写环节会比较一份「轨道渲染所依赖的一切」(顺序、名称、图标、显示/隐藏)的摘要,只要它变了就重新投影——所以第二次换图标、第二次改名,和第一次一样立刻上轨道。

2.10 接管范围

管理面板里从第一次打开起就列出整条轨道:不需要先把某个页签「纳入」才能碰它,也不存在一份「还没接管」的清单。(中途曾有过「未配置清单 + 逐项接管 / 恢复原生」的设计,已按用户反馈删除——它们只是让人多点一步;后来那条「还没接管顺序」的说明横幅与「以当前轨道顺序作为起点」按钮也一并删掉了:列表本身就是轨道,这道门槛不可见。)

真正仍然成立的「最小干预」性质落在写入上,而不是界面上:

- 配置里的 sequence 是部分权威:只记你实际改过的东西。
- sequence 为空时不写任何 order(新装即如此,纯 no-op),未配置项的名字/图标/隐藏一概不碰;它们保持贡献者自己的顺序。
- 你第一次做结构性改动(拖动、合并分组)时,当前顺序会被写成显式序列;此后未配置项仍按各自的原生顺序列在已配置块之后(§2.8)。

实现见 src/shared/model.ts:rows 永远是整条轨道(未配置的行 order 为 null,即「不改」),而 reordering 决定是否真的写 order。

2.11 每页只允许一个挂载

外壳可能在同一页里把客户端 bundle 应用两次(文件变了会重新求值,HMR 也会再应用一次)。两个控制器同时操作同一份账本会互相打架:后来者会把前一个装上去的图标包装当成贡献者自己的组件,双方还都往同一批注册项上写。因此 apply 在构建自己的挂载之前先退役上一个(src/client/mount.ts),并且本插件装上去的每个包装都是可识别的——遇到还挂着包装的注册项会先拆开再接管(§7.2)。

2.12 外壳自带的「更多」行

轨道最后一行「更多」不是 Panellist 的注册项:它由 DSH 自己渲染(来自 sidebar.panellist.actions 槽位),点开的侧边菜单被 portal 到 document.body。所以它不是 sidebar.panellist 的条目——没有 order、没有名称 thunk、没有图标组件可以让本插件改写(§7.2)。但它同样是那个 nav 里的普通 flex 子节点,因此仍然出现在序列里:

- 位置。 该行(shell:more)在序列里占一个位置,可以拖拽或上移/下移。改写环节按 DOM 顺序写轨道,所以它在序列里的下标就说明了它前面有多少行;注入的规则给「更多」锚点 order: 1、给它所在位置及其后的每一行 order: 2,用 flex 把它精确停在那里。不改动任何其他行选择器;当它本来就在最后时,一条规则也不写。
- 隐藏。 隐藏它就等于去掉面板菜单的入口,所以只在你不用那个菜单时才隐藏。

按设计它不能被改名、换图标或并入分组:名称、图标与行为都属于外壳,而且它没有可打开的页面,既不能作为分组的开头,也不能被折进分组——它的外观编辑会直接说明这一点,而不是给出那些控件。还有一个注意点:移动的是它画出来的位置,键盘顺序并不跟随,Tab 仍按 DOM 位置最后到达它。(其他行不同:它们的顺序写进注册对象本身,外壳按新顺序渲染,键盘顺序也随之改变。)

旧版本靠探测处理,不比较版本号。 探测 5(§5)如实报告轨道里到底有什么,只有两种结果表示「没有这一行」:面板列表里除了行以外什么都没有,以及列表里那个非行的子节点不再匹配。这两种情况下该行不出现在序列里,于是不支持的版本上不会留下一条死条目。如果这个配置里已经写过它——存过位置,或点过隐藏——该行仍会列出,并带一个「当前版本无此行」标记,因为直接丢掉等于把它交给孤儿清理。仅仅「轨道没挂载」(侧边栏收起)属于未知而不是「没有」,未知时保留该行。

3. 设置页签

在「设置」对话框左侧新增一项:

- 名称:面板列表;位置:排在设置导航靠后;
- 图标:自己专用的图形——一个「面板列表」,线宽取那一列统一的 IconOutlineMedium(1.3),以 currentColor 蒙版画在外壳原本的兜底齿轮位置。设置导航的契约里没有图标字段,所以这一行按可见文本定位并打标记;探测失败则保留原生齿轮(不报错)。见 §7.5。
- 页面没有「完成」按钮:关窗由对话框自己的 X 负责。

页面内容:

3.1 序列编辑器

- 列出整条轨道(从一开始就是全部页签,无需先「纳入」)。
- 主通道是拖拽:排序、拖入已有分组、把成员拖出分组。
- 每行的「与下一项合并为分组」建组;分组行的「解散分组」拆组。
- 键盘兜底是每项的上移/下移。
- 每行还有「隐藏」/「显示」按钮,就地切换这一行的可见性——分组行、以及分组内的成员行同样适用(成员隐藏后不再出现在分组菜单里,见 §2.3)。按钮排在每行最后(↑ ↓ − 隐藏),和顶层行一致。
- 点某行的名字即可打开该项的外观编辑(§3.2),分组内的成员同样可以(它们没有自己的顶层行,否则根本点不到)。
- 外壳自带的「更多」行也在这个列表里,带「系统行」标记;那里只能改它的位置和显隐(§2.12)。

3.2 项的外观覆盖

- 名称:单字符串覆盖(§2.4)。
- 图标:保留原图标 / 内置图标集 / 粘贴 SVG(§2.5),并带实时预览;选内置图标按该行自己的尺寸预览,「保留原图标」的预览显示贡献者真实的组件。
- 显示或隐藏由序列列表每行的「隐藏」/「显示」按钮负责(单一入口,不在两处重复)。
- 该项位于分组内时这里会写明,因此「为什么它不在轨道上」一眼可见。

3.3 分组编辑

- 组头的名称与图标。
- 有序的成员列表:上移/下移、移出分组、隐藏或显示。

3.4 配置导入 / 导出

导入会先整体校验再写盘;导出序列化整份文件(含所有 profile)。导入失败只报字段级原因,不改动任何内容。

3.5 打开配置文件

POST /panellist/api/reveal 在系统文件管理器中定位 $DSH_HOME/panellist.json。按钮旁边也会打印路径。

3.6 已缺失的面板

配置里存在、但没有任何注册项应答的 id 会在这里汇总(带计数),「清理缺失项」在明确确认后删除它们——绝不自动清理。

3.7 自检状态

四项启动自检(§5)的结果,以及「重新自检」按钮。失败项会写明它测了什么、因此停用了什么,以及本插件测到的 DSH 版本。

4. 配置与接口

4.1 存放位置与作用域

- 文件:$DSH_HOME/panellist.json(权限 0600,写入为「临时文件 + 原子重命名」,Windows 文件锁冲突时回退直写);
- 作用域:按 profile 分开存于同一份文件内,因为不同 profile 安装的插件不同;
- 迁移:配置带 schemaVersion,读到旧版本按需迁移;
- 损坏:解析失败时备份原文件并回退默认值,不静默丢弃用户手改过的内容。

4.2 配置形状(示意)

{
"schemaVersion": 2,
"profiles": {
"desktop": {
"sequence": [
{ "type": "entry", "id": "plugins" },
{
"type": "group",
"id": "group:work",
"label": "更多",
"icon": { "kind": "original" },
"members": ["schedules", "skills", "memory"]
},
{ "type": "entry", "id": "ssh" }
],
"entries": {
"plugins": { "label": "插件市场", "icon": { "kind": "builtin", "name": "puzzle" } },
"mcp-connector": { "hidden": true }
},
"meta": { "updatedAt": "2026-10-10T03:00:00.000Z" }
}
}
}

权威定义见 docs/schema/config.schema.json(JSON Schema 2020-12;宿主在每次写入前、每次读取后都用 ajv 校验整份文档,tests/shared-schema.spec.ts 会把上面这个例子原样跑一遍)。sequence 是部分权威:列出的项按此顺序,未列出的项由程序追加到末尾。

4.3 本插件占用的接口

| 用途 | 标识 |
| 设置导航页签 | settings.section id = dsh-panellist |
| 分组行(轨道) | sidebar.panellist id = dsh-panellist:<groupId>,例如 dsh-panellist:group:work |
| 分组页(main 兜底页,防止程序化选中分组 id 时报错) | main key = 同上 |
| 宿主半本机路由 | /panellist/api |

宿主半路由(仅本机回环可达,带 trusted-host 栅栏):

| 方法 | 路径 | 用途 |
| GET | /panellist/api/config?profile=<id> | 读取该 profile 的配置 |
| PUT | /panellist/api/config?profile=<id> | 整体写入(服务端校验 + 原子落盘) |
| POST | /panellist/api/config/reset?profile=<id> | 回退默认 |
| POST | /panellist/api/reveal | 在系统文件管理器中定位配置文件(「打开配置文件」按钮) |
| GET | /panellist/api/health | 配置存储与字段级自检状态 |

?profile= 只接受 $DSH_HOME/profiles/<id> 真实存在的 id;不带该参数时写入宿主自己的 profile(DSH_PROFILE,缺省为 default)。/health 与 /reveal 是整份文件级别的,忽略该参数。

两个半边的导出契约(与生态一致):

- 宿主半:name、inject、apply(ctx, config?)
- 浏览器半:apply(ctx)、inject
- 清单:dsh.bundle.patch(自动挂载)+ dsh.client.{inject,platform}

4.4 本插件建立在其上的外壳事实

下面每一条都不是公开契约,所以每一条都在启动时自检(§5)——但实现确实依赖它们:

| 事实 | 出处 |
| sidebar.panellist 是 list 槽位,由侧边栏条目的 children 表声明;向未声明的槽位注册会抛错 | 侧边栏自己的注册;槽位注册表的 register() |
| 存入的注册项是普通对象({component, options}),从不冻结;entries/entriesOfSlot 把该对象原样交回 | 槽位注册表的 register() |
| 对注册项的字段直写(含 entry.component)对下一次投影可见,但不增加账本版本、也不通知任何人 | 注册表 + 批量通知通道 |
| register/dispose 会改动账本并通知订阅者——同步抖动(§7.2)正是搭在这条通道上 | 注册表的变更通知通道 |
| 外壳渲染行文字用的是 resolveSlotLabel(options.label) ?? id,所以没有 label 的注册项本来就显示自己的 id | 侧边栏的渲染器 |
| 行的图标组件以 {size, active} 渲染;宽轨时 size 为 16(收起时为 18) | 侧边栏的 renderSlot("sidebar.panellist", {size, active}) |
| 「更多」行是同一个 nav.…_panelList 内的 span.…_moreAnchor,也是其中唯一的非按钮子节点;它打开的菜单被 portal 到 document.body | 侧边栏的 PanelMoreMenu + primitives 的 Menu |
| CSS 模块类名是 _<构建哈希>_<稳定键>;激活行会再拼上 _panelActive,所以 [class="_panelRow"] 能匹配而 [class$="_panelRow"] 不行 | 侧边栏的样式表 |
| ctx.layout.panelInfo 是 HostObservable<{activePanelId}>,且 ctx.layout.selectPanel(id) 对没有 main 入口的 id 会抛错 | 布局服务公开的读取面 |
| settings.section 契约里没有图标字段;外壳按 section id 从一份封闭清单里挑导航图标(IconOutlineMedium,size 16) | dsh-client-ui-settings-general 的 navIcon(id) |

5. 兼容性、自检与降级

版本门槛:engines.dsh: ">=0.2.0-rc.2 <0.3.0-0"(与开发所用的内核版本一致;类型包在 npm 上同版本可取,因此编译期就能发现契约漂移)。

本插件依赖 DSH 的五条内部事实(都不是公开契约),因此启动时逐项自检:

| 依赖的内部事实 | 失效后的后果 | 探测方式(src/client/rail/probe.ts) | 降级 |
| 槽位注册项对象未被冻结,可事后改写 order/label/图标组件 | 顺序、名称、图标改不动 | 临时注册一个探针行,改写它的 order 再通过 entriesOfSlot 回读;同时检查对象与 options 都未被冻结 | 停用改写,只读 |
| 轨道行的 DOM 结构可被定位,且 CSS 支持 :has() 选择器 | 「隐藏」这一项无法生效 | CSS.supports('selector(:has())'),并用与隐藏规则完全相同的选择器数行数,与 rail 注册项数比对(轨道未挂载时记为通过并写明原因) | 停用隐藏(其余改写保留),只读提示 |
| 外壳仍把「是否激活」传给图标组件(隐藏中的激活页签需要它) | 隐藏中的激活页签无法临时显示 | 临时注册一个探针行,等两帧确认它的图标组件确实被渲染、且收到的 active 是布尔值 | 该规则失效:隐藏变为无条件隐藏 |
| 设置导航按钮可由可见文本定位 | 设置页签图标换不了 | 对话框打开时按文本匹配自己的行;未打开时记为通过并写明(标记由 MutationObserver 在每次打开时重跑) | 保留原生齿轮图标 |
| 外壳把自带的「更多」行渲染为轨道列表最后一个非按钮子节点,其类名带 _moreAnchor | 「更多」行保持原生位置,无法移动或隐藏 | 用 nav[class="_panelList"] > span[class="_moreAnchor"] 匹配;轨道未挂载,或根本没有「更多」行(sidebar.panellist.actions 无人注册)时记为通过并写明是哪种情况;其他非按钮子节点会被报出类名。两种「没有这一行」的结论也正是把该行从序列中去掉的依据——除非配置里已经写过它(§2.12) | 只有这一行失去位置与隐藏能力;在没有该行的版本上,未配置过的行根本不会列出 |

- 探针会先等轨道槽位存在再测。 sidebar.panellist 由侧边栏条目的 children 表声明,而本插件可能比它先挂载;此时注册探针行会被拒,而「只读」正是由这一次拒绝触发的。所以这一轮会先等外壳自己的「席位出现后再跑」这条缝(slots.inject),上限 5 秒——启动顺序的竞态不会再让插件卡在只读。始终不声明该槽位的外壳,会在超时后如实报出。
- 任一项自检失败 → 降级为只读:不再做任何改写,管理面板顶部给出明确提示(哪一项失败、影响什么、如何反馈),并保留「重新自检」按钮。
- 绝不静默半坏,也不阻止插件加载(DSH 升级后界面仍然可用)。
- 已知不确定性:DSH 的样式类名是构建哈希,会在发版时重掷。因此选择器一律使用「稳定后缀」匹配(例如 [class="_panelRow"],实测有效;[class$="_panelRow"] 在激活行上会失败),并且每次挂载都重新探测一次。

6. 安装与开发

用户安装(发布后)

dsh plugin --profile desktop add dsh-panellist

包的清单里声明了自动挂载层,因此不需要手改 profile 配置。

本机开发(当前阶段)

在本仓库根目录
pnpm install
pnpm build

把本仓库以 link 方式挂进 desktop profile(DSH 需先完全退出)
dsh plugin --profile desktop add link:E:/ai-code-space/dsh-panellist

开发循环
pnpm watch # tsdown --watch,重新构建浏览器半,由客户端 HMR 热载,无需刷新页面

注意:link: 不会安装本仓库自己的依赖,所以先 pnpm install;桌面版要求 profile 已初始化且应用已退出。

回滚(把插件从 profile 摘掉):

dsh plugin --profile desktop remove dsh-panellist

如果卸载不干净,直接编辑 ~/.dsh/profiles/desktop/package.json:删掉 dependencies 里的 dsh-panellist 与 dsh.profile.bundles 里的同名字符串,再删掉 node_modules/dsh-panellist。配置文件 $DSH_HOME/panellist.json 是唯一的外部状态,删掉它即恢复出厂。

插件被禁用或卸载时不会留下残留:注册项的 order/label/组件都会还原、分组行被释放、注入的样式表被移除、所有标记属性被清除(src/client/registry/rail.tsx 的 dispose)。

质量门槛

pnpm typecheck # tsc --noEmit
pnpm test # vitest run(jsdom)
pnpm build # tsc(仅声明)+ tsdown(宿主半 + 浏览器半)
pnpm check # 以上全部

测试覆盖四块:模型(序列/分组/覆盖的归一化、序列化、迁移、损坏回退)、管理面板交互(拖拽、上下移、图标选择、SVG 净化、导入导出、成员隐藏)、降级路径(四项自检各自失败后的只读行为,含启动顺序等待)、打包契约(清单、挂载层、schema 版本与路由前缀单一来源)。不引入对真实 DSH 界面的 E2E。

发布(维护者)

打 tag 即发布,机器上不需要任何 token:.github/workflows/release.yml 用 npm trusted publishing(OIDC)认证——GitHub 为这次运行签发 OIDC token,registry 把它换成短期发布凭据,provenance 自动生成,没有可以泄露或过期的长期凭据。流程是:改 package.json 的 version → pnpm check → git tag -a vX.Y.Z -m "…" → git push origin main vX.Y.Z。CI 先校验 tag 与 package.json 一致,再 npm publish(lib/ 不入库,所以这一步同时负责产出要发布的文件)。

一次性配置(npm 上的 trusted publisher 字段)、发完的核验清单、常见失败对照表,以及 OIDC 走不通时的手动兜底,都在 docs/RELEASING.md。

7. 实现方式

7.1 模块布局

dsh-panellist/
├─ src/
│ ├─ index.ts # 宿主半入口(重新导出 plugin/index) │ ├─ plugin/ # 宿主半:配置存储、本机路由、迁移与损坏回退 │ │ ├─ index.ts # name / inject / apply │ │ ├─ store.ts # 文件存储:ajv 校验、0600 原子写、损坏隔离 │ │ └─ routes.ts # /panellist/api 处理器(回环栅栏、四个端点 + reveal) │ ├─ shared/ # 两个半边共用的类型与配置模型(无副作用) │ │ ├─ config.ts # 文档形状、内置图标名、id 前缀 │ │ ├─ model.ts # buildPlan:序列 → 每行该写什么(纯函数) │ │ ├─ edit.ts # 序列/分组的编辑算子(纯函数,保持「恰好一次」不变量) │ │ ├─ migrate.ts # schemaVersion 迁移框架 │ │ ├─ validate.ts # 浏览器半的宽容读取 │ │ └─ protocol.ts # 路由路径与跨半边 JSON 形状 │ └─ client/ # 浏览器半 │ ├─ index.tsx # apply(ctx):装配顺序、样式、自检、语言 │ ├─ controller.ts # 配置 → 计划 → 改写 / 分组行 的唯一环路 │ ├─ contracts.ts # 各单元之间的冻结接口 │ ├─ ctx.ts # 只做类型增补:ctx.slots / ctx.layout / 槽位声明 │ ├─ styles.ts # 单一注入样式表(按 id 聚合各单元的 CSS 块) │ ├─ mount.ts # 每页只保留一个挂载:二次 apply 会先退役上一个 │ ├─ service.ts # 可选 cordis 服务的容错读取 │ ├─ config/store.ts # 配置状态:乐观应用、失败回滚、导入导出 │ ├─ i18n/index.ts # 中英词条(键集合是封闭的) │ ├─ registry/rail.tsx # 注册表发现 + 改写 + 同步抖动 + 我们自己的行 │ ├─ rail/css.ts # DOM 规则集中在一处(隐藏、设置导航蒙版) │ ├─ rail/probe.ts # 四项自检与降级判定 │ ├─ groups/ # 组头行、捕获阶段点击拦截、菜单、分组页 │ ├─ settings/ # 设置页签:序列编辑、外观覆盖、配置、自检 │ └─ icons/ # 24 个内置图形 + 设置导航图形 + SVG 白名单净化 ├─ docs/ # 文档资产 │ ├─ images/ # README 截图 │ ├─ RELEASING.md # 发布流程(打 tag → CI 发布) │ └─ schema/config.schema.json # 配置的权威形状(宿主运行时读取) ├─ tests/ # 21 个 spec(模型、宿主半、改写、挂载守卫、探针、分组、设置、图标、文案一致性、打包契约) ├─ cordis.patch.yml # 自动挂载层 ├─ .github/workflows/release.yml # 打 tag 即发布(npm trusted publishing / OIDC) ├─ README.md # 英文 └─ README.zh-CN.md # 本文件

7.2 改写环节与同步抖动

registry/rail.tsx 读取轨道槽位,第一次见到某个注册项时记下它原本的 order/label/组件,此后只写有差异的部分。三条后果值得知道:

- 写前必比。 每个写入者都先比较,每个生成值(label thunk、图标包装)都按注册项缓存。这不是风格偏好:每次都是新身份就永远比不相等,于是这一轮报告「有变化」→ 抖动 → 再次进入,永不收敛。控制器另外把一次连发限制在 CASCADE_LIMIT 轮,并打日志告警。
- 该不该抖动由计划决定,而不是由写入决定。 字段直写不增加账本版本,所以没有任何重渲染。写完一批之后,这一轮会注册一个一次性行并在同一任务里销毁它——账本因此变更两次,外壳就会重读 order/label。而只改变「我们自己的包装所绘制的东西」的编辑(给已经带内置图标的面板再选一个内置图标、被已装好的 thunk 解析的第二次改名)一个字段都不写——所以这一轮同时比较一份「轨道渲染所依赖的一切」摘要(顺序、名称、图标、隐藏、抑制;面板与分组),只要它动了就抖动,哪怕无字段可写。挂载的第一轮只记住摘要、绝不因此抖动,所以新装仍是严格 no-op。
- 我们的包装是可识别的。 包装在行被隐藏时渲染标记而非图形,而贡献者的组件只存在于它的闭包里——任何回读 entry.component 的地方都可能拿到我们自己的包装。因此每个包装都登记进一个页面级 WeakMap(包装 → 取被捕获组件的 getter);捕获环节发现是自己的包装就先拆开并认领该槽位,nativeGlyph 出口再拆一次。没有这一步时,隐藏面板与分组成员在设置页里的预览是一个空盒子。

7.3 DOM 足迹

只有一张样式表,注入一次并按 document id 复用(bundle 被重新求值也不会出现第二份)。隐藏是一条 :has() 规则,作用在 nav[class="_panelList"] > button[class*="_panelRow"] 上——glyph 里渲染出标记 span 的行会消失。因为标记由 React 渲染,这条规则是声明式的:没有观察器、没有挪动节点、没有布局抖动。hidden = 「除非自己是激活页否则隐藏」;suppressed = 「无条件隐藏,它显示在分组里」。还有一条同样声明式的规则负责摆放外壳自带的「更多」行(§2.12):锚点取 order: 1,必须排在它后面的行取 order: 2,于是 flex 容器把它画在用户选定的序列位置上,一个节点都不挪;而且只有确实需要移动时才会输出这条规则。

7.4 分组行与分组菜单

分组行是本插件向 sidebar.panellist 的真实注册,并在同一个 key 上带一个 main 页:外壳按 order 摆放它、渲染它的 label,并把两者同时用作无障碍名称与收起态提示。sync() 是幂等的——只有顺序或解析后文字真的变了的行才重新注册,因为无谓的重注册会重挂用户可能正在交互的行。

这一行是外壳的单个 ,所以组头 glyph 在它的祖先按钮上装捕获阶段的 click/keydown 监听:捕获先于根部的 React 委托处理器,因此一次 stopPropagation() 就能让外壳自己的 selectPanel 永不执行,而 preventDefault() 取消该行的默认激活。菜单本体是挂在 document.body 的覆盖层,位置由该行的 getBoundingClientRect() 算出,因此能溢出侧边栏的裁剪;它在显示前先量一次尺寸,Esc 或外部点击即关闭。

组头高亮取布局服务公开的读取面(ctx.layout.panelInfo),而不是外壳的 aria-current:外壳只在「这一行本身就是激活页」时高亮,而分组行永远不是,所以这里由成员关系驱动高亮。

7.5 设置导航的标记

settings.section 契约没有图标字段,外壳按 section id 从一份封闭清单里挑导航图标,所以这一行按可见文本定位:document.body 上的 MutationObserver 把 [role="dialog"] nav button 的文本与本插件自己的本地化标签比对,命中则打上 data-dsh-panellist-settings-nav,样式表据此隐藏外壳自带的 并在前面画出我们的蒙版图形。全部工作在 dispose 时撤销;若始终匹配不到,就保留原生齿轮并通过自检报告——绝不报错。

8. 首版范围与非目标

首版包含:序列编辑(拖拽 + 键盘兜底)、分组、显示/隐藏(含分组内成员)、名称与图标覆盖(原图标 / 内置图标集 / 粘贴 SVG)、配置导入导出、孤儿保留与清理、中英双语界面、自检与只读降级,以及这套双语 README。

首版不做(留给后续版本):分组内搜索、轨道上的右键快捷菜单、多语言分别填写的名称、对真实 DSH 界面的 E2E。

非目标:管理右栏卡片页签;管理「设置」对话框自身的页签顺序;改动任何插件页面的内容;替代 DSH 自带的面板列表。

9. 里程碑

1. ✅ 设计——与用户敲定行为规范;
2. ✅ 骨架——tsdown + tsc + vitest、清单与自动挂载层;
3. ✅ 最小闭环——注册表改写(顺序/名称/图标/隐藏标记)→ 隐藏 → 设置页签 → 持久化;
4. ✅ 分组——组头行、分组菜单、拖拽编辑、分组页;
5. ✅ 加固——四项自检与只读降级、SVG 净化、导入导出、孤儿清理、中英双语;
6. ✅ 真机迭代——插件已在真实 desktop profile 里跑过若干轮反馈:设置页从第一次打开就列出整条轨道、隐藏与分组行为正确、图标集按外壳自己的美术重绘、分组内成员隐藏落地、自检的启动顺序竞态被修掉;
7. ✅ 发布——pnpm check 全绿(21 个 spec / 325 条用例,见 §6);代码在 GitHub 上并打上 v0.1.0,已发布到 npm(dsh-panellist),并已向 awesome-dsh-plugin 提交收录。

不启动应用也能做的静态校验

挂载层本身可以离线验:用 CLI 把本仓库的 patch 叠加到任意非 desktop profile 上,只导出组合结果(desktop profile 由 Electron 应用独占管理,CLI 会直接拒绝,这是刻意的)。

造一个临时 profile(从内置 web 模板派生),叠加本仓库的挂载层,只打印组合树
dsh panellist-dryrun --from-default-profile web \
--patch E:/ai-code-space/dsh-panellist/cordis.patch.yml --dump-config

组合树里应当出现下面这两行(已实测):

== E:/ai-code-space/dsh-panellist/cordis.patch.yml
- id: panellist
name: dsh-panellist

这条命令不启动任何服务、不装依赖;验证完把 $DSH_HOME/profiles/panellist-dryrun 删掉即可。tests/manifest.spec.ts 把这几个一致性断言(包名 = 插件 name 导出 = patch 里的 name、运行时读取的文件都在 files 里、schema 版本与路由前缀单一来源)变成了每次 pnpm check 都会跑的用例。

许可

MIT
预览分享卡片长什么样 ↓qwq1999/dsh-panellist
分享 · DeepSeek Harness Hub
qwq1999/dsh-panellist

DSHDeepSeek Harness客户端「面板列表」的管理插件。

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部