dsh-skill-tag

skilltested: passnpmMIT

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

下载量 / Downloads
2,332
依赖体积 / node_modules
4.28 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

Install

dsh plugin --profile web add dsh-skill-tag

Sandbox test results

StageResult
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;需要 DSH 0.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

Links

GitHub · Homepage · npm

Same category