- Home
- Plugin
- Miglioramenti UI
- dsh-scope-manager
dsh-scope-manager
nanaaya98/dsh-scope-manager
Scope manager for DeepSeek Harness: binds presets, persona cards, tools, skills and MCP connections to global / workspace / session layers, with a sidebar.
Installazione
dsh plugin --profile web add github:nanaaya98/dsh-scope-managerREADME
功能管理和预设(dsh-scope-manager)
一个带侧边栏的 DSH 管理插件(侧栏里叫「功能管理和预设」,包名 dsh-scope-manager):把「哪些工具、哪些技能、用哪张角色卡」按 全局 / 工作区 / 会话 三层管起来。
优先级:会话 > 工作区 > 全局。没绑预设的节点继承上级;顶层不绑就是「默认预设」——全部工具与技能都启用,之后新增的也自动算启用。
六个页签(作用域 / 预设 / 角色卡 / 插件 / MCP / 技能)
| 页签 | 干什么 |
|---|---|
| 作用域 | 一张层级图:全局 → 各工作区 → 工作区内的会话,每个节点只绑一个预设(不绑 = 继承上级,绑「默认预设」= 全部工具技能都启用,含以后新增的);工作区可重命名/移除,会话可归档/取消归档/置顶/删除 |
| 预设 | 「一套设定」的打包:角色卡 + 工具名单 + 技能名单 + 系统提示词段落名单,可被任意一层引用;名单同样按两级分组勾选。工具/技能的候选集取全量(所有 agent 的并集),不会因为当前会话绑了窄预设就报「不在当前列表里」。一套预设一张卡片(名字 + 权限/新工具徽标 + 说明 +「关掉几把工具 / 几个技能 / 几段提示词 · 角色卡是谁」),编辑与删除在卡片最下面那一行;点卡片进详情改。挨着「作用域」摆:先在作用域里看层级,再回这里改那一套预设 |
| 角色卡 | 角色卡模板集。一张卡一片卡片(首字图标 + 名字 + 注入方式徽标 + 一段说明或正文摘要 +「世界观/角色/约束」各多少字),编辑与删除在卡片最下面那一行;点卡片进详情改三段正文。卡片本身写在「世界观 / 角色设定 / 行为约束」三段里(分别对应系统提示词的 harness:identity、deployment:persona-prefix、deployment:persona-suffix),并可选两种注入方式(详情页顶部就是模式选择器,两档并排、各带一句说明;切换模式只改注入方式,下面三个框里的字一个字都不动,要保存才生效):角色卡上下文(拼成一条合成 user 消息)或系统提示词覆盖(装配时直接盖掉那两段文字:世界观 → harness:identity,角色设定 + 行为约束 → deployment:persona-prefix,行为约束接在末尾;覆盖模式下某一格留空也会盖,等于把那一段抹掉)。供预设与各层引用 |
| 插件 | 两个子页:系统插件(宿主自带的 @deepseek-ai/dsh-*,只显示这个会话里真正在跑的)与用户插件(自己装进 profile 的,连「一个工具都没注册」的换肤/挂件类也算在内)。每个插件一张卡片:名字 + 版本 + 一句简介 + 「几把工具 / 几段提示词」,点卡片进详情 —— 详情里就是它自己带的系统提示词段落(可改写、可开关注入、可关联工具)和它注册的工具(可改显示名、写描述备注)。这一页是全量清单,跟当前会话绑了什么预设无关:插件注册了什么就列什么,用来查询、改显示名、写备注、指定归属 —— 预设决定的是「最后给会话调用的那份」,不在这页体现。MCP 那批不在这里:它们跟着「MCP」页的连接器走。备注、改写、是否注入都只有全局一份——改了就对所有会话生效。接不到任何插件的段落(既认不出来源、又接不上工具组的那几段)列在系统插件子页底部的「未归组的提示词」里,每段同样有「注入 / 归属 / 编辑」——归属决定这段跟着哪几把工具走(关联着的工具一个都没启用,它就不注入);有来源插件的段落候选就是那个包自己的工具,未归组的段落候选是全量工具名。同一个人点出来的「归属」上半截还能指定这段算哪个插件的东西(下拉里是全量插件),选完这段就搬进那张插件卡片的系统提示词里、从「未归组」消失;选「不归属」又搬回来 |
| 技能 | 两个子页:已安装(内置与第三方技能一览,能读能改 SKILL.md,改之前自动留 .bak,还能停用 / 启用)与技能市场(直接读 skillhub.cn,点「安装」就装进本机) |
| MCP | MCP 连接器与它们的工具:谁在跑、跑没跑起来、有几个工具。一台连接器一张卡片(名字 + 状态徽标 + 一句「怎么连的」+ 工具数),启用/停用、编辑、探测、复制、删除都排在卡片最下面那一行;点卡片正文进详情,详情里才是它的系统提示词和工具清单(逐个开关、逐个写备注)。新增 / 编辑走弹窗(第一个字段是给人看的「名称」) |
作用域的三种取值,就是存储里的三种写法:
| 界面上的选法 | 存的东西 | 含义 |
|---|---|---|
| 继承上级 | 这层没有 scene 键 | 往上找:会话 → 工作区 → 全局 |
| 默认预设 | scene: null | 从这里往下全部放开,角色卡/名单/备注都不会从下面漏上来 |
| 预设「X」 | scene: "X" | 用这套预设 |
勾选的语义
工具与技能列表默认全勾:勾着 = 这一层让它生效,取消勾选 = 这一层停用它。
- 全勾 = 这一层不限制(配置里干脆不出现这个键),所以新装的插件/新注册的工具默认就是可用的,不会被一份旧的名单悄悄挡在外面。
- 只有真的取消了几项,才会写进「停用名单」(
{ mode: 'deny', names: [...] })。 mode: 'allow'形态的名单(只放行)读进来会按当前候选集反推成停用名单,语义不变。
MCP 页(连接器与工具)
一块 MCP server 在 DSH 里是一条 loader 行:名字固定 @deepseek-ai/dsh-mcp-client,
config.serverName 是服务器名([A-Za-z0-9_-]{1,32},全局唯一),transport 二选一
(stdio 用 command/args/env/cwd,streamable-http 用 url/headers)。
它挂上以后注册的工具叫 mcp__<serverName>__<toolName>。
真源是补丁文件,不是插件内存。这一页读写的是 cordis.patch.yml:
| 层 | 路径 | 作用范围 |
|---|---|---|
| 全局 | $DSH_HOME/cordis.patch.yml | 所有 profile 共用 |
| 本 profile | <profile>/cordis.patch.yml | 只影响当前 profile |
宿主按「bundle → profile 层 → 全局层」叠补丁,全局层优先级更高:两层有同名 id 时, profile 那行是死的(页面上标「重复条目(这条不生效)」)。改一台已经存在的服务器时, 本插件就地改它所在的那一层,不另起一行。
新写的连接器一律进全局层(所有 profile 共用,启用 = 默认启用), 细分到「哪个预设用哪些工具」去「预设」页挑;profile 层里已经有的行仍然就地改。
写盘的规矩和宿主一致:新增行必须进 - insert: 块(裸 - id: 是「覆盖既有行」,
不是新增);!!js 表达式标签原样保留;原子写(临时文件 + rename)。
写完宿主靠 dsh-hmr 监视这两个补丁文件自动重载,不用叫它重启。
状态怎么判
页面上的状态徽标是三处事实对出来的,不是猜的:
| 补丁里有 | loader 里有 | 工具表里有 | 状态 |
|---|---|---|---|
| — | — | — | 补丁里没有这一行 |
有,写着 disabled: true | — | — | 已停用(宿主不认它) |
| 有 | 没有 | — | 宿主没挂上(多半被另一层补丁盖住了) |
| 有 | 有,但没激活 | — | 宿主里挂着,但没激活(点「启用」会自动补挂,见下) |
| 有 | 有 | 一个都没有 | 已挂载但没连上(多半是地址/鉴权/网络) |
| 有 | 有 | 有 | 已连接(N 个工具) |
「配了 MCP 却没被识别」基本都落在中间三行里:常见的例子是
某个连接那行写着 disabled: true,所以宿主一个工具都没给它。
点「启用」为什么不会再卡在「挂着但没激活」
宿主 loader 的 entry.update() 里,disabled 从 true 变回 false 时不会重新 init() ——
它先看 this.fiber?.uid,只要旧 fiber 还没拆干净(dispose() 是异步的),就只走「改配置」
那条分支;于是 entry 在、fiber 不在,页面上一辈子是「宿主里挂着,但没激活」。
所以这一页启用一台时不靠等:写盘 → 让宿主重读补丁(hmr.reconcileProfile())→ 短等
(5 秒)→ 还不通就直接自己补挂:先 fiber.dispose(),轮询到 fiber.uid 变 null
(最多 3 秒),再 entry.init(),然后再等(最多 20 秒)它连上。实测往返十几次都直接落到
「已连接(N 个工具)」。
页面上那颗「刷新」按钮做两件事:让宿主重读一遍补丁,再把所有「补丁里有、没停用、 但就是没激活」的连接器挨个补挂一遍,然后重新读一次列表。按下就变成「刷新中…」, 下面直接出一行结果(比如「已让宿主重新读了一遍补丁,顺手把 2 台重新挂上了」)。
系统提示词绑在连接器上
MCP 服务器连上以后会往系统提示词里推一段目录(段落名固定 mcp:<服务器名>)。这一页把它
挂在那台连接器自己下面:连接器条目里一行「▸ 系统提示词(注入中 / 不注入)」,展开是
段落名、注入勾选、原文、以及「改写这段 / 保存改写 / 还原原文」。
改动落到的地方和插件页一致:注入开关 → layers.json 的 sectionStates,
改写 → sectionOverrides(留空 = 还原原文)。同一段提示词只在这一页维护,
插件页不重复列 mcp: 开头的段落(有连接器那张卡就行)。
只按平台的方式连,不碰别的插件
连接器只认平台的补丁行(cordis.patch.yml):这一页写它、启停它、删它,就这些。
市场里的 dsh-mcp-connector 也把连接记在自己的 unit 状态里
($DSH_HOME/storages/mcp_connector.json),但这一页只读不写——读它只是为了给
还没在这里起过名的连接器兜底显示名(「面板服务」这种);起过名的用自己的表
(layers.json 的 mcpNames)。
页面上能做什么
- 新增 / 编辑:点开是一个弹窗(不是页面底部那一长条表单),字段按这个顺序排:
名称 → 服务器名 → 传输方式 → URL / 命令 / 参数 / 环境变量 / 工作目录 / 请求头 →
要不要先停用。点遮罩空白处或右上角 × 关掉。
- 名称是给人看的(卡片上显示「面板服务」那种),
只存在这个插件自己的表里(
layers.json的mcpNames),不会写进补丁 —— 因为宿主 loader entry 的config有 schema,塞不认识的键有风险。 不填就退回显示服务器名;另一个插件(dsh-mcp-connector)已经给这台起过名时, 页面会拿它的名字兜底。 - 服务器名才是机器认的(
mcp__<服务器名>__<工具名>);改它会连工具名前缀一起改。
- 名称是给人看的(卡片上显示「面板服务」那种),
只存在这个插件自己的表里(
- 停用 / 启用:停用写
disabled: true,启用把这个键删掉(另一个插件也管着这台时,两边一起改)。 - 探测:stdio 查命令在不在 PATH;HTTP 按 MCP 的规矩 POST 一个
initialize(带上这一行自己的请求头,5 秒超时;只看状态码,不读 SSE 长连接)。 - 复制:留在原来那一层,复制出一台新的(新 id,服务器名自动加后缀
-2, 因为工具前缀mcp__<服务器名>__必须唯一)。 - 删除:删掉补丁里的那一行(连它那条显示名也一起清掉,不留没人用的名字)。
- 逐工具开关:每台服务器的工具清单就折在它自己那条目里 —— 条目底部一行 「▸ 工具(N)」,点一下展开,工具名 + 说明 + 一个勾选框一个工具。 顶上的搜索框搜的是所有连接器的工具清单,搜到哪台,那台会自动展开。
- 版式和插件页/技能页是同一套(三页共用同一个卡片组件):列表 = 一排卡片
(名字 + 状态徽标 + 「HTTP/stdio · 地址」+ 工具数),动作按钮全在卡片最下面那一行;
点卡片正文进详情 —— 返回条 → 头部卡片(名称、服务器名、状态、地址、
id=、探测结果)→ 「系统提示词」(提示词在前)→「工具」(工具在后,逐个开关 + 逐个备注)。 - 工具也能写备注:插件详情里的工具行右边那颗「备注」,和插件页是同一个编辑器、同一张表
(全局层的
tools)——显示名、描述备注、「作为注入描述」都能改;在哪边改,另一边 立刻就是改完的样子。
逐工具开关存在哪
工具开关不走层里的 toolFilter——那个键是「整块取第一个」的(会话 → 工作区 → 全局),
往全局写 deny 名单会被会话层整块盖掉。所以另开一把跨层共用的总闸:存储顶层
mcpTools[toolName].disabled,在 assemble 过滤工具定义时和 ctx.tools.guard(...)
调用兜底时都生效(两头都拦,改完下一轮就生效,也能立刻反悔)。
五个落点(它到底改了模型看到的什么)
- 工具描述:
ctx.on('system-prompt/assemble')里只替换description,parameters一个字不动;没勾「作为注入描述」的工具原样保留。 - 工具可见性:在
system-prompt/assemble里把名单外的工具从工具定义数组里剔除 —— 模型拿到的tools里根本没有那一条。这是按次计算的,改完下一轮就生效、也能立刻反悔,不像tools.restrict()那样留下一层撤不掉的 deny(插件一热重载,撤销函数就跟着实例一起没了,工具被永久锁死)。 - 工具调用:
ctx.tools.guard(...)在调用时兜一层(技能名单也在这里拦,按exec.arguments.name)。 - 角色卡:
ctx.on('agent/pre-step', ..., { prepend: true })往消息列表前面插一条合成 user 消息,内容没变就不重发;选了「系统提示词覆盖」的卡片不走这条,改在 assemble 瀑布里把三段文字盖到对应段落上(优先级高于普通段落覆写)。 —— 不走systemPrompt:只要有任一 section 标了complete: true,宿主会把 sections 还原成只剩那一个,写进去也白写。 - 系统提示词段落:assemble 钩子用
{ prepend: true }站到最外层,await next()之后按名单把整段sections摘掉。 —— 段落和工具是两套东西:别的插件(比如 meme 的「斗图规则」)是往assembled.sections里push的,跟它的工具开没开毫无关系,所以只能在 assemble 末尾摘。 —— 段落只有名字没有归属字段,所以「见一次记一次」,落盘$DSH_HOME/scope-manager/sections.json,界面上按名字里的冒号前缀分组。 —— 来源:反查已装插件的源码(找谁写了这个段落名),在行尾显示插件:行号,鼠标悬停看文件与那一行代码。查不到就标「来源未定位」。 —— 覆写:行尾的「编辑」会先把整段原文完整展示出来(可滚动、只读),下面才是覆写框;只改装配那一刻送进模型的 text,不写回插件源码;清空保存即取消。 —— 会自动长:每装一个新插件,只要它在装配时 push 了段落,下一次装配后这个目录就多出对应条目(新名字还会顺手触发一次来源反查)。不需要手动刷新。 —— 会标「本次未出现」:目录是只增不改的缓存,插件卸了条目也还在;所以列表会对照最近一次装配标出没出现的那些(也可能是按需注入)。覆写按名字存,不受它影响,插件装回来照样生效。 —— 可清理:列表下方出现「清理未出现的条目(N)」时,点一下就把这些陈条目从目录里删掉。带覆写的无论如何都留着(删了覆写变孤儿,插件装回来你改过的字就没了);按需注入的段落被清也没事,它下次出现会自己记回来。 - MCP 工具:「MCP」页关掉的工具,在 assemble 里从工具定义数组剔除(和名单外的工具一个待遇),调用期再由
ctx.tools.guard(...)兜一层。
段落怎么和工具组对上
插件详情里的「系统提示词」那一块,列的就是这个插件自己的段落。接上的线索有两条:
- 段落名带工具前缀:
tool:bash→ 工具bash→ 它所在的那个组。这是最准的一条(名字是插件自己起的,跟工具名一字不差)。 - 段落的来源插件:反查出来是
@deepseek-ai/dsh-tool-bash→ 包名dsh-tool-bash→ 组归属者也是它。名字不是工具前缀的段落靠这条接上(表情包的dsh-expression:companion归dsh-meme,genui 的genui:fence归dsh-genui)。
宿主自带的段落(@deepseek-ai/* 的包)大多只有段落、没有工具,所以接不到任何工具组 —— 它们的归属查出来(来源反查 + 一张内置的系统插件对照表)之后,就挂在那个官方包名下、算系统工具,跟别的系统插件一起列出来。人设那三段(世界观 / 角色设定 / 收尾口径)归「角色卡」管、永远注入,插件页不重复列。
系统插件对照表(
lib/system-sections.js):宿主自带的段落 → 归属包名,先查表、再查源码。表里没有的、或者表跟实际不一致的,都以源码反查为准 —— 宿主以后加了新的系统段落,不用改这个包也能认出来。
「未归组的提示词」那一块留给既认不出来源、又接不上工具的段落(少数情况):直接在这里开关和改写。
- 人点出来的归属(第三条线索,优先级最高):段落卡片上那颗「归属」按钮,上半截是个插件下拉(全量插件,含一个工具都没注册的换肤/挂件类,含宿主自带的包)。选了谁保存,这段就搬到那张插件卡片的系统提示词里、从「未归组」消失;选「不归属」又搬回来。存在
layers.json的sectionOwners[段落名] = 归属包名,全局一份、不分层。为什么需要它:宿主给的段落只有{name, text},反查源码也猜不出归属的那些,否则永远没人能认领。
段落页里的两种操作,都写在当前选中的那一层(段落页顶部那个「全局层 / 工作区层 / 当前会话层」下拉说了算):
| 操作 | 存到哪 | 优先级 |
|---|---|---|
| 逐段选「跟随名单 / 强制注入 / 强制不注入」 | 该层的 sectionStates[name] | 点名比整张名单细:预设里排掉的一段,会话里可以点名放回来 |
| 逐段改写文字 | 该层的 sectionOverrides[name] | 层内覆写 > 目录里的全局覆写 > 原文 |
「跟随名单」= 把这个键删掉,听整张名单的;「强制注入 / 强制不注入」= 点名。名单本身(sectionFilter)还是挂在那套预设上的名单:预设里排掉的段落在本层仍会被摘掉,除非本层点名放回来。
固定名次(重排)
装配末尾还有一步「重排」,把模型读到的顺序钉成三段式:
| 名次 | 是谁 |
|---|---|
| 1、2、3 | harness:identity → deployment:persona-prefix → deployment:persona-suffix |
| 中间 | 其余官方包(@deepseek-ai/*)的段落,保持宿主原本的 order 顺序 |
| 末尾 | 第三方插件注入的段落,相对顺序不变 —— 等价于 push 型 |
人设三条在宿主里原本隔着几千个 order(identity −1000、prefix 0、suffix 10200),中间夹着
全部工具手册,读起来是断的;这里把它们抽出来挨着放。名次写在 lib/pin-order.js 的
PINNED_SECTIONS 里,数组顺序就是名次,改一行就能换。
「第三方」的判据是 lib/tool-owner.js 的 isOfficialPlugin()(包名不以 @deepseek-ai/ 开头),
所以 @dsh-custom/dsh-browser-desktop、dsh-kaomoji、@changfenhuang/dsh-genui 的段落都会沉到末尾。
重排是幂等的:同一批段落排两次结果一模一样,已经排好的一轮 moved 就是 0(不重复 record)。
缺段也不报警——预设里没有 deployment:persona-suffix 时就只钉存在的那两条。
面板上的体现:段落列表每行前面有名次,被钉住的带「固定名次」徽章(接口返回的 pinned 字段)。
注意名次是装配时的数字,所以它跟宿主源码里的 SECTION_ORDERS 不一样——那是宿主排的,
这是本插件改完之后模型真正读到的顺序。
会话里的「会话信息」
会话标题下面那排视图页签(对话 / 轨迹 / 刷过的视频 / 上下文)里多一条 会话信息,
排在「轨迹」后面(conversation.view,id scm-session-view,order 15)。只读,按当前
会话给一份现状快照:
- 预设、角色卡(名字 / 注入方式 / 三段正文)、权限;
- 工具:先按 系统工具 / 插件工具 / MCP 工具 三类分开数,每类下面再按归属者折叠展开;
- 系统提示词:注入 / 停用 / 空段计数,再按宿主真实装配顺序(
position)把每一段的原文 摊开——这就是模型实际读到的那份提示词,空段标「空段(宿主丢弃)」。
数据走已有的 scope-context、tools-list、sections-list,没有新接口;右上角有「刷新」。
页签行的宽度有限:visibleViewLimit = max(1, floor((行宽-48)/120)),排不下的会落进
「⋯ 更多会话视图」。order 排在 chat(0)、trajectory(10) 之后,默认可见。
注册这类槽要注意:conversation.view 由 conversation.session 条目声明,必须先
slots.inject('conversation.view', …) 再 slots.register({ name: 'conversation.view', … }),
否则抛 slot "conversation.view" is not declared (a parent entry's children table must declare it)。
conversation.session.header.utilities、sidebar.footer.action 这几个槽本插件碰不到,别再试。
右侧边栏那份同名面板已经删掉(用户要求),只剩页签这一处入口。
装进 profile
在线安装(npm / GitHub)
# npm
dsh plugin --profile <profile> add dsh-scope-manager
# GitHub
dsh plugin --profile <profile> add github:nanaaya98/dsh-scope-manager
装完把 dsh-scope-manager 加进 profile 的 dsh.profile.bundles 列表,重启 DSH 即生效。
本地目录
profile 的 package.json:
{
"dependencies": {
"dsh-scope-manager": "link:/path/to/dsh-scope-manager"
},
"dsh": { "profile": { "bundles": ["...", "dsh-scope-manager"] } }
}
再让 node_modules/dsh-scope-manager 指过来(ln -s)即可。宿主热重载会自己把它挂上;没有 HMR 时重启 DSH 生效。
改完宿主那一半,怎么让它立刻生效
插件分两半:
- 界面那一半(
lib/client.js)每次打开都从磁盘读 → 刷新浏览器就看到新的。 - 宿主那一半(
lib/index.js、lib/http-api.js、lib/mcp.js、lib/mcp-manager.js) 是宿主进程里 import 进去的模块 → 改了要重新 import 才算生效。
实测:改一下 profile 的 cordis.patch.yml(内容有变化,不是只 touch 时间戳),
宿主的 reconcile 就会把这个插件的模块重新 import 一遍,新代码立刻在跑 ——
不用重启 DSH(重启会连带把跑在同一进程里的 agent loop 打断)。
printf '\n# reload\n' >> <profile>/cordis.patch.yml # 触发一次 reconcile
# 改完记得把这一行删回去
数据落在哪
$DSH_HOME/scope-manager/(默认 ~/.dsh/scope-manager/)
layers.json— 三层设定 + 工具归属修正 +mcpTools(MCP 逐工具总闸)+mcpNames(MCP 连接器显示名)+sectionOwners(段落的「归属插件」,人点出来的那份)personas/<id>.md+<id>.json— 角色卡正文与元信息scenes/<id>.json— 预设(存储里仍叫 scene)sections.json— 系统提示词段落目录(含来源定位与全局覆写)
写盘一律「临时文件 + rename」,改一层只动这一层。
技能启停
宿主没有独立的技能开关表,停用的语义落在 frontmatter 的两个调用面开关上:
停用 = 写 disable-model-invocation: true + user-invocable: false;
启用 = 把这两个键删掉(和 dsh-mcp-studio 的 rewriteSkillPolicy 同一个口径)。
技能文件是 $DSH_HOME/skills/<name>/SKILL.md,写完宿主约 200ms 后广播 skills/change,
界面自己刷新。页面上停用的卡片会挂「已停用」徽标。
技能列表也是一排卡片(首字图标 + 名字 + 来源徽标 + 一句简介 + 来源说明),
停用/启用、读取、删除都在卡片最下面那一行;点卡片进详情,详情里是它的完整
SKILL.md(可直接改、点保存写回)和路径、来源。
已安装列表按三组折叠(点组标题收起 / 展开,顺序固定):
| 组 | 装的是谁 | 卡片上的标签 |
|---|---|---|
| 内置 | dsh-agent-preset/skills 里跟着 agent 预设一起加载的;official-skills 官方技能包里的 | 内置 · @deepseek-ai/dsh-agent-preset / 内置 · official-skills |
| 插件 | 插件包自己带的(node_modules/.../skills/) | 插件 · <包名> |
| 其他 | 自己从技能市场装的、在会话里建的、项目 .dsh/skills 里的、宿主配置里额外加的目录 | 其他 |
标签后面的包名取全(作用域的包要连作用域一起,比如 @changfenhuang/dsh-genui,
只取第一段会变成 @changfenhuang)。
「算哪一组」按宿主报的 source 判(user-dsh / user-agents → 其他),不按路径正则 ——
/root/.dsh/skills/xxx(用户级)里也有 .dsh/skills,只看路径会被误判成「项目内」。
卸载自装技能
只有「其他」组里自装的那一类带「删除」按钮(从技能市场装的、或用会话自己建的,都落在
$DSH_HOME/skills 里)。内置(预设自带 / 官方技能包)、插件自带都不给删 —— 删了下次升级又回来,
还会把包弄坏(后端会直接拒绝并说明原因)。
点删除会先确认一次,确认后把整个技能目录直接删掉(rm -rf,硬删,没有回收站,
不能撤销)。删完宿主约 200ms 内广播 skills/change,列表和左上角计数自己就少了那一条。
删之前会过两道判断:位置必须是自装(user),路径必须真的是一个技能子目录
(…/skills 这个根自己、或 node_modules 那层都不让删)。
技能市场(技能页的第二个子页)
数据不是我们自己的,直接读 SkillHub 的公开接口(不用登录、不用 key):
| 用途 | 接口 |
|---|---|
| 列表 / 搜索 | GET https://api.skillhub.cn/api/skills?page=1&pageSize=20&keyword=&category=&sortBy=&order= |
| 分类 | GET https://api.skillhub.cn/api/v1/categories |
| 下载 | GET https://api.skillhub.cn/api/v1/download?slug=<slug> → 302 到一个 zip |
注意两件事:https://skillhub.cn/ 本身是个 SPA 壳,/api/... 打在那个域名上只会拿到 HTML,
必须打 api.skillhub.cn;列表只在 /api/skills(/api/v1/skills?page= 会 405)。
装到哪儿:一律装进用户级技能目录 $DSH_HOME/skills/<slug>/ —— 就是
dsh-skill-filesystem 的 roots() 里那个 user-dsh 根,所有项目都能用,界面上没有选项。
(后端 installSkill 还留着 skillsRoot 参数,以后想装到项目级 .dsh/skills 只要传就行,
只是页面不暴露。)
装完会把最终落地的绝对路径回显出来(→ /root/.dsh/skills/xxx),不用猜。
「安装」就是下 zip + 解开:
- 下
api/v1/download?slug=(跟随 302,60 秒超时); - 用自己写的 zip 读解(stored / deflate 都支持,
node:zlib就够,不装第三方依赖); - 每个条目都过一遍「必须在目标目录里」的检查 —— 绝对路径、
..、\0一律拒(zip slip); - 解到
$DSH_HOME/skills/<slug>/,宿主 chokidar 监视这个目录,写完约 200ms 广播skills/change,不用重启,「已安装」那一页自己就会多出一条。
已装过的 slug 会被拦下来并提示「要覆盖就勾上覆盖」(页面上那颗勾选框),勾了才覆盖。
列表是瀑布流:一页 20 条,滚到底自动续下一页(一个哨兵 div + IntersectionObserver,
rootMargin 240px 提前拉),顶上那行实时报「共 N 个 · 已加载 M」;到底了显示「到底了」,
不再请求下一页。排序有四档:相关度(score)、热门(按安装数)、下载多、星标多 ——
走接口的 sortBy / order=desc。往下传的 sortBy 只放行 score / installs / downloads /
stars 这四个:接口遇到不认识的排序字段会当成没排序,悄悄给回一份热门榜,看起来就像筛选没生效。
搜索怎么排:浏览时默认按热门;一旦填了搜索词就换成相关度。带搜索词时接口那边的 score
是按这次查询算的相关度分(同一个技能换个关键词分值就变),接口就按它出这一页。接口的关键词是
全文召回,只要描述里出现过这个词就会被收进来,所以前端还按贴合档位重排一遍:名字、发布者标识
或 slug 里带搜索词的排在只写在描述里的前面,同一档里再按你选的那档指标排。档位只在这一页里
生效 —— 翻页时不动已经列出来的条目,滚动位置才不会跳。
点卡片看详情:整张卡片可点(cursor: pointer),弹出「技能详情」—— 图标 / 名字 / 版本 /
@作者/slug / 分类 / 作者 / 下载·安装·星标 / 两个安全扫描的结论 / 中文摘要 / 英文摘要 /
标签 / 来源页链接,底下三颗按钮「安装到本机」「在 skillhub.cn 打开」「关闭」。详情比列表多
一个请求(GET api/v1/skills/<slug>),点开先出「读取中」,回来再填。
图标怎么来的:市场条目自带 iconUrl,有就用图。没有图标的(以及「已安装」里所有技能 ——
技能目录里根本没有图标)用「名字的第一个字」当图标:英文名首字母大写(find-skills → F,
gmai-free-lw-06 → G),中文名取第一个字;底色按名字散列到一个固定色,同一个名字每次同色。
解压到一半失败会把半成品目录删掉,不留一个坏技能在那儿。技能的名字和描述从 SKILL.md 的
frontmatter 里读 —— 下载包里的 _meta.json 只有版本号。
装完还会顺手把 name 揉成宿主认的样子:宿主只认 kebab-case
(dsh-skill 里 SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/),而市场里不少技能把 name:
写成展示名(Word / DOCX、Poetry Daily Art)—— 这种宿主会静默丢掉
(日志里 skill file ... ignored: invalid skill name),表现就是「装到目录里了,但技能页没有」。
所以解压后会读 SKILL.md,name 不合法就改成 slug 那种 kebab 形式、把原展示名塞进 title:
(本来合法就一个字不动);改过的话「装好了」那行会写出来,比如
「装好了:word-docx(2 个文件)→ /root/.dsh/skills/word-docx (市场里写的名字是「Word / DOCX」,
宿主只认 kebab-case,已改成「word-docx」)」。
自带测试
node test/smoke.mjs
纯 Node、假 ctx,292 项检查:HTTP 门禁、三层覆盖与逐字段合并、assemble 改描述、名单剔除工具定义、guard 拦截、角色卡不重发、预设被显式清空、四类分组、段落↔工具组关联、逐段点名开关与逐段覆写、MCP 补丁行的解析/新增/启停/删除与往返稳定、MCP 管理器真写补丁(新增落全局层、改名跟不跟得上、撞名拦不拦、复制加后缀)、MCP 逐工具总闸、外部插件(dsh-mcp-connector)只读、不写它的状态文件、MCP 连接器显示名(存取、落盘、改名时搬家、删连接器时一起清掉)、MCP 状态七态判定与「启用救不回来时自己补挂」(dispose→等 uid→init,往返十几次都落到已连接)、remountStuck 只捞卡住的不动已跑的、插件页不列 mcp: 段落、MCP 页工具行的备注与工具页共用同一张表、技能来源标签的包名(含作用域包名两段)与三档分类桶(内置/插件/其他,每种来源都落在桶里)、技能启停的 frontmatter 开关、技能市场的 zip 解压与 zip slip 拒绝、非法 slug 与「已经装过了」的拦截、frontmatter 解析、装到指定技能根目录(用假 fetch 喂 zip,验证落点与读到的名字)、展示名被揉成 kebab(原展示名留在 title)、本来就合法的名字不动、没有 frontmatter 就补一个、技能位置按宿主的 source 判(用户目录不会被路径骗成项目内)、卸载自装技能(插件自带/预设自带的拒删、硬删、按路径删也行、技能根目录本身不删)、段落「归属到插件」(命中已有工具组就直接挂过去、归属到没工具的包补空组并标 pinnedOwner、归属到宿主自带的包也照挂不跳过、没人点过时行为不变、第三参不传也不炸、落盘与取消归属、缺段落名报 400)、插件页列全量工具(total 不因当前会话的名单缩水、allowed 只作数据字段给别的判断用、页面不拿它做任何标记)。
Plugin correlati
dsh-web (dsh-task-board)
zhu1090093659/dsh-web
dsh-web (dsh-web-all)
zhu1090093659/dsh-web
dsh-TUI
ccch1mneyyy/dsh-tui
dsh-web-ui (dsh-task-board)
zhu1090093659/dsh-web-ui