dsh-toolbox-ui 1.6.4 → 1.6.6

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/lib/index.js CHANGED
@@ -12,8 +12,9 @@ import { admitImportedServers } from "./import-admission.js";
12
12
  import { collectImportLocations, missingEnvNames, resolveImportLocation } from "./mcp-import-sources.js";
13
13
  import { deriveMcpPhase, formatMcpLog, sanitizeMcpLog } from "./mcp-observability.js";
14
14
  import { allMcpEntries, entriesForName, projectToolSchema, revisionOf, toolInventory } from "./mcp-registry.js";
15
- import { invocationOf, listSkillViews, resolveTargetReference, skillRowKey, skillTargetsView } from "./skills-registry.js";
16
- import { importSkill, readSkillDocument, removeSkill, resolveSkillTarget, scanDisabledSkills, setSkillEnabled, setSkillModelInvocable } from "./skills-file-ops.js";
15
+ import { diagnoseSkill } from "./skills-frontmatter.js";
16
+ import { invocationOf, listSkillViews, resolveSkillReference, resolveTargetReference, skillRowKey, skillTargetsView } from "./skills-registry.js";
17
+ import { importSkill, readSkillDocument, removeSkill, scanDisabledSkills, setSkillEnabled, setSkillModelInvocable, setSkillUserInvocable } from "./skills-file-ops.js";
17
18
  import { downloadHubArchive, hubCategories, hubSkillDetail, searchHubSkills } from "./skillhub.js";
18
19
  import { setOwn } from "./own-property.js";
19
20
  import { WHATS_NEW } from "./whats-new.js";
