dsh-skill-tag
Tag-based skill filtering: attach tags to DSH skills, then hand each workspace or conversation only the subset its boolean tag expression selects (for example `pdf + docx` or `aegis * plugin`). Excluded skills are deny-shadowed out of the model catalog rather than merely hidden in the settings UI; s
Install
dsh plugin --profile web add dsh-skill-tag
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-skill-tag
给 DSH 的技能(skill)打标签,然后用一条布尔表达式在「工作区 / 对话」两级
选出真正要下发给模型的技能子集。
装了几十上百个 skill 之后,真正的痛点不是"怎么分类",而是"**这次对话只想让模型
看到哪几个**"。全部下发会撑爆上下文、也会让路由互相打架;手工一条条开关又太累。
这个插件把这件事变成一条表达式:
pdf + docx # 只要处理文档的技能 office * plugin # 只要 office 包带来的、且是插件相关 (aegis + tdd) * !wip # 两个都要,且排除 wip
它能做什么
能力 · 说明
全局标签 · 建/改名/删除标签,标签之间无层级
标签组模板 · 把表达式存成模板(前端组、只留本机技能…),可复用、可求值
布尔表达式 · +=OR、=AND、 优先、支持括号;改标签名会自动改写所有引用它的表达式
工作区级筛选 · 每个工作区一套条件,作为该工作区新建对话的初始模板;改工作区不影响已存在的对话
对话级筛选 · 每个对话有自己的快照:新建时继承一次,之后冻结;未配置过的对话 = 空条件 = 全部技能;可一键「继承工作区配置」重新拉取
禁用 / 手动添加 · 在标签命中之外额外放行某个技能,或强行屏蔽某个(即使它命中)
技能 ↔ 标签双向编辑 · 「tag 联系 skill」和「skill 联系 tag」两个方向都能改
打开技能所在文件夹 · 任何列出技能的地方都有 📁 按钮,一点即在文件管理器里定位
自带一个 skill · 安装后你的 agent 会获得 tagging-skills,能照着文档自己给技能打标签
界面预览
全部是真实实例截图(105 个技能 / 48 个标签 / 一个开了筛选的工作区),没有 mock。
图片源文件在 images/(不进 npm 包,README 里用 jsDelivr 绝对地址,GitHub 与 npm 都能显示;
之所以不走 raw.githubusercontent.com,是因为部分网络到该域名的 IPv6 不通,图片会间歇性加载失败)。
图 1 · 入口:会话行「…」→「管理标签」
<img src="https://cdn.jsdelivr.net/gh/aawef/dsh-skill-manager-tags@main/images/session-menu.png" width="240" alt="会话行菜单里的管理标签入口">
对话级的配置就挂在会话行的「…」菜单最下面,不用去翻设置页。
图 2 · 入口:命令面板里的 /skill-tag-manage
<img src="https://cdn.jsdelivr.net/gh/aawef/dsh-skill-manager-tags@main/images/command-palette.png" width="820" alt="命令面板中的管理对话标签命令">
在输入框敲 / 也能直接开「管理对话标签」——同一个界面,省一次点菜单。
图 3 · 打标签:设置 →「Skill 标签」
<img src="https://cdn.jsdelivr.net/gh/aawef/dsh-skill-manager-tags@main/images/tag-settings.png" width="620" alt="Skill 标签管理页:新建标签、按来源分组的标签列表">
三个页签分工明确:tag 联系 skill(一个标签包含哪些技能)、skill 联系 tag
(一个技能拥有哪些标签)、编辑标签组(写表达式存模板)。
标签按来源分组(来自:aegis 23 个 Skill),每条带 tagId 和「全局删除」。
图 4 · 一个技能看它身上有哪些标签
<img src="https://cdn.jsdelivr.net/gh/aawef/dsh-skill-manager-tags@main/images/skill-to-tags.png" width="640" alt="skill 联系 tag:左列技能、右列标签勾选">
左列 技能 (105) 每行标着它有几个标签;右边勾选即时保存,不用点确认。
右列会把该技能已有的标签排在列表最前面(并保持它们原来的相对顺序),所以不用滚动
去列表里找;顶部搜索框过滤之后,"已拥有优先"依然成立。
这是配条件前最常开的一页:先看清楚"这个技能到底被哪些标签命中"。
图 5 · 工作区「管理标签」:左写条件,右就是下发名单
<img src="https://cdn.jsdelivr.net/gh/aawef/dsh-skill-manager-tags@main/images/workspace-filter.png" width="900" alt="工作区管理标签双列界面">
左列是标签条件,下面实时显示解析结果和状态「有条件筛选(严格子集)」;
右列标题是 技能 (54) · 模型可见 48:前一个数是命中表达式的全部技能,
后一个数是其中真正下发给模型的那些。两者不等时标题下方会多出橙色说明,
列出差额的去向——X 个只能 /名字 手动调用(SKILL.md 里写了 disable-model-invocation: true)
和 Y 个宿主没注册(扫描到了但宿主没装),对应行也会带 仅手动 / 未注册 徽标并置灰。
还有第三种状态 暂不可判定:这一趟没拿到完整的宿主视角(比如该会话没在运行,
宿主的 skill registry 只给了全局层),此时"没返回"不等于"没注册",这些技能带灰色虚线徽标、
不置灰,也不计入 模型可见 N。这三种状态的区别与成因见下一节。
每行都可以「查看 tag」「禁用」,📁 直接打开技能所在文件夹。列表顶部的搜索框只过滤显示
(标题会变成 技能 (匹配/总数)),不改动实际下发的技能集合。底部还能「+ 精细化添加技能」
「查看被禁用的技能」。
图 6 · 对话「管理标签(对话)」:可以有自己的一份
<img src="https://cdn.jsdelivr.net/gh/aawef/dsh-skill-manager-tags@main/images/conversation-filter.png" width="900" alt="对话级管理标签界面与继承来源">
同一个工作区下的某个对话想单独收窄,就在这里改:它会记下继承来源与时间
(改动不影响工作区),左下角一键「继承工作区配置」覆盖回来。
技能注册的三种状态:已注册 / 未注册 / 暂不可判定
界面右列标题 技能 (全部) · 模型可见 N 里的 N,只数已被宿主确认注册的技能。
而"未注册"和"暂不可判定"是两件完全不同的事,别把它们读成一句:
状态 · 判据 · 界面表现 · 是否计入 模型可见 N
已注册 · 这一趟宿主快照里返回了这个名字 · 正常显示 · ✅ 计入
未注册 · 宿主视角完整,但没返回这个名字 · 置灰 + 橙色「未注册」 · ❌ 不计入
暂不可判定 · 这次没取到完整的宿主视角 · 灰色虚线徽标,不置灰 · ❌ 不计入
「宿主快照」是什么
插件判断一个技能"模型能不能看到",是去问宿主:"这条技能你注册了吗?" ——
宿主对这个问题的一次完整回答,就是一次宿主快照(宿主 ctx.skills.snapshot()
返回的技能清单)。没取到快照=这一问没有得到一个可信的完整回答,
于是那批技能既不该标"已注册"(没证据),也不该标"未注册"(更没有证据)。
为什么会取不到完整快照
宿主的技能注册表是分层的:global 层 + 按 agent / preset 展开的层。
不带 scope 去问时,宿主只回 global 层。而覆盖用户级技能根
(~/.dsh/skills、~/.agents/skills)的那些 provider 恰好注册在 agent 层里 ——
一旦 scope 传不下去,这些名字就整段不在回答里,"没返回"于是被误当成"没注册"。
这是 0.1.15 修掉的缺陷(issue #1):此前同一份本机文件,在有 live agent 的对话里是
"已注册",在没在跑的对话里就变成"宿主没注册,模型侧根本不存在"——仅仅换一个对话,
结论就翻转。
可能的触发原因,按实际出现频率排序:
1. 该会话没有在运行——历史对话、已关闭的标签页、或刚切过来还没起 agent。
agents.list() 里找不到它,scope 传不下去,宿主只给 global 层。这时列表顶部会
标注「该会话未运行,以下为全局视角」。
2. 会话 ID 对不上——浮层传的是 session-<uuid>,宿主 agent 暴露的 id 若是另一个
字符串(宿主侧依次取 session.id / session.header.id / agent.id),同样等于取不到 agent。
3. 宿主自己报 complete: false——宿主明确说这一趟没收集全(某个 provider 还在加载、
某个根扫描失败)。插件直接采用宿主的结论,整批降级为不可判定。
4. 快照抛错或并发守卫被占——snapshotSkills() 返回 null,插件退回纯文件系统自扫描;
这时它更没资格断言"宿主没注册"。
5. 宿主确实没注册——这种情况"暂不可判定"是保守过头了。但只要拿到完整视角,
插件就会给出明确的「未注册」,不会一直挂着。
想看确定结论怎么办
- 把对话切到正在运行的那个会话再看——它能拿到完整视角,用户级技能会恢复成"已注册"。
- 设置页那种不带
conversationId的口径本身就是全局视角,出现"暂不可判定"是预期行为。 - 视角不完整时,
POST /skill-tag/api/skills/purge-stale会直接返回409 INCOMPLETE_VIEW
而拒绝执行——因为它要靠"未注册"这个结论去删关联记录,视角不可信时删了就是误删。
安装
这是一个 DSH(DeepSeek Harness)插件,装进 profile 后随宿主启动加载。
一条命令
dsh plugin --profile <your-profile> add dsh-skill-tag
也可以不走 npm,直接从 GitHub 装:
dsh plugin --profile <your-profile> add github:aawef/dsh-skill-manager-tags
dsh plugin 本质是把参数转发给 profile 目录里的 pnpm,它不会替你启用插件 ——
装完还要在 profile 的 package.json 的 dsh.profile.bundles 数组里加上 "dsh-skill-tag":
{
"dsh": {
"profile": {
"bundles": [ "...", "dsh-skill-tag" ]
}
}
}
重启 DSH 后生效(设置页会出现「Skill 标签」一项)。
官方 DSH 桌面端的 desktop profile 归应用独占,命令行改不动它 ——
桌面端请在 Settings → Plugins 或插件市场里安装。
依赖 Node ≥ 20;需要 DSH0.2.0-rc.1及以上(见engines.dsh)。
宿主的 @deepseek-ai/cordis 声明为 peerDependency,由 DSH 本体提供,不要自己安装。
存储域 spec 由本插件src/storage/domain-spec.js本地构造(等价于宿主的defineDomain/
domainTable),运行时通过ctx.storageDomain.open()交给宿主打开,不 import 任何宿主内核包。
这样 profile 里不会出现与宿主同名的第二份模块副本:早期版本曾 import
@deepseek-ai/dsh-storage-domain,宿主升级(如0.2.1-rc.2→rc.3)时那份旧副本会被判定
不兼容并拒绝加载,导致存储域整体不启动、会话与工作区列表全空(详见 CHANGELOG 2026-10-24)。
本地开发式安装
运行时加载的是 profile 里 node_modules/<插件名> 那一份,不是你的源码目录。两种接法:
- junction(推荐):让
~/.dsh/profiles/web/my-plugins/<插件名>指向你的源码目录,
pnpm 会把它硬链接进 node_modules —— 三个路径是同一个 inode、同一份文件,
改完内容立刻生效,不需要复制;只有*
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::14,最后更新 2026-11-06