dsh-skill-picker 0.5.11 → 0.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ DSH Web GUI 的技能选择器:在输入框(composer)工具行右侧加一
26
26
 
27
27
  English: A skill picker for the DSH Web GUI — a button in the composer's right tool row opens a searchable list of installed skills; picking one inserts the official `/skill-name` gesture into the draft, so DSH's native user-invocation path loads the skill with your message.
28
28
 
29
- 当前版本:**v0.5.10**(**修复全局安装下 `/` 补全增强静默失效**(issue #7)+ ⚡ 面板**置顶分组** + `/` 补全**自动增强补丁** + 拼音搜索 + **搜索结果按匹配相关度排序**)
29
+ 当前版本:**v0.5.12**(**修复 `user-invocable: false` 的技能未被隐藏**(issue #10)+ 修复全局安装下 `/` 补全增强静默失效(issue #7)+ ⚡ 面板**置顶分组** + `/` 补全**自动增强补丁** + 拼音搜索 + **搜索结果按匹配相关度排序**)
30
30
 
31
31
  ## 为什么用它(vs 官方 `/` 补全)
32
32
 
@@ -124,6 +124,7 @@ DSH 的 [dsh-tool-skill](https://github.com/deepseek-ai/deepseek-harness) 在 `a
124
124
 
125
125
  ## 更新日志
126
126
 
127
+ - **v0.5.12**:**修复 `user-invocable: false` 的技能仍出现在 ⚡ 面板(对应 issue #10)**——面板取数有两条路:① **官方宿主 API**(`remote.skills.list`)在**服务端就过滤好了**(`dsh-api-session-controller` 的 skill-catalog 里是 `.filter(isUserInvocable)`,且它的线上 DTO `SkillEntry` 只带 `modelInvocable`、**根本不带 `userInvocable`**);② **本插件自己的兜底扫描路由**(`/dsh-skill-picker/skills`,面板底部显示「本地扫描」徽标那条)只读 `name` / `description`,**完全没读调用策略** —— 这就是 0.1.7 线(官方客户端 UI 包重构、兜底路径被触发)下面板会列出 `user-invocable: false` 技能的原因。**危害不止"多显示一条"**:点选后插入的 `/技能名` 手势会被 `dsh-tool-skill` 的 `!isUserInvocable(skill)` **静默跳过**——用户以为选中了,实际什么都没发生。修复:host 兜底扫描新增 `frontmatterBoolean()` / `isUserInvocableSkill()`,按官方 `dsh-skill-filesystem` 的 `parseInvocationPolicy` 完整对齐语义 —— 接受 YAML 布尔与**不分大小写**的 `true`/`false`、`yes`/`no`、`on`/`off`、`1`/`0`;**显式 `false` 不列出**;**非法拼写或遗留键(`userInvocable` / `modelInvocable` / `disableModelInvocation`)整条丢弃**(官方也是丢整条,而不是静默放行);`disable-model-invocation: true` 只影响模型面,`/` 与面板照常列出。client 侧三个取数点(官方 API / 兜底 fetch / 喂给 `/` 的模糊匹配器)都加了 `isUserFacingSkill()` 守卫,同时认平铺 `userInvocable` 与嵌套 `invocation.userInvocable`,防内核将来更换协议形状。新增 6 个 `npm test` 回归用例:`user-invocable: false` 隐藏、`true`/省略保留、全部 false 拼写、非布尔值丢弃、遗留键丢弃、`disable-model-invocation: true` 仍列出
127
128
  - **v0.5.11**:**修复「补丁已就位也每次启动都打印 `ui-skill patch report`」(对应 issue #8)**——`healUiSkillPatches()` 返回的 `report.files` 是**逐目标文件的报告数组**:只要扫描到 ≥1 个目标文件就有一项,与这一轮**是否真的改动过无关**;而打印守卫用的正是 `report.files.length > 0`,等于把「找到目标」当成了「发生了变更」,于是**补丁早已 up-to-date 也每次启动都打印一行** `[dsh-skill-picker] ui-skill patch report: {…}`。这行虽然只是 `console.log`,但形态上落在启动日志第一行、长得像告警,很容易被误判成插件出问题(#8 就是这么来的),还会淹没真正需要关注的 `noop` / `errors`。现在改为按「这一轮到底发生了什么」判定:① **已是最新且无错 → 完全安静**;② 仅在**确实改过文件**(`patched` 非空)时打印报告,且只报这一轮真正动过的文件 + 全部错误;③ **锚点未命中(`noop` 非空)单独 `console.warn`**——它的语义是「官方实现又换了形态、增强**没打上**」,与 issue #7 的静默失效同类,不能再被淹没;④ 新增 `DSH_SKILL_PICKER_LOG=debug` 显式开关,需要完整报告(含 `skipped`)时按需打开。新增 7 个 `npm test` 回归用例:已就位时静默、无目标不重复报、真改动打一行、`noop` 转 warn、仅错误转 warn、改动+错误合并一行、debug 开关
128
129
  - **v0.5.10**:**修复全局安装下 `/` 补全增强静默失效(对应 issue #7)**——`uiSkillClientPaths()` 原先只枚举两个位置:`profiles/<profile>/local/dsh-client-ui-skill` 与 `profiles/<profile>/node_modules/@deepseek-ai/dsh-client-ui-skill`。但用**全局 `npm i -g @deepseek-ai/dsh`** 安装时,官方包位于**共享根** `profiles/node_modules/@deepseek-ai/dsh-client-ui-skill`(`readdir(profiles)` 只会给出 `node_modules` 和 `web` 两个条目,两个候选**全部落空**),于是 `found = []`、**两个补丁一次都没跑**——而且**完全无声**:`{"files":[],"errors":[]}` 与「补丁都已应用、全部 skipped」在输出上一模一样,用户和排查者都看不出补丁根本没生效,表现成「插件一切正常、技能列表能用,**就是拼音/模糊搜索是坏的**」。修复四件事:① 候选新增**共享根**(不属于任何单个 profile,放在循环外采集);② 每个 profile 额外走一次 Node 自身解析 `createRequire().resolve()` 兜底,未枚举到的布局也能命中(按 realpath 去重,不会重复打补丁);③ 跳过 `profiles/node_modules` 这个假 profile 条目;④ **`found.length === 0` 时 `console.warn` 大声报出**——这个静默正是 issue #7 里最坑人的地方。另修写入方式:由原地 `writeFile` 改为**临时文件 + `rename`**——pnpm 安装的包是**硬链接**到共享内容寻址 store 的,原地写会连带改动 store 里的同一份(影响其他使用同版本的项目),`rename` 只替换目录项、不动共享 inode,顺带获得写入原子性(中断的启动不会留下半截文件)。新增 6 个 `npm test` 回归用例:共享根、profile local、profile node_modules、共享根+profile 去重、无任何安装、profiles 目录缺失
129
130
  - **v0.5.9**:**修复符号链接 / Junction 型技能查不到(对应 issue #6)**——扫描技能目录时 `readdir` 的 `Dirent` 走的是 lstat 语义:Windows 下符号链接**和 Junction** 都报告 `isDirectory() === false` / `isSymbolicLink() === true`,于是链接型技能(如 `~/.agents/skills/neat` → `D:\repos\icraft-toolkit\skills\neat`)在第 79 行的目录过滤里被静默 `continue` 掉。现在链接条目改用 `stat`(跟随链接)判定真实类型:链接型技能与普通目录**完全一视同仁**,断链或指向普通文件的链接安全跳过(不再抛错、也不占用列表)。四个扫描根(`~/.agents/skills`、`~/.dsh/skills`、项目级 `.agents/skills` / `.dsh/skills`)与 profile 枚举路径全部受益;新增 `npm test`(`node --test`)回归用例:普通目录、链接目录、链接+普通混排、断链、无 `SKILL.md` 的链接、项目级链接技能
package/lib/client.js CHANGED
@@ -25799,6 +25799,10 @@ function saveUsage(usage) {
25799
25799
  } catch {
25800
25800
  }
25801
25801
  }
25802
+ function isUserFacingSkill(skill) {
25803
+ const flag = skill?.userInvocable ?? skill?.invocation?.userInvocable;
25804
+ return flag !== false;
25805
+ }
25802
25806
  function rankByUsage(skills, usage) {
25803
25807
  return skills.slice().sort((a, b) => {
25804
25808
  const ua = usage[a.name];
@@ -26022,7 +26026,7 @@ function SkillPickerButton(props) {
26022
26026
  const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: "application/json" } });
26023
26027
  const json = await res.json();
26024
26028
  if (!json.ok) throw new Error(json.error || "bad response");
26025
- setSkills(Array.isArray(json.skills) ? json.skills : []);
26029
+ setSkills((Array.isArray(json.skills) ? json.skills : []).filter(isUserFacingSkill));
26026
26030
  setSource("host");
26027
26031
  } catch (cause) {
26028
26032
  setError(String(cause?.message ?? cause));
@@ -26234,7 +26238,7 @@ function apply(ctx) {
26234
26238
  const { result } = await skills.list({ sessionId }, controller.signal);
26235
26239
  if (!result.ok) throw new Error(`skill.list failed: ${result.error?.code}: ${result.error?.message}`);
26236
26240
  const raw = result.value?.skills ?? [];
26237
- return raw.map((skill) => ({ name: skill.name, description: skill.description ?? "" }));
26241
+ return raw.filter(isUserFacingSkill).map((skill) => ({ name: skill.name, description: skill.description ?? "" }));
26238
26242
  };
26239
26243
  let currentCwd = "";
26240
26244
  const syncCwd = () => {
@@ -26265,7 +26269,8 @@ function apply(ctx) {
26265
26269
  }, "dsh-skill-picker: composer input slot");
26266
26270
  ctx.effect(() => {
26267
26271
  const fuzzyMatch = (skills, query = "") => {
26268
- const ordered = groupByPinned(skills, loadUsage(), loadPinned()).flatMap((group) => group.items);
26272
+ const visible = (Array.isArray(skills) ? skills : []).filter(isUserFacingSkill);
26273
+ const ordered = groupByPinned(visible, loadUsage(), loadPinned()).flatMap((group) => group.items);
26269
26274
  const q = String(query).trim().toLowerCase();
26270
26275
  if (q === "") return ordered;
26271
26276
  const rankByName = new Map(ordered.map((skill, index) => [skill.name, index]));