@@ -1448,7 +1449,7 @@ let McpManagerGateway = (() => {
1448
1449
  let SkillsManagerGateway = (() => {
1449
1450
  let _classSuper = TypertRemoteService;
1450
1451
  let _instanceExtraInitializers = [];
1451
- let _list_decorators, _detail_decorators, _setEnabled_decorators, _setModelInvocable_decorators, _removeSkill_decorators, _importSkill_decorators, _skillTargets_decorators, _skillHubSearch_decorators, _skillHubDetail_decorators, _skillHubCategories_decorators, _skillHubInstall_decorators, _conflictingManager_decorators;
1452
+ let _list_decorators, _detail_decorators, _setEnabled_decorators, _setModelInvocable_decorators, _setUserInvocable_decorators, _removeSkill_decorators, _importSkill_decorators, _skillTargets_decorators, _skillHubSearch_decorators, _skillHubDetail_decorators, _skillHubCategories_decorators, _skillHubInstall_decorators, _conflictingManager_decorators;
1452
1453
  return class SkillsManagerGateway extends _classSuper {
1453
1454
  static {
1454
1455
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
@@ -1460,6 +1461,8 @@ let SkillsManagerGateway = (() => {
1460
1461
  __esDecorate(this, null, _setEnabled_decorators, { kind: "method", name: "setEnabled", static: false, private: false, access: { has: (obj) => "setEnabled" in obj, get: (obj) => obj.setEnabled }, metadata: _metadata }, null, _instanceExtraInitializers);
1461
1462
  _setModelInvocable_decorators = [Remote("setModelInvocable")];
1462
1463
  __esDecorate(this, null, _setModelInvocable_decorators, { kind: "method", name: "setModelInvocable", static: false, private: false, access: { has: (obj) => "setModelInvocable" in obj, get: (obj) => obj.setModelInvocable }, metadata: _metadata }, null, _instanceExtraInitializers);
1464
+ _setUserInvocable_decorators = [Remote("setUserInvocable")];
1465
+ __esDecorate(this, null, _setUserInvocable_decorators, { kind: "method", name: "setUserInvocable", static: false, private: false, access: { has: (obj) => "setUserInvocable" in obj, get: (obj) => obj.setUserInvocable }, metadata: _metadata }, null, _instanceExtraInitializers);
1463
1466
  _removeSkill_decorators = [Remote("removeSkill")];
1464
1467
  __esDecorate(this, null, _removeSkill_decorators, { kind: "method", name: "removeSkill", static: false, private: false, access: { has: (obj) => "removeSkill" in obj, get: (obj) => obj.removeSkill }, metadata: _metadata }, null, _instanceExtraInitializers);
1465
1468
  _importSkill_decorators = [Remote("importSkill")];
@@ -1487,130 +1490,141 @@ let SkillsManagerGateway = (() => {
1487
1490
  async list(payload) {
1488
1491
  const workspaces = Array.isArray(payload?.workspaces) ? payload.workspaces.map(String) : [];
1489
1492
  const rows = await listSkillViews(this.ctx, { workspaces });
1490
- // 已禁用技能是**独立一组**:宿主目录里看不到它们(SPEC 约束 3),只能扫各根下的 .disabled。
1493
+ // ⚠️ 「已停用」不再是**另一个数组**,而是行上的派生态(enabled)。
1491
1494
  //
1492
- // ⚠️ 待扫的根必须来自**能力**(skillTargetsView),不能从可见行反推:
1493
- // 某个根里的技能**全被停用**时,可见行里根本没有那个根 → 它永远不被扫 →
1494
- // 面板零显示、也无法恢复(与"启用判据写反"叠加就是永久失联)。
1495
- // 顺带:目标自带 source / projectPath,正是客户端分组、作用域标签与药丸需要的两样东西
1496
- // ——缺了它们,所有已停用技能会并成一个无名组,并弹出一个空标签的药丸。
1495
+ // 旧版这里是 `{ skills, disabled }` 两组:停用 = 把 SKILL.md 改名成 .disabled,
1496
+ // 宿主因此看不见它,面板只好自己扫盘补一份。那套做法有两个结构性代价,都已实际爆过:
1497
+ // ① 状态只存在于文件名里 → 现场扫盘比宿主缓存新 → 同一个身份在两组里各出现一次
1498
+ // → 客户端 React 重复 key(生产版不报警告,静默重复渲染 + 徽章挂错行);
1499
+ // ② `.disabled` 不是任何规范认可的形态,而 `~/.agents/skills` 是 Agent Skills 标准的
1500
+ // 共享位置(VS Code / Codex 都扫它)——改名等于把用户的技能在别的工具里弄坏。
1501
+ //
1502
+ // 现在停用 = 写宿主**原生的两个调用面字段**(disable-model-invocation + user-invocable),
1503
+ // 而宿主的 list 是**调用无关**的:两个面都关掉的技能照样出现在目录里。
1504
+ // 于是「面板看得见的东西」与「宿主报告的东西」重新变成同一件事——
1505
+ // 不需要扫盘、不需要两组互斥、不可能双行。这是一个身份一行在结构上的保证。
1506
+ const skills = rows.map((row) => ({ ...row, key: skillRowKey(row) }));
1507
+ // 旧版约定(.disabled 改名)留下的条目:**一次性迁移读取**,不是第二个真值来源。
1508
+ // 1.6.x 的用户磁盘上可能已经有这种文件(我们自己写进去的),必须列出来并能恢复,
1509
+ // 否则那些技能永远回不来。用户恢复之后就走新机制;本段与它的 wire 字段可整体删除。
1510
+ //
1511
+ // 待扫的根必须来自**能力**(skillTargetsView),不能从可见行反推:某个根里的技能
1512
+ // 全被旧机制停用时,可见行里根本没有那个根 → 它永远不被扫 → 面板零显示、也救不回来。
1497
1513
  const { targets } = await skillTargetsView(this.ctx, { workspaces });
1498
- const disabled = [];
1514
+ const migrated = [];
1515
+ const seenKeys = new Set(skills.map((row) => row.key));
1499
1516
  for (const target of targets) {
1500
- if (target.writable === false) continue; // bundled 根只读,没有可启用的 .disabled
1517
+ if (target.writable === false) continue; // bundled 根只读:我们从不往那里写 .disabled
1501
1518
  if (target.exists === false) continue; // 目录还没建,扫它只会白挨一个 ENOENT
1502
1519
  for (const row of await scanDisabledSkills({ root: target.root, scope: target.scope })) {
1503
- disabled.push({
1504
- ...row,
1520
+ // 技能名要**读那个文件**才知道(旧机制的条目名也不保证等于技能名)。
1521
+ const document = await readSkillDocument(row).catch(() => undefined);
1522
+ const diagnosed = document === undefined ? undefined : diagnoseSkill(document.text);
1523
+ const name = diagnosed?.name ?? row.entry;
1524
+ const identity = {
1525
+ name,
1505
1526
  source: target.source,
1506
1527
  cwd: target.projectPath,
1528
+ root: target.root,
1529
+ entry: row.entry,
1530
+ kind: row.kind,
1531
+ };
1532
+ const key = skillRowKey(identity);
1533
+ // 同一个身份已经在可见行里(用户恢复了一半、或宿主也报了这一条):可见行优先,
1534
+ // 它是宿主认可的当前真相。这里只补宿主**看不见**的那些,仍然保证一个身份一行。
1535
+ if (seenKeys.has(key)) continue;
1536
+ seenKeys.add(key);
1537
+ // ⚠️ 迁移行的权限推导**单独**兜住读失败:这里任何一个文件读不出来都不该让 list
1538
+ // 整体 reject——那会让面板技能页整块打不开,连健康的技能都管不了。
1539
+ // 注意这只兜「这个文件读不到」这一类(`invocationOf` 自己已经把「读不到」
1540
+ // 压成 null),不是把 bug 藏起来:真正的实现错误照原样抛。
1541
+ const invocation = await invocationOf({
1542
+ entry: row.entry, root: target.root, kind: row.kind, scope: target.scope,
1543
+ });
1544
+ migrated.push({
1545
+ ...identity,
1546
+ key,
1547
+ description: diagnosed?.description ?? "",
1548
+ ...invocation,
1549
+ enabled: false,
1550
+ scope: target.scope,
1551
+ provider: "filesystem",
1552
+ path: row.path,
1553
+ source: target.source,
1507
1554
  writable: target.writable !== false,
1508
- // 调用权限与可见行走**同一处**推导(skills-registry 的 invocationOf):宿主目录里
1509
- // 看不到停用件,这个文件是唯一能回答「能不能被模型调用」的地方。缺了它,客户端
1510
- // 会把「未知」渲染成「不可调用」——一个凭空的断言。
1511
- ...(await invocationOf({ name: row.name, root: target.root, scope: target.scope })),
1555
+ // 客户端据此把这一行标成「旧机制留下的」,并给出与新机制不同的操作文案。
1556
+ migrated: true,
1512
1557
  });
1513
1558
  }
1514
1559
  }
1515
- disabled.sort((left, right) => left.name.localeCompare(right.name));
1516
- // ⚠️ 两个数组必须**互斥**:同一个身份(source \0 cwd \0 name)只能出现在一边。
1517
- //
1518
- // 宿主视图来自它的发现缓存,而停用件是我们**现场扫盘**得到的。面板刚把 `SKILL.md`
1519
- // 改名成 `.disabled` 时,缓存里那份仍然报「已启用」(宿主不会因为一次 rename 就丢缓存),
1520
- // 于是同一个身份在 `skills` 与 `disabled` 里各出现一次。客户端按 `skillIdentity` 生成
1521
- // 行 key,两行 key 相同 → React **重复 key**:生产版不报警告,只是把那一行重复渲染,
1522
- // 并把「被遮蔽」徽章挂到**别的行**上(真机实测:停用一个技能后,它自己重复 3 份、
1523
- // 邻居被误标「被遮蔽」,详情里的状态也停在旧值)。
1524
- //
1525
- // 现场扫盘比缓存新,所以**停用件优先**:把同身份的可见行剔掉。这不是给客户端打的补丁
1526
- // ——「一个身份一行」是这两个数组的契约,就该在产出它们的地方成立。
1527
- const disabledKeys = new Set(disabled.map((row) => skillRowKey(row)));
1528
- const visible = rows.filter((row) => !disabledKeys.has(skillRowKey(row)));
1560
+ migrated.sort((left, right) => left.name.localeCompare(right.name));
1529
1561
  // 曾经这里还返回一个 `revision`(目录指纹)供面板轮询判断要不要重渲染——
1530
1562
  // 但技能面从来没有轮询循环,那个字段没有任何消费者,已删(MCP 面有它自己的指纹与轮询)。
1531
- return { skills: visible, disabled };
1563
+ return { skills: [...skills, ...migrated] };
1532
1564
  }
1533
1565
  async detail(payload) {
1534
- const name = String(payload?.name ?? "");
1535
- if (name === "") throw new Error("缺少技能名");
1536
1566
  // ⚠️ 按**引用定位到的那个根**读正文,不再走 `ctx.skills.get(name)`:那个按 rank
1537
- // 返回**生效者**,而面板的模型是 (name, root) 精确操作(D8)——点「被遮蔽」那一行
1567
+ // 返回**生效者**,而面板的模型是 (根, 条目) 精确操作(D8)——点「被遮蔽」那一行
1538
1568
  // 会看到生效者的正文,用户以为自己在读被遮蔽的那份。
1539
- // 顺带把「已停用」的正文也读得出来:停用时 resolveSkillTarget 返回的是 `.disabled` 路径。
1540
- const target = await resolveTargetReference(this.ctx, {
1541
- source: payload?.source,
1542
- root: payload?.root,
1543
- projectPath: payload?.projectPath,
1544
- });
1545
- const resolved = await resolveSkillTarget({
1546
- name,
1547
- root: target.root,
1548
- scope: target.scope,
1549
- writable: target.writable !== false,
1550
- });
1569
+ const resolved = await resolveSkillReference(this.ctx, payload);
1551
1570
  // 正文与调用权限来自**同一次读**(readSkillDocument 的契约):分两次读会配错版本,
1552
1571
  // 用户看到的就是「这份正文 + 别人的权限」。
1553
1572
  const document = await readSkillDocument(resolved);
1573
+ // 技能名以**文件里**的 frontmatter 为准(磁盘条目名可以与之不同,见 resolveSkillTarget)。
1574
+ const name = diagnoseSkill(document.text).name ?? String(payload?.name ?? "");
1575
+ const modelInvocable = document.invocation?.modelInvocable ?? null;
1576
+ const userInvocable = document.invocation?.userInvocable ?? null;
1577
+ // ⚠️ `enabled` 必须与 `list` 的**同一套判据**(否则同一件事在两个 RPC 上结论相反)。
1578
+ // 两条分支缺一不可:
1579
+ // · 旧机制残留:它的「停用」在**文件名**里(`.disabled`),不在 frontmatter 里,
1580
+ // 所以恒为 false——照内容派生会把它说成「已启用」(真机实测过这个矛盾)。
1581
+ // · 普通技能:两个调用面都关掉才是停用;读不出来(null)就是不知道。
1582
+ const migrated = resolved.migratedDisabled === true;
1583
+ const enabled = migrated
1584
+ ? false
1585
+ : modelInvocable === null || userInvocable === null
1586
+ ? null
1587
+ : !(modelInvocable === false && userInvocable === false);
1554
1588
  return {
1555
1589
  skill: {
1556
1590
  name,
1557
1591
  content: document.text,
1558
- modelInvocable: document.invocation?.modelInvocable ?? null,
1559
- userInvocable: document.invocation?.userInvocable ?? null,
1592
+ modelInvocable,
1593
+ userInvocable,
1594
+ enabled,
1560
1595
  kind: resolved.kind,
1596
+ entry: resolved.entry,
1561
1597
  path: resolved.path,
1562
- enabled: resolved.disabled !== true,
1563
- source: target.source,
1564
- root: target.root,
1565
- scope: target.scope,
1598
+ migrated,
1599
+ source: payload?.source,
1600
+ root: resolved.root,
1601
+ scope: resolved.scope,
1566
1602
  },
1567
1603
  };
1568
1604
  }
1569
1605
  async setEnabled(payload) {
1570
- // ⚠️ `root` / `writable` **不再从载荷取**:wire 只送一个引用,服务端拿自己算出来的
1571
- // 目标核对(见 resolveTargetReference)。否则「写入限定在技能根内」只由客户端保证。
1572
- const target = await resolveTargetReference(this.ctx, {
1573
- source: payload?.source,
1574
- root: payload?.root,
1575
- projectPath: payload?.projectPath,
1576
- });
1577
- const resolved = await resolveSkillTarget({
1578
- name: String(payload?.name ?? ""),
1579
- root: target.root,
1580
- scope: target.scope,
1581
- writable: target.writable !== false,
1582
- });
1606
+ const resolved = await resolveSkillReference(this.ctx, payload);
1583
1607
  return await setSkillEnabled(resolved, payload?.enabled !== false);
1584
1608
  }
1585
1609
  async setModelInvocable(payload) {
1586
1610
  // 「允许模型自动调用」的开关:写技能文件的 disable-model-invocation。纪律与 setEnabled
1587
1611
  // 完全一致——wire 只送引用、服务端核对自己算出来的目标、只读根拒绝;改写的形状规则在
1588
1612
  // lib/skills-frontmatter.js,读不准时那个错误原样上抛给用户。
1589
- const target = await resolveTargetReference(this.ctx, {
1590
- source: payload?.source,
1591
- root: payload?.root,
1592
- projectPath: payload?.projectPath,
1593
- });
1594
- const resolved = await resolveSkillTarget({
1595
- name: String(payload?.name ?? ""),
1596
- root: target.root,
1597
- scope: target.scope,
1598
- writable: target.writable !== false,
1599
- });
1613
+ const resolved = await resolveSkillReference(this.ctx, payload);
1600
1614
  return await setSkillModelInvocable(resolved, payload?.modelInvocable !== false);
1601
1615
  }
1616
+ async setUserInvocable(payload) {
1617
+ // 「用户可调用」的开关:写技能文件的 user-invocable。
1618
+ //
1619
+ // ⚠️ 为什么这个面也要有开关,而不是只让它只读显示:宿主支持的是**两个**调用面,而
1620
+ // 「停用」= 两个都关这一种组合。只暴露一个面的话,用户在面板里拨不回「仅手动可调用」
1621
+ // 那一档——只能去编辑文件。补上之后面板与文件是 1:1 的,`setEnabled` 退回快捷动作。
1622
+ // 与 setModelInvocable 共用同一个实现(setSkillInvocationFace),差别只在写哪个字段。
1623
+ const resolved = await resolveSkillReference(this.ctx, payload);
1624
+ return await setSkillUserInvocable(resolved, payload?.userInvocable !== false);
1625
+ }
1602
1626
  async removeSkill(payload) {
1603
- const target = await resolveTargetReference(this.ctx, {
1604
- source: payload?.source,
1605
- root: payload?.root,
1606
- projectPath: payload?.projectPath,
1607
- });
1608
- const resolved = await resolveSkillTarget({
1609
- name: String(payload?.name ?? ""),
1610
- root: target.root,
1611
- scope: target.scope,
1612
- writable: target.writable !== false,
1613
- });
1627
+ const resolved = await resolveSkillReference(this.ctx, payload);
1614
1628
  return await removeSkill(resolved);
1615
1629
  }
1616
1630
  async importSkill(payload) {