dsh-skill-picker 0.5.23 → 0.5.24

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
@@ -141,6 +141,7 @@ source.onPick = (args) => { track(args?.candidate?.name); return onPick.call(sou
141
141
 
142
142
  ## 更新日志
143
143
 
144
+ - **v0.5.24**:**删掉面板底部那两个徽标(「本地扫描」/「/ 增强:运行时接管」)**——国豪的反馈:`确实有了,不过本地扫描和增强感觉没必要啊,感觉有点占地方,有点多余`。他说得对:**它们是"给排障的人看的",不是给用户看的**,却常驻占掉两行。**但没有直接丢掉** —— 「插件一切正常」和「插件压根没在跑」在界面上一模一样,这正是 **issue #14** 的教训,所以信息搬到了**不占地方的两处**:① ⚡ 按钮的 **tooltip**(鼠标悬停才显示,如 `选择技能(插入 /技能名 到发送框) · 列表来自本地扫描 · / 增强:运行时接管`);② **控制台一行**(面板每次打开打印一次:技能条数 + 列表来源 + `/` 增强是否真的生效)。顺带把已经没人用的 `sourceBadgeStyle` / `sourceBadgeTextStyle` 一并删除。全量 `npm test`:**88 tests / 87 pass / 0 fail**
144
145
  - **v0.5.23**:**⚡ 面板里可以直接管理技能了:关闭 / 开启 · 定位 · 卸载(可撤回)**——起因是国豪视频评论区有人问"能加一个管理 skill 功能吗?实现删除、加、启动、关闭等操作"。查证结论:**官方没有任何技能管理界面**(把 asar 里 289 个官方包全扫了一遍,没有禁用/卸载技能相关代码;评论里说的"专门的插件"指的是 `find-skills` 那种**搜索+安装**的 skill,只解决"加"),所以这块是真空白。**交互方式(国豪的主意,比我的第一版好)**:动作**不占行内 UI**,而是**右键技能行弹出一个小菜单**(置顶 / 关闭 / 定位 / 卸载),手机则**长按 500ms** 弹同一个菜单(滑动即取消,长列表滚动不会误触);行内只留一个 glyph(📌/📍,兼作"已置顶"状态指示)。**踩到的 UI 坑**:关闭监听最初挂在**捕获阶段**,会在菜单项自己的 `onClick` 之前就把菜单关掉 → 点了没反应;改成**冒泡阶段 + 菜单自身 `stopPropagation`**。**①「关闭」是真关闭,不是障眼法**:读了官方 `dsh-skill-filesystem` 的判定逻辑 —— **一个技能 = 目录下有 `SKILL.md`(大小写严格)**,或技能根下的顶层 `.md`。所以关闭就是把 `SKILL.md` 改名成 **`SKILL.md.disabled`**:provider 直接看不见它,**agent 也不再加载**,要回来只需改回名字。**②「卸载」从不删除**:移进 `$DSH_HOME/skill-backups/<时间戳>-<名字>/`(旁边写 `manifest.json` 记原路径),面板里出现「撤回」;`skill-ops.js` 里**一个破坏性文件调用都没有**,而且有一条**结构性回归测试**盯着这件事(注释里出现过破坏性调用的名字都会让它红 —— 这是它第一次触发时教我的)。跨盘无法 `rename` 时**直接报错**,绝不退化成"复制+删除"。**③ 三道安全闸**(这些路由**不需要鉴权**就能到达,所以不是可选项):请求必须带 `x-dsh-skill-picker: 1` 自定义头(跨站表单 POST 设不了自定义头,CORS 预检 host 也不应答);**客户端只报技能名,路径由 host 自己查**(⚡ 面板主数据来自官方 API,那套 DTO 不保证带 `path`;而且浏览器说了不算);路径必须**恰好落在已知技能根的一层之下**(`../` 逃逸、根目录本身、技能内的文件、任意绝对路径全部拒绝)。**④ 一个必须补的洞**:官方 `skills/list` **不会返回已关闭的技能** → 关掉之后就再也找不到、开不回来了;所以面板额外拉一次 host 扫描,单独渲染「⏻ 已关闭」分组专门用来开回来。**⑤ 真机端到端实测(用我自己建的假技能,全程只动它一个)**:关闭后磁盘上 `SKILL.md`→`SKILL.md.disabled` ✓、**扫描仍能列出它且 `disabled:true`** ✓(这是"能开回来"的前提)、开启还原 ✓、卸载后原位置消失而备份里 `SKILL.md`+`manifest` 完整 ✓、`backups` 能列出 ✓、`restore` 回原位 ✓、不带自定义头 → **403** ✓、`name` 塞 `../../../Windows/System32` → **404** ✓、`GET` → **405** ✓;**测试后你的技能数一个没变、备份目录零残留**。**回归测试**:新增 `test/skill-ops.test.mjs` 13 例(路径闸 9 种情形、关闭/开启往返、重复关闭拒绝、开启未关闭的拒绝、卸载后原位置消失但备份完整且 manifest 正确、撤回后再占用时拒绝覆盖、备份原路径被改成根外后不可撤回、备份 id 不允许带路径、**"模块里不存在破坏性调用"**、无 manifest 的杂目录被忽略、备份目录必须落在 `DSH_HOME` 下)。全量 `npm test`:**88 tests / 87 pass / 0 fail**
145
146
  - **v0.5.22**:**「置顶 / 最近使用」改成两端(乃至手机)共用一份**——起因是国豪发现"桌面端和网页端的置顶不一样",并当场质疑"不就读同一个配置吗,要一个小时?"。**他是对的,而且根因就是"读的不是同一个配置"**:`localStorage` **按源(origin)隔离**,桌面端是 `dsh-app://app`、网页端是 `http://127.0.0.1:3080`、手机连上去又是第三个源 —— **同一份数据被存了三遍**,所以在桌面端钉的技能网页端永远看不到。**修法(约 40 行 host + 一个客户端模块,不是重写数据层)**:host 侧新增 `/dsh-skill-picker/state`(GET 取 / PUT 存),存到 **`$DSH_HOME/dsh-skill-picker-state.json`**(写用 temp+rename,原子);客户端新增 `src/client/shared-state.js`,把这份**母本镜像进 localStorage**,于是渲染期的**同步**读取一行都不用改(原来的数据路径是同步的,改成异步要动一大片 —— 这个顾虑没必要)。**细节**:① 每次 `savePinned`/`saveUsage`(含 `/` 菜单里的选中记录)都会**自动回推**;② 插件启动时 + **⚡ 面板每次打开时**都会拉取,所以另一端改了立刻能看见;③ **首次同步用并集、之后才跟随**(关键,实测中发现的坑):host 端 `?migrate=1` 会把收到的状态与已有状态**合并**再返回;客户端用一个 `dsh-skill-picker:shared-synced` 标记区分"这台还没同步过"。**没同步过**且本地有数据 → 走并集迁移(绝不覆盖);**同步过** → 直接采纳文件(所以"在 A 端取消置顶"能真的传播到 B 端)。**为什么必须有这一步**:如果首次也直接采纳,那么**哪台客户端先启动,文件就被它写成什么样,后面启动的那台就会丢掉自己独有的钉子** —— 桌面端 3 条、网页端 2 条的情况下,网页端先起就会把桌面端的 `ji-zhang` 吃掉。端到端实测时正是这条被暴露出来的;④ 文件不存在 / 为空而本地有数据时,**推送而不是覆盖为空**;⑤ 全程 **best-effort**:host 不可达时行为和以前**完全一样**(纯本地),绝不会因为同步失败把选择器弄坏。**回归测试**:新增 `test/shared-state.test.mjs` 7 例(拉取即镜像、缺文件时以本机播种并带 `migrate=1`、迁移返回的并集被本地采纳、远端为空时不把本机数据清成空、推送内容正确、host 失败时一律返回 false 且不抛、同步后会广播事件让界面重读)。全量 `npm test`:**72 tests / 71 pass / 0 fail**
146
147
  - **v0.5.21**:**文件补丁正式退休:默认不再改官方文件,并自动把改过的还原回去**——起因是国豪发现**同一件事桌面端对、网页端不对**,并直接怀疑到"之前魔改过"。**查证结果:他猜对了。** 两端加载的官方 `@deepseek-ai/dsh-client-ui-skill` 根本不是同一份:**桌面端从 `app.asar` 取**(打包只读,永远是原版),**网页端从 `~/.dsh/profiles/node_modules/` 取共享那份 —— 而那份被本插件的老文件补丁改过**(实测:`__dshSkillPickerFuzzy`×2、`order: -1`,旁边躺着 `.bak`)。**这个差异为什么会让排序看起来不一样**:补丁把官方 `candidates` 改成走 `window.__dshSkillPickerFuzzy`,而它返回的**已经是「置顶+使用热度」排好序**的列表;我的包装层再做一次排序时,`groupByPinned` 把「🗂️ 全部」分组的**输入顺序**保留下来(同热度时的稳定排序)——于是网页端的「全部」是**使用热度序**、桌面端是**字母序**。**根因级别的问题**:文件补丁自 v0.5.15 起**已是纯冗余** —— 它只做三件事(技能组置顶 `order: -1`、模糊+拼音匹配、`onPick` 记使用),而**运行时接管三件全都自己做了**。它却仍在改用户的官方包,还让两个 profile 对同一个文件产生分歧。**修法**:① 新增 `revertUiSkillPatches()`,启动时**只针对"仍带我们标记"的文件**从 `.bak` 还原(用 temp+rename,绝不动 pnpm 硬链接的共享 inode;幂等);② 文件补丁改为**默认关闭**,只有显式设 `DSH_SKILL_PICKER_FILE_PATCH=1` 才走旧的自愈路径(给"触发源注册表里没有 live sources 可接管"的老内核兜底)。**本机已实测还原**:共享那份 `lib/client.js` 现在 `__dshSkillPickerFuzzy`=0、`order: 2`、**hash 与 `.bak` 完全一致**。全量 `npm test`:**65 tests / 64 pass / 0 fail**
package/lib/client.js CHANGED
@@ -26390,23 +26390,6 @@ var menuItemStyle = {
26390
26390
  cursor: "pointer",
26391
26391
  whiteSpace: "nowrap"
26392
26392
  };
26393
- var sourceBadgeStyle = {
26394
- display: "inline-flex",
26395
- alignItems: "center",
26396
- alignSelf: "flex-start",
26397
- margin: "0 8px 8px",
26398
- padding: "2px 8px",
26399
- border: "1px solid rgba(255, 193, 7, 0.35)",
26400
- borderRadius: "999px",
26401
- background: "rgba(255, 193, 7, 0.1)",
26402
- color: "#d9a520",
26403
- fontSize: "11px",
26404
- lineHeight: "16px",
26405
- flex: "none"
26406
- };
26407
- var sourceBadgeTextStyle = {
26408
- fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)"
26409
- };
26410
26393
  function BoltIcon() {
26411
26394
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", style: { display: "block" }, children: [
26412
26395
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", { id: "dsh-sp-bolt-grad", x1: "0", y1: "0", x2: "0", y2: "1", children: [
@@ -26561,6 +26544,12 @@ function SkillPickerButton(props) {
26561
26544
  window.removeEventListener("resize", close);
26562
26545
  };
26563
26546
  }, [menu]);
