dsh-skill-picker 0.5.11 → 0.5.13
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 +9 -2
- package/lib/client.js +43 -14
- package/lib/client.js.map +3 -3
- package/lib/index.js +97 -5
- package/lib/index.js.map +2 -2
- package/package.json +12 -8
- package/src/client/index.jsx +758 -717
- package/src/client/session-view.js +68 -0
- package/src/index.js +91 -3
- package/src/patch-ui-skill.js +110 -5
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
|
|
29
|
+
当前版本:**v0.5.12**(**修复 `user-invocable: false` 的技能未被隐藏**(issue #10)+ 修复全局安装下 `/` 补全增强静默失效(issue #7)+ ⚡ 面板**置顶分组** + `/` 补全**自动增强补丁** + 拼音搜索 + **搜索结果按匹配相关度排序**)
|
|
30
30
|
|
|
31
31
|
## 为什么用它(vs 官方 `/` 补全)
|
|
32
32
|
|
|
@@ -124,6 +124,12 @@ DSH 的 [dsh-tool-skill](https://github.com/deepseek-ai/deepseek-harness) 在 `a
|
|
|
124
124
|
|
|
125
125
|
## 更新日志
|
|
126
126
|
|
|
127
|
+
- **v0.5.13**:**适配 DSH 0.2.0 线(对应 issue #13、#11、#9)**——这一版修的是三件不同的事,但它们有一个共同的教训:**插件"没坏"和"没在跑"长得一模一样**。
|
|
128
|
+
- **① 内核加载门不再拒载(issue #13)**:`peerDependencies` 里六项 `@deepseek-ai/dsh-*` 全钉在 `^0.1.0-rc.6`,semver 上界是 `<0.2.0-0`,于是 DSH **0.2.0-rc.1 / rc.2 启动时直接 skip 掉整个插件**(`dsh: skipping profile bundle "dsh-skill-picker"`),⚡ 按钮**凭空消失**、没有任何其它症状。关键点是:**这不是"真不兼容",是"声明过度"**——`dsh-app-boot` 的 `evaluatePluginCompatibility()` 是拿**内核版本**去逐项比对 `@deepseek-ai/dsh*` 的 peer 范围,而这些包与内核**锁步同版本**(实测 `dsh` / `dsh-app-boot` / `dsh-client-locale` / `dsh-client-ui-slots` / `dsh-host-webserver` / `dsh-skill` / `dsh-system-prompt` / `dsh-client-ui-skill` 全是同一个号),所以钉住一个小版本线 = **内核每跳一版就必然误拒一次**。现在改为 `^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0`:0.1.x / 0.2.x 及以后所有 0.x 都能加载,到 `1.0.0` 才需要重新确认。真正的"已验证"信号交还给 `dsh.compatibility.dshReleases` 兼容表(本版补上 `0.1.7-rc.1`、`0.2.0-rc.1`、`0.2.0-rc.2`)
|
|
129
|
+
- **② 项目级技能不再消失(issue #11,由 @Arcobalneo 定位并提交 PR #12)**:0.1.7 起 ⚡ 面板**静默丢掉整个项目级技能层**(`<workspace>/.agents/skills` 里的技能一支都不出现,用户级正常,无报错无 warning)。两条取数路同时退化:**主路** `remote.skills.list` 的 gate 读 `props.session?.sessionId`,而 session scope 插槽给的是框架标准 prop `sessionId`、**没有 `session` 对象** → 这条路一次都没发过请求;**兜底路**用 `ctx.sessions.list.getSnapshot().current` 定位当前会话,而 0.1.7 的 store 状态是 `{ ids, byId, phase, projectionsBySession }`、**没有 `current` 游标** → `currentCwd` 恒为 `''` → 请求退化成不带 `?cwd=` → 只扫用户级。修法是把"会话身份 + 工作区 cwd"收敛到新模块 `src/client/session-view.js`,两路都只依赖座位自己的标准 props。**本版在其之上补了一处**:`props.useSessions` 座位在 0.1.2 / 0.1.5 内核上可能不存在,那样 cwd 会退化回空串——所以把旧的 `ctx.sessions.list.current` 取值保留为**次级来源**,仅在标准 props 取不到时使用,避免"修了 0.1.7、砸了 0.1.5"
|
|
130
|
+
- **③ 桌面端补丁目标不再静默打偏(issue #9)**:候选路径新增**活动桌面安装树**(`<resources>/app/node_modules/@deepseek-ai/…`,从 Electron 的 `process.resourcesPath` 推导,解除安装的 desktop 构建就此命中,无需再靠 profile 里的软链)。同时把最坑的**静默**变成**自诊断**:打包型桌面构建把官方技能 UI 放在 `resources/app.asar` 里,**asar 只能读、不能就地写**,而只要 profile 没有把该包 `link:` 到本地副本,**真正被服务的就是 asar 里那份**——此时老代码会"成功"地给一堆 profile 副本打补丁(`patched: [...]`,看起来一切正常)却对用户毫无影响。现在这种情况会明确 `console.warn` 指出「补丁已打,但打的不是你实际在用的那份」以及该怎么救
|
|
131
|
+
- **回归测试**:新增 `test/desktop-paths.test.mjs` 6 例(无 Electron 环境下惰性、命中解除安装树、真正打上三个补丁、打包场景必须告警、profile 有重定向时保持安静、零目标时指向 issue #9)。全量 `npm test`:**40 tests / 39 pass / 0 fail**(1 skip 是 Windows 建文件符号链接需权限那条)
|
|
132
|
+
- **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
133
|
- **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
134
|
- **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
135
|
- **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` 的链接、项目级链接技能
|
|
@@ -155,7 +161,8 @@ DSH 的 [dsh-tool-skill](https://github.com/deepseek-ai/deepseek-harness) 在 `a
|
|
|
155
161
|
- **链接型技能**:技能目录里的**符号链接 / Junction**会被跟随读取(v0.5.9 起,对应 issue #6),链接型技能与普通目录一视同仁;断链、指向普通文件的链接静默跳过,不影响其它技能。
|
|
156
162
|
- **暂不支持**:自定义技能目录(官方 `customSkillDirs` 配置)——需要的话欢迎 PR。
|
|
157
163
|
- **失败保护**:client 端用 `ctx.slots.inject`(等 `conversation.input.right` 插槽声明存在才注册,插槽缺失时静默跳过,不会拖垮启动);host 端路由 try/catch,扫描目录不存在时返回空列表而非报错。
|
|
158
|
-
-
|
|
164
|
+
- **依赖版本**:peer 范围声明为 `^0.1.0-rc.6 || >=0.2.0-rc.1 <1.0.0-0`(v0.5.13 起)——DSH 的启动加载门会拿**内核版本**逐项比对 `@deepseek-ai/dsh*` 的 peer 范围,而内核包是**锁步同版本**的,钉死单个小版本线会让插件在内核每次升级时被**误拒**(`skipping profile bundle`,表现为 ⚡ 按钮消失、无其它症状)。真正的"已验证到哪一版"以 `package.json` 的 `dsh.compatibility.dshReleases` 为准;要回退只需 `dsh plugin --profile web remove dsh-skill-picker`。
|
|
165
|
+
- **桌面端(Electron)**:官方技能 UI 由**安装树**拥有——解除安装的构建是 `resources/app/node_modules/…`(本插件直接作为候选打补丁),**打包构建**是 `resources/app.asar` 里那份(**只能读、不能就地写**)。若你的 profile **没有**把 `@deepseek-ai/dsh-client-ui-skill` 声明成 `link:` 到本地副本,真正被服务的就是 asar 那份,`/` 菜单的模糊/拼音升级**打不上去**——这种情况启动日志会明确告警(v0.5.13 起,不再静默)。救法:复制该包到 `profiles/<profile>/local/dsh-client-ui-skill`,在 profile 的 `package.json` 里加 `"@deepseek-ai/dsh-client-ui-skill": "link:…/profiles/<profile>/local/dsh-client-ui-skill"`,重启后本插件即可给这份打补丁。
|
|
159
166
|
|
|
160
167
|
## 开发
|
|
161
168
|
|
package/lib/client.js
CHANGED
|
@@ -25762,6 +25762,22 @@ function pinyin(word, options) {
|
|
|
25762
25762
|
return middlewareType(list, options, word);
|
|
25763
25763
|
}
|
|
25764
25764
|
|
|
25765
|
+
// src/client/session-view.js
|
|
25766
|
+
function sessionIdOf(props) {
|
|
25767
|
+
return props?.sessionId ?? props?.session?.sessionId;
|
|
25768
|
+
}
|
|
25769
|
+
function selectWorkspaceCwd(sessionId) {
|
|
25770
|
+
return (state) => {
|
|
25771
|
+
const cwd = sessionId === void 0 ? void 0 : state?.byId?.[sessionId]?.cwd;
|
|
25772
|
+
return typeof cwd === "string" ? cwd : "";
|
|
25773
|
+
};
|
|
25774
|
+
}
|
|
25775
|
+
function useWorkspaceCwd(props) {
|
|
25776
|
+
const useSessions = props?.useSessions;
|
|
25777
|
+
const cwd = typeof useSessions === "function" ? useSessions(selectWorkspaceCwd(sessionIdOf(props))) : void 0;
|
|
25778
|
+
return typeof cwd === "string" ? cwd : "";
|
|
25779
|
+
}
|
|
25780
|
+
|
|
25765
25781
|
// src/client/index.jsx
|
|
25766
25782
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
25767
25783
|
var inject = ["slots", "connection", "sessions", "inputTriggers"];
|
|
@@ -25799,6 +25815,10 @@ function saveUsage(usage) {
|
|
|
25799
25815
|
} catch {
|
|
25800
25816
|
}
|
|
25801
25817
|
}
|
|
25818
|
+
function isUserFacingSkill(skill) {
|
|
25819
|
+
const flag = skill?.userInvocable ?? skill?.invocation?.userInvocable;
|
|
25820
|
+
return flag !== false;
|
|
25821
|
+
}
|
|
25802
25822
|
function rankByUsage(skills, usage) {
|
|
25803
25823
|
return skills.slice().sort((a, b) => {
|
|
25804
25824
|
const ua = usage[a.name];
|
|
@@ -26007,9 +26027,10 @@ function SkillPickerButton(props) {
|
|
|
26007
26027
|
}
|
|
26008
26028
|
const load = (0, import_react.useCallback)(async () => {
|
|
26009
26029
|
if (skills !== void 0 || error !== void 0) return;
|
|
26030
|
+
const sessionId = sessionIdOf(props);
|
|
26010
26031
|
try {
|
|
26011
|
-
if (typeof props.listSkills === "function" &&
|
|
26012
|
-
const listed = await props.listSkills(
|
|
26032
|
+
if (typeof props.listSkills === "function" && sessionId !== void 0) {
|
|
26033
|
+
const listed = await props.listSkills(sessionId);
|
|
26013
26034
|
setSkills(Array.isArray(listed) ? listed : []);
|
|
26014
26035
|
setSource("official");
|
|
26015
26036
|
return;
|
|
@@ -26022,12 +26043,12 @@ function SkillPickerButton(props) {
|
|
|
26022
26043
|
const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: "application/json" } });
|
|
26023
26044
|
const json = await res.json();
|
|
26024
26045
|
if (!json.ok) throw new Error(json.error || "bad response");
|
|
26025
|
-
setSkills(Array.isArray(json.skills) ? json.skills : []);
|
|
26046
|
+
setSkills((Array.isArray(json.skills) ? json.skills : []).filter(isUserFacingSkill));
|
|
26026
26047
|
setSource("host");
|
|
26027
26048
|
} catch (cause) {
|
|
26028
26049
|
setError(String(cause?.message ?? cause));
|
|
26029
26050
|
}
|
|
26030
|
-
}, [skills, error, props.listSkills, props.session, props.cwd]);
|
|
26051
|
+
}, [skills, error, props.listSkills, props.sessionId, props.session, props.cwd]);
|
|
26031
26052
|
const toggle = () => {
|
|
26032
26053
|
if (!open) {
|
|
26033
26054
|
setUsage(loadUsage());
|
|
@@ -26234,23 +26255,30 @@ function apply(ctx) {
|
|
|
26234
26255
|
const { result } = await skills.list({ sessionId }, controller.signal);
|
|
26235
26256
|
if (!result.ok) throw new Error(`skill.list failed: ${result.error?.code}: ${result.error?.message}`);
|
|
26236
26257
|
const raw = result.value?.skills ?? [];
|
|
26237
|
-
return raw.map((skill) => ({ name: skill.name, description: skill.description ?? "" }));
|
|
26258
|
+
return raw.filter(isUserFacingSkill).map((skill) => ({ name: skill.name, description: skill.description ?? "" }));
|
|
26238
26259
|
};
|
|
26239
|
-
let
|
|
26240
|
-
const
|
|
26260
|
+
let legacyCwd = "";
|
|
26261
|
+
const syncLegacyCwd = () => {
|
|
26241
26262
|
try {
|
|
26242
26263
|
const snapshot2 = ctx.sessions.list.getSnapshot();
|
|
26243
26264
|
const sessionId = snapshot2.current;
|
|
26244
26265
|
const cwd = sessionId === void 0 ? void 0 : snapshot2.byId[sessionId]?.cwd;
|
|
26245
|
-
|
|
26266
|
+
legacyCwd = typeof cwd === "string" ? cwd : "";
|
|
26246
26267
|
} catch {
|
|
26247
|
-
|
|
26268
|
+
legacyCwd = "";
|
|
26248
26269
|
}
|
|
26249
26270
|
};
|
|
26250
|
-
|
|
26251
|
-
const
|
|
26271
|
+
syncLegacyCwd();
|
|
26272
|
+
const unsubscribeLegacyCwd = ctx.sessions.list.subscribe(syncLegacyCwd);
|
|
26252
26273
|
ctx.effect(() => {
|
|
26253
|
-
const PickerWithCwd = (props) =>
|
|
26274
|
+
const PickerWithCwd = (props) => {
|
|
26275
|
+
const cwd = useWorkspaceCwd(props);
|
|
26276
|
+
return import_react.default.createElement(SkillPickerButton, {
|
|
26277
|
+
...props,
|
|
26278
|
+
cwd: cwd === "" ? legacyCwd : cwd,
|
|
26279
|
+
listSkills
|
|
26280
|
+
});
|
|
26281
|
+
};
|
|
26254
26282
|
const dispose = ctx.slots.inject(
|
|
26255
26283
|
"conversation.input.right",
|
|
26256
26284
|
() => ctx.slots.register(
|
|
@@ -26260,12 +26288,13 @@ function apply(ctx) {
|
|
|
26260
26288
|
);
|
|
26261
26289
|
return () => {
|
|
26262
26290
|
dispose();
|
|
26263
|
-
|
|
26291
|
+
unsubscribeLegacyCwd();
|
|
26264
26292
|
};
|
|
26265
26293
|
}, "dsh-skill-picker: composer input slot");
|
|
26266
26294
|
ctx.effect(() => {
|
|
26267
26295
|
const fuzzyMatch = (skills, query = "") => {
|
|
26268
|
-
const
|
|
26296
|
+
const visible = (Array.isArray(skills) ? skills : []).filter(isUserFacingSkill);
|
|
26297
|
+
const ordered = groupByPinned(visible, loadUsage(), loadPinned()).flatMap((group) => group.items);
|
|
26269
26298
|
const q = String(query).trim().toLowerCase();
|
|
26270
26299
|
if (q === "") return ordered;
|
|
26271
26300
|
const rankByName = new Map(ordered.map((skill, index) => [skill.name, index]));
|