dsh-skill-tag 0.1.5 → 0.1.7

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
@@ -57,6 +57,8 @@ office * plugin # 只要 office 包带来的、且是插件相关
57
57
  <img src="https://raw.githubusercontent.com/aawef/dsh-skill-manager-tags/main/images/skill-to-tags.png" width="640" alt="skill 联系 tag:左列技能、右列标签勾选">
58
58
 
59
59
  左列 `技能 (105)` 每行标着它有几个标签;右边勾选**即时保存**,不用点确认。
60
+ 右列会把该技能**已有的标签排在列表最前面**(并保持它们原来的相对顺序),所以不用滚动
61
+ 去列表里找;顶部搜索框过滤之后,"已拥有优先"依然成立。
60
62
  这是配条件前最常开的一页:先看清楚"这个技能到底被哪些标签命中"。
61
63
 
62
64
  ### 图 5 · 工作区「管理标签」:左写条件,右就是下发名单
@@ -65,7 +67,8 @@ office * plugin # 只要 office 包带来的、且是插件相关
65
67
 
66
68
  左列是标签条件,下面实时显示**解析结果**和状态「有条件筛选(严格子集)」;
67
69
  右列 `技能 (54)` 就是该工作区下发给模型的全部技能,每行可以「查看 tag」「禁用」,
68
- 📁 直接打开技能所在文件夹。底部还能「+ 精细化添加技能」「查看被禁用的技能」。
70
+ 📁 直接打开技能所在文件夹。列表顶部的**搜索框**只过滤显示(标题会变成 `技能 (匹配/总数)`),
71
+ 不改动实际下发的技能集合。底部还能「+ 精细化添加技能」「查看被禁用的技能」。
69
72
 
70
73
  ### 图 6 · 对话「管理标签(对话)」:可以有自己的一份
71
74
 
@@ -120,8 +123,12 @@ dsh plugin --profile <your-profile> add github:aawef/dsh-skill-manager-tags
120
123
 
121
124
  ### 本地开发式安装
122
125
 
123
- 如果你直接改源码,注意**源码目录和 profile 的 `node_modules` 里那份是两个物理文件**,
124
- 改完必须手动复制并校验哈希:
126
+ 运行时加载的是 profile 里 `node_modules/<插件名>` 那一份,不是你的源码目录。两种接法:
127
+
128
+ - **junction(推荐)**:让 `~/.dsh/profiles/web/my-plugins/<插件名>` 指向你的源码目录,
129
+ pnpm 会把它硬链接进 `node_modules` —— 三个路径是**同一个 inode、同一份文件**,
130
+ 改完内容立刻生效,不需要复制;只有**新增/删除文件**才要在 profile 里重跑一次 `pnpm install`。
131
+ - **物理拷贝**:如果当初是手动 `cp` 进去的,那就是两份独立文件,改完必须再 `cp` 一次并校验哈希:
125
132
 