26547
+ (0, import_react.useEffect)(() => {
26548
+ if (skills === void 0) return;
26549
+ console.info(
26550
+ `[dsh-skill-picker] \u6280\u80FD\u5217\u8868 ${skills.length} \u6761\uFF0C\u6765\u6E90\uFF1A${source === "host" ? "\u672C\u5730\u626B\u63CF\uFF08\u5B98\u65B9 skills API \u4E0D\u53EF\u7528\uFF09" : "\u5B98\u65B9 API"}\uFF1B/ \u589E\u5F3A\uFF1A${slashEnhancementMode() === "runtime" ? "\u8FD0\u884C\u65F6\u63A5\u7BA1\uFF0C\u5DF2\u751F\u6548" : "\u672A\u751F\u6548\uFF08\u89C1 issue #14\uFF09"}`
26551
+ );
26552
+ }, [skills, source]);
26564
26553
  const toggle = () => {
26565
26554
  if (!open) {
26566
26555
  void syncSharedState();
@@ -26642,7 +26631,7 @@ function SkillPickerButton(props) {
26642
26631
  {
26643
26632
  type: "button",
26644
26633
  onClick: toggle,
26645
- title: "\u9009\u62E9\u6280\u80FD\uFF08\u63D2\u5165 /\u6280\u80FD\u540D \u5230\u53D1\u9001\u6846\uFF09",
26634
+ title: `\u9009\u62E9\u6280\u80FD\uFF08\u63D2\u5165 /\u6280\u80FD\u540D \u5230\u53D1\u9001\u6846\uFF09${source === "host" ? "\u3000\xB7\u3000\u5217\u8868\u6765\u81EA\u672C\u5730\u626B\u63CF" : ""}${slashEnhancementMode() === "runtime" ? "\u3000\xB7\u3000/ \u589E\u5F3A\uFF1A\u8FD0\u884C\u65F6\u63A5\u7BA1" : ""}`,
26646
26635
  "aria-label": "\u9009\u62E9\u6280\u80FD",
26647
26636
  style: {
26648
26637
  ...buttonStyle,
@@ -26848,15 +26837,6 @@ function SkillPickerButton(props) {
26848
26837
  `off-${skill.name}`
26849
26838
  ))
26850
26839
  ] }),
26851
- source === "host" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: sourceBadgeStyle, title: "\u5B98\u65B9\u6280\u80FD API \u4E0D\u53EF\u7528\uFF0C\u5217\u8868\u6765\u81EA\u672C\u5730\u76EE\u5F55\u626B\u63CF\uFF08\u4E0E\u5B98\u65B9 / \u8865\u5168\u540C\u6E90\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: sourceBadgeTextStyle, children: "\u672C\u5730\u626B\u63CF" }) }),
26852
- slashEnhancementMode() === "runtime" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26853
- "div",
26854
- {
26855
- style: sourceBadgeStyle,
26856
- title: "`/` \u83DC\u5355\u7684\u6A21\u7CCA+\u62FC\u97F3\u641C\u7D22\u7531\u672C\u63D2\u4EF6\u5728\u8FD0\u884C\u65F6\u63A5\u7BA1\u5B98\u65B9\u6280\u80FD\u6E90\uFF08\u4E0D\u4F9D\u8D56\u6539\u5199\u4EFB\u4F55\u6587\u4EF6\uFF0Cissue #14\uFF09",
26857
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: sourceBadgeTextStyle, children: "/ \u589E\u5F3A\uFF1A\u8FD0\u884C\u65F6\u63A5\u7BA1" })
26858
- }
26859
- ),
26860
26840
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { margin: "0 10px 8px", fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #8a94a6)" }, children: "\u53F3\u952E\u6280\u80FD = \u7F6E\u9876 / \u5173\u95ED / \u5B9A\u4F4D / \u5378\u8F7D\uFF08\u624B\u673A\u957F\u6309\uFF09" }),
26861
26841
  menu !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
26862
26842
  "div",