126
133
  ```bash
127
134
  S=/path/to/your/dsh-skill-tag
package/lib/client.js CHANGED
@@ -103,6 +103,20 @@ window.__ModuleLoader__.load({
103
103
  return out;
104
104
  }
105
105
 
106
+ /**
107
+ * 「技能已有的标签排前面」:稳定分区,不改变各分区内部的原有顺序。
108
+ * 只排显示顺序——勾选状态与写库内容都不受影响。
109
+ * @param {Array<{tagId:string}>} tags 全部标签(顺序来自服务端)
110
+ * @param {Iterable<string>} ownedIds 该技能已拥有的 tagId
111
+ */
112
+ function ownedTagsFirst(tags, ownedIds) {
113
+ const owned = new Set(ownedIds ?? []);
114
+ const yes = [];
115
+ const no = [];
116
+ for (const t of tags ?? []) (owned.has(t?.tagId) ? yes : no).push(t);
117
+ return [...yes, ...no];
118
+ }
119
+
106
120
  // ── 「打开技能所在文件夹」 ────────────────────────────────────
107
121
  // 宿主没有给插件 reveal API,服务端由 src/reveal.js 拉起文件管理器。
108
122
  // 这里只负责:名字 → 绝对路径(扫描结果自带 path)、发请求、缺路径时
@@ -515,6 +529,9 @@ window.__ModuleLoader__.load({
515
529
  const ownedIds = selName ? (links.find((l) => l.skillId === selName)?.tagIds ?? []) : [];
516
530
  const owned = new Set(ownedIds);
517
531
  const tagMatch = (t) => !state.skillTagSearch.trim() || t.name.toLowerCase().includes(state.skillTagSearch.trim().toLowerCase());
532
+ // 已拥有的标签排在前面(不再是纯粹按服务端返回的字母序),便于确认"这个技能到底挂了什么"
533
+ const tagsOrdered = ownedTagsFirst(tags, ownedIds);
534
+ const tagsShown = tagsOrdered.filter(tagMatch);
518
535
 
519
536
  /** 整组覆盖该技能的标签集合(勾选/取消都走这里,立即落库) */
520
537
  const saveTags = async (nextIds) => {
@@ -576,9 +593,9 @@ window.__ModuleLoader__.load({
576
593
  }),
577
594
  react.createElement("div", { key: "rows", style: { maxHeight: "360px", overflowY: "auto" } }, [
578
595
  tags.length === 0 && react.createElement("div", { key: "e", style: { color: "#888", fontSize: "12px", padding: "6px 2px" } }, "还没有任何标签,请先在主页面新建"),
579
- tags.filter(tagMatch).length === 0 && tags.length > 0 &&
596
+ tagsShown.length === 0 && tags.length > 0 &&
580
597
  react.createElement("div", { key: "e2", style: { color: "#888", fontSize: "12px", padding: "6px 2px" } }, "无匹配标签"),
581
- ...tags.filter(tagMatch).map((t) => react.createElement("label", { key: t.tagId, style: { ...rowStyle, cursor: "pointer" } }, [
598
+ ...tagsShown.map((t) => react.createElement("label", { key: t.tagId, style: { ...rowStyle, cursor: "pointer" } }, [
582
599
  react.createElement("input", {
583
600
  key: "c", type: "checkbox", checked: owned.has(t.tagId), disabled: busy,
584
601
  onChange: () => toggleTag(t.tagId),
@@ -1126,6 +1143,8 @@ window.__ModuleLoader__.load({
1126
1143
  const [pickOpen, setPickOpen] = react.useState(false);
1127
1144
  const [disabledOpen, setDisabledOpen] = react.useState(false);
1128
1145
  const [tagOfSkill, setTagOfSkill] = react.useState(null);
1146
+ /** 右列技能搜索词(对话版 / 工作区版共用;只影响列表显示,不影响下发结果) */
1147
+ const [skillSearch, setSkillSearch] = react.useState("");
1129
1148
 
1130
1149
  const load = react.useCallback(async () => {
1131
1150
  try {
@@ -1190,6 +1209,10 @@ window.__ModuleLoader__.load({
1190
1209
  /** 可添加的技能:排除已有(可见)与被禁用的 */
1191
1210
  const addable = (ws?.universe ?? []).filter((n) => !visibleSet.has(n) && !disabledSet.has(n));
1192
1211
 
1212
+ // 右列搜索:仅过滤显示,不影响「技能 (总)」这个语义数字
1213
+ const skillQuery = skillSearch.trim().toLowerCase();
1214
+ const shownSkills = (view.visible ?? []).filter((n) => !skillQuery || String(n).toLowerCase().includes(skillQuery));
1215
+
1193
1216
  // 技能名 → SKILL.md 路径(服务端把扫描结果的 path 一并带回来了)
1194
1217
  const catalog = ws?.catalog ?? [];
1195
1218
  const pathOf = (name) => catalog.find((s) => s.name === name)?.path ?? null;
@@ -1236,11 +1259,27 @@ window.__ModuleLoader__.load({
1236
1259
 
1237
1260
  // ── 右列:技能 ──
1238
1261
  react.createElement("div", { key: "right", style: colStyle }, [
1239
- react.createElement("div", { key: "h", style: { fontWeight: 600, marginBottom: "8px" } }, `技能 (${(view.visible ?? []).length})`),
1262
+ react.createElement("div", { key: "h", style: { fontWeight: 600, marginBottom: "8px" } },
1263
+ skillQuery
1264
+ ? `技能 (${shownSkills.length}/${(view.visible ?? []).length})`
1265
+ : `技能 (${(view.visible ?? []).length})`),
1266
+ // 搜索框:放列表顶部,只过滤显示(不改变实际下发的技能集合)
1267
+ react.createElement("div", { key: "search", style: { display: "flex", gap: "6px", alignItems: "center", marginBottom: "6px" } }, [
1268
+ react.createElement("input", {
1269
+ key: "i", style: { ...input, flex: 1, minWidth: 0 }, type: "search", placeholder: "搜索技能…",
1270
+ value: skillSearch, onChange: (e) => setSkillSearch(e.target.value),
1271
+ }),
1272
+ skillSearch.trim() !== "" && react.createElement("button", {
1273
+ key: "c", style: { ...btn, padding: "1px 7px", fontSize: "11px" },
1274
+ onClick: () => setSkillSearch(""),
1275
+ }, "清除"),
1276
+ ]),
1240
1277
  react.createElement("div", { key: "list", style: { flex: 1, overflowY: "auto", minHeight: "80px" } }, [
1241
1278
  (view.visible ?? []).length === 0 && react.createElement("div", { key: "e", style: { color: "#888", fontSize: "12px" } },
1242
1279
  "没有技能。可在下方「+ 精细化添加技能」手动加入,或清空标签条件回到全部技能。"),
1243
- ...(view.visible ?? []).map((name) => react.createElement("div", { key: name, style: { ...listRow, flexWrap: "wrap" } }, [
1280
+ (view.visible ?? []).length > 0 && shownSkills.length === 0 && react.createElement("div", { key: "nf", style: { color: "#888", fontSize: "12px" } },
1281
+ `没有匹配「${skillSearch.trim()}」的技能。`),
1282
+ ...shownSkills.map((name) => react.createElement("div", { key: name, style: { ...listRow, flexWrap: "wrap" } }, [
1244
1283
  react.createElement("span", { key: "n", style: { flex: 1 } }, name),
1245
1284
  manualSet.has(name) && react.createElement("span", { key: "m", style: { color: "#3498db", fontSize: "11px" } }, "手动"),
1246
1285
  react.createElement(RevealButton, { key: "rev", skillName: name, path: pathOf(name), label: "📁" }),
@@ -1266,7 +1305,8 @@ window.__ModuleLoader__.load({
1266
1305
  };
1267
1306
 
1268
1307
  return react.createElement("div", { style: overlayStyle, onMouseDown: (e) => { if (e.target === e.currentTarget) onClose(); } },
1269
- react.createElement("div", { style: modalBox, onClick: (e) => e.stopPropagation() }, [
1308
+ // 固定高度:搜索让右列行数变少时窗口不能跟着缩(否则每敲一个字母窗口就抖一下)
1309
+ react.createElement("div", { style: { ...modalBox, height: "82vh" }, onClick: (e) => e.stopPropagation() }, [
1270
1310
  react.createElement("div", { key: "head", style: { display: "flex", alignItems: "center", gap: "10px", marginBottom: "10px" } }, [
1271
1311
  react.createElement("strong", { key: "t", style: { fontSize: "14px" } }, scope.title),
1272
1312
  react.createElement("span", { key: "w", style: { color: "#888", fontSize: "11px", ...monoStyle } }, scope.id),
@@ -1640,7 +1680,7 @@ window.__ModuleLoader__.load({
1640
1680
  skillsLinkedTo, skillsNotLinkedTo, ZONE_LABEL,
1641
1681
  workspaceIdOfRowKey, isWorkspaceMenu, manage, manageSet,
1642
1682
  manageScope, registerComposerEntry,
1643
- skillPathOf, RevealButton,
1683
+ skillPathOf, RevealButton, ownedTagsFirst,
1644
1684
  };
1645
1685
  return module.exports;
1646
1686
  },
package/lib/index.js CHANGED
@@ -18,7 +18,7 @@ import { skillTagDomain } from "../src/storage/schema.js";
18
18
  import { createDshRepository, serializeConversation, deserializeConversation } from "../src/storage/repository.js";
19
19
  import { createServices } from "../src/service.js";
20
20
  import { groupIdOf } from "../src/domain/model.js";
21
- import { workspaceDeniedSkills, normalizeWorkspacePath, activeTagsOf } from "../src/domain/workspace.js";
21
+ import { workspaceDeniedSkills, workspaceSkillView, normalizeWorkspacePath, activeTagsOf } from "../src/domain/workspace.js";
22
22
  import { candidatesFor } from "../src/domain/match.js";
23
23
  import { mountApi } from "../src/api.js";
24
24
  import { scanSkillRoots } from "../src/skill-scan.js";
@@ -336,6 +336,54 @@ export function runtimeDeniedSkills(ws, universe, candidates) {
336
336
  });
337
337
  }
338
338
 
339
+ /**
340
+ * 运行时真正下发给 provider 的拒绝集:新口径 ∪ 旧口径兜底的**严格补集**。
341
+ *
342
+ * 2026-10-27 修正的 bug:此前调用方把两段结果**无条件并集**——
343
+ * ① workspaceDeniedSkills(懂 manualSkills / disabledSkills)
344
+ * ② computeDeniedSkillIds(只看标签条件,linked − candidates)
345
+ * ② 不知道"手动添加",于是把「打了标签、但没被标签规则命中、靠手动加进对话」
346
+ * 的技能重新判成 deny,推翻 ① 的放行结论。本机唯一受害者是 tagging-skills
347
+ * (它只挂来源标签,靠「管理标签」右列手动添加)。
348
+ *
349
+ * 兜底原本的前提是「universe 扫不到这个名字,所以新口径没机会判定它」。
350
+ * 那就把这个前提写成代码 —— 两道闸:
351
+ * ① 新口径已放行的名字(标签命中 ∪ 手动添加 − 禁用)绝不再 deny;
352
+ * ② universe 里存在的名字已由新口径给出判定,兜底不重复表态。
353
+ * 剩下的才是真正需要兜底的历史残留(已卸载但标签还在的名字)。
354
+ *
355
+ * @param {{config?:object, manualSkills?:string[], disabledSkills?:string[]}} effective 生效配置
356
+ * @param {object} p
357
+ * @param {string[]} p.universe 本趟可发现的全部技能名
358
+ * @param {string[]} p.candidates 标签规则命中的技能名
359
+ * @param {string[]} p.linkedSkillIds 有标签关联的全部技能名(兜底段的输入)
360
+ * @returns {Set<string>}
361
+ */
362
+ export function computeRuntimeDeniedSkillIds(effective, { universe, candidates, linkedSkillIds }) {
363
+ const view = workspaceSkillView({
364
+ config: effective?.config,
365
+ manualSkills: effective?.manualSkills,
366
+ disabledSkills: effective?.disabledSkills,
367
+ universe,
368
+ candidates,
369
+ });
370
+ const denied = workspaceDeniedSkills({
371
+ config: effective?.config,
372
+ manualSkills: effective?.manualSkills,
373
+ disabledSkills: effective?.disabledSkills,
374
+ universe,
375
+ candidates,
376
+ });
377
+ const allowed = new Set(view.visible);
378
+ const inUniverse = new Set(universe);
379
+ for (const skillId of computeDeniedSkillIds(effective?.config, linkedSkillIds, () => candidates)) {
380
+ if (allowed.has(skillId)) continue;
381
+ if (inUniverse.has(skillId)) continue;
382
+ denied.add(skillId);
383
+ }
384
+ return denied;
385
+ }
386
+
339
387
  /**
340
388
  * 技能发现:与「模型实际看到的技能目录」对齐的唯一入口。
341
389
  *
@@ -660,15 +708,13 @@ export async function apply(ctx, config) {
660
708
  config: effective.config,
661
709
  active: activeTagsOf(effective.config),
662
710
  });
663
- for (const skillId of workspaceDeniedSkills({ ...effective, universe, candidates })) denied.add(skillId);
664
- // 旧口径兜底:已被打标签的技能同样要按条件过滤(universe 扫不到时)
665
- for (const skillId of computeDeniedSkillIds(
666
- effective.config,
667
- links.map((l) => l.skillId),
668
- () => candidates,
669
- )) {
670
- denied.add(skillId);
671
- }
711
+ // 当前生效配置下"该放行"的名字(标签命中 ∪ 手动添加 − 禁用)。
712
+ // 兜底段必须尊重它,否则会把手动添加的技能重新判成 deny。
713
+ for (const skillId of computeRuntimeDeniedSkillIds(effective, {
714
+ universe,
715
+ candidates,
716
+ linkedSkillIds: links.map((l) => l.skillId),
717
+ })) denied.add(skillId);
672
718
  }
673
719
  }
674
720
  const after = [...denied].join(",");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-tag",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "给 DSH 的技能(skill)打标签,用一条布尔表达式在「工作区 / 对话」两级选出要下发给模型的技能子集",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",