@yottameta/yotta-skills 0.19.4 → 0.19.5

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## v0.19.5 (2026-09-15)
2
+
3
+ **多源技能盘点版本修复**:
4
+
5
+ - 同名技能多副本扫描新增 `variants` 明细,代表版本取合法版本中的最高 semver;版本差异持久化为 `conflicts`,不再由首个来源副本决定显示版本。
6
+ - `--inventory` / `--route` 显示代表版本与多副本摘要;`doctor` 优先按目标目录匹配精确副本版本,旧注册表缺少 `variants` 时回退聚合版本。
7
+ - 回归覆盖多副本 semver 选择、注册表幂等、CLI 文本/JSON 与 doctor 精确匹配;`npm test` 168/168。
8
+
1
9
  ## v0.19.4 (2026-09-14)
2
10
 
3
11
  **元造 0.1.2 清单同步**:
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yotta-skills
3
- version: 0.19.4
3
+ version: 0.19.5
4
4
  description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 运行时 hook 适配。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录(默认 --pin 锁死清单精确版本);盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工只建议跑本地 --reindex;更新检查走手动 --check 或后台 --check --scheduled);运行时适配层:hook capabilities / evaluate / bind / unbind 按宿主能力矩阵执行六个统一事件并留证降级,元信 before_install 已接入安装管线;自包含零依赖,不依赖任何元技能;MCP 按需加载且需用户确认后写入配置(可选:list_installed_skills/describe_skill/reindex/route_request,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
5
5
  license: MIT
6
6
  metadata:
@@ -37,7 +37,7 @@ const hookAdapterLib = require('../lib/hook-adapter');
37
37
  const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
38
38
 
39
39
  const PKG_ROOT = path.join(__dirname, '..');
40
- let VERSION = '0.19.4';
40
+ let VERSION = '0.19.5';
41
41
  try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
42
42
 
43
43
  function loadManifest() {
@@ -1189,7 +1189,15 @@ function runRoute(opts) {
1189
1189
  out('');
1190
1190
  out('调用顺序:');
1191
1191
  for (const skill of result.skills) {
1192
- out(' ' + skill.order + '. ' + skill.slug + ' [' + (skill.installed ? '已装' : '缺失') + '] - ' + skill.role);
1192
+ const status = skill.installed
1193
+ ? '已装' + (skill.version ? ' v' + skill.version : '')
1194
+ : '缺失';
1195
+ const conflictHint = skill.conflicts && skill.conflicts.length
1196
+ ? '(其他副本: ' + skill.conflicts
1197
+ .map((item) => (item.source || '其他') + ' v' + (item.version || '?'))
1198
+ .join('、') + ')'
1199
+ : '';
1200
+ out(' ' + skill.order + '. ' + skill.slug + ' [' + status + '] - ' + skill.role + conflictHint);
1193
1201
  }
1194
1202
  if (result.missing_skills.length) {
1195
1203
  out('');
@@ -24,6 +24,20 @@ function readManifest(target, slug) {
24
24
  }
25
25
  }
26
26
 
27
+ /** 优先读取目标目录对应的副本版本,旧注册表缺少 variants 时回退聚合版本。 */
28
+ function registryVersionForTarget(record, target) {
29
+ const variants = Array.isArray(record.variants) ? record.variants : [];
30
+ const normalizedTarget = path.resolve(target);
31
+ const exact = variants.find((variant) => {
32
+ if (!variant || !variant.dir) return false;
33
+ return path.resolve(variant.dir) === normalizedTarget;
34
+ });
35
+ if (exact && exact.version) {
36
+ return { version: String(exact.version).trim(), scope: 'target' };
37
+ }
38
+ return { version: record.version ? String(record.version).trim() : '', scope: 'aggregate' };
39
+ }
40
+
27
41
  function checkInstalledSkill(context) {
28
42
  const ctx = context || {};
29
43
  const slug = String(ctx.slug || '');
@@ -122,18 +136,22 @@ function checkInstalledSkill(context) {
122
136
  addCheck(checks, 'registry', false, '注册表未记录当前技能', 'warning');
123
137
  warnings.push('注册表未记录当前技能');
124
138
  fixes.push('运行 yotta-skills --reindex 重扫注册表');
125
- } else if (version && record.version && record.version !== version) {
126
- addCheck(
127
- checks,
128
- 'registry',
129
- false,
130
- '注册表版本 v' + record.version + ' 与已安装 v' + version + ' 不一致',
131
- 'warning',
132
- );
133
- warnings.push('注册表版本 v' + record.version + ' 与已安装 v' + version + ' 不一致');
134
- fixes.push('运行 yotta-skills --reindex 重扫注册表');
135
139
  } else {
136
- addCheck(checks, 'registry', true, '注册表记录一致');
140
+ const registryVersion = registryVersionForTarget(record, target);
141
+ if (version && registryVersion.version && registryVersion.version !== version) {
142
+ const scopeText = registryVersion.scope === 'target' ? '注册表版本(目标副本)' : '注册表版本(聚合)';
143
+ addCheck(
144
+ checks,
145
+ 'registry',
146
+ false,
147
+ scopeText + ' v' + registryVersion.version + ' 与已安装 v' + version + ' 不一致',
148
+ 'warning',
149
+ );
150
+ warnings.push(scopeText + ' v' + registryVersion.version + ' 与已安装 v' + version + ' 不一致');
151
+ fixes.push('运行 yotta-skills --reindex 重扫注册表');
152
+ } else {
153
+ addCheck(checks, 'registry', true, '注册表记录一致');
154
+ }
137
155
  }
138
156
  }
139
157
 
package/lib/route.js CHANGED
@@ -183,6 +183,8 @@ function routeRequest(request, options) {
183
183
  installed: isInstalled,
184
184
  version: isInstalled ? record.version || '' : '',
185
185
  sources: isInstalled ? record.sources || [] : [],
186
+ variants: isInstalled && Array.isArray(record.variants) ? record.variants : [],
187
+ conflicts: isInstalled && Array.isArray(record.conflicts) ? record.conflicts : [],
186
188
  };
187
189
  });
188
190
  const missingSkills = skills
@@ -81,6 +81,67 @@ function parseFrontmatter(text) {
81
81
  return fm;
82
82
  }
83
83
 
84
+ /** 解析 semver(用于多副本版本选择;无法解析时返回 null)。 */
85
+ function parseSemver(value) {
86
+ const match = String(value || '').trim().match(
87
+ /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/,
88
+ );
89
+ if (!match) return null;
90
+ return {
91
+ major: Number(match[1]),
92
+ minor: Number(match[2]),
93
+ patch: Number(match[3]),
94
+ prerelease: match[4] ? match[4].split('.') : [],
95
+ };
96
+ }
97
+
98
+ function comparePrerelease(left, right) {
99
+ if (!left.length && !right.length) return 0;
100
+ if (!left.length) return 1;
101
+ if (!right.length) return -1;
102
+ const length = Math.max(left.length, right.length);
103
+ for (let i = 0; i < length; i++) {
104
+ if (left[i] === undefined) return -1;
105
+ if (right[i] === undefined) return 1;
106
+ const a = left[i];
107
+ const b = right[i];
108
+ const aNumeric = /^\d+$/.test(a);
109
+ const bNumeric = /^\d+$/.test(b);
110
+ if (aNumeric && bNumeric) {
111
+ const diff = Number(a) - Number(b);
112
+ if (diff) return diff;
113
+ } else if (aNumeric !== bNumeric) {
114
+ return aNumeric ? -1 : 1;
115
+ } else if (a !== b) {
116
+ return a < b ? -1 : 1;
117
+ }
118
+ }
119
+ return 0;
120
+ }
121
+
122
+ /** semver 比较:left > right 返回正数,left < right 返回负数。 */
123
+ function compareSemver(left, right) {
124
+ const a = parseSemver(left);
125
+ const b = parseSemver(right);
126
+ if (!a || !b) return 0;
127
+ for (const key of ['major', 'minor', 'patch']) {
128
+ if (a[key] !== b[key]) return a[key] - b[key];
129
+ }
130
+ return comparePrerelease(a.prerelease, b.prerelease);
131
+ }
132
+
133
+ /** 从多个安装副本中选择代表版本:优先合法版本中的最高 semver。 */
134
+ function selectRepresentativeVariant(variants) {
135
+ let selected = null;
136
+ for (const variant of variants) {
137
+ if (!parseSemver(variant.version)) continue;
138
+ if (!selected || compareSemver(variant.version, selected.version) > 0) {
139
+ selected = variant;
140
+ }
141
+ }
142
+ return selected || variants.find((variant) => variant.version) || variants[0] || null;
143
+ }
144
+
84
145
  /** 扫描单个技能目录:子目录含 SKILL.md 且 frontmatter 有 name 即视为技能。 */
85
146
  function scanSkillDir(dir) {
86
147
  const results = [];
@@ -179,21 +240,41 @@ function scanRoots(roots) {
179
240
  scanned.push({ dir: r.dir, label: r.label });
180
241
  for (const s of scanSkillDir(r.dir)) {
181
242
  const key = s.slug;
243
+ const variant = {
244
+ source: r.label,
245
+ dir: s.source_dir,
246
+ version: s.version || '',
247
+ description: s.description || '',
248
+ };
182
249
  const item = map.get(key);
183
250
  if (item) {
184
251
  if (!item.sources.includes(r.label)) item.sources.push(r.label);
185
252
  if (!item.source_dirs.includes(s.source_dir)) item.source_dirs.push(s.source_dir);
186
- if (item.version && s.version && item.version !== s.version) {
187
- item.conflicts = item.conflicts || [];
188
- item.conflicts.push({ dir: s.source_dir, version: s.version });
189
- }
253
+ item.variants.push(variant);
190
254
  } else {
191
255
  s.sources = [r.label];
192
256
  s.source_dirs = [s.source_dir];
257
+ s.variants = [variant];
193
258
  map.set(key, s);
194
259
  }
195
260
  }
196
261
  }
262
+ for (const item of map.values()) {
263
+ const representative = selectRepresentativeVariant(item.variants);
264
+ if (!representative) continue;
265
+ item.version = representative.version || '';
266
+ if (representative.description) item.description = representative.description;
267
+ item.source_dir = representative.dir || item.source_dir;
268
+ const conflicts = item.variants
269
+ .filter((variant) => variant.version && variant.version !== item.version)
270
+ .map((variant) => ({
271
+ source: variant.source,
272
+ dir: variant.dir,
273
+ version: variant.version,
274
+ }));
275
+ if (conflicts.length) item.conflicts = conflicts;
276
+ else delete item.conflicts;
277
+ }
197
278
  return {
198
279
  generated_at: new Date().toISOString(),
199
280
  skills: Array.from(map.values()).sort((a, b) => a.slug.localeCompare(b.slug)),
@@ -231,30 +312,33 @@ function mergeRegistry(scanResult, previous) {
231
312
  for (const s of scanResult.skills) {
232
313
  seen.add(s.slug);
233
314
  const old = prev[s.slug];
315
+ const variants = Array.isArray(s.variants) ? s.variants : [];
316
+ const conflicts = Array.isArray(s.conflicts) ? s.conflicts : [];
317
+ const nextRecord = old ? { ...old } : {
318
+ slug: s.slug,
319
+ first_seen: now,
320
+ };
321
+ nextRecord.version = s.version;
322
+ nextRecord.description = s.description;
323
+ nextRecord.sources = s.sources;
324
+ nextRecord.source_dirs = s.source_dirs;
325
+ nextRecord.last_seen = now;
326
+ nextRecord.status = 'known';
327
+ if (variants.length) nextRecord.variants = variants;
328
+ else delete nextRecord.variants;
329
+ if (conflicts.length) nextRecord.conflicts = conflicts;
330
+ else delete nextRecord.conflicts;
234
331
  if (!old) {
235
- next[s.slug] = {
236
- slug: s.slug,
237
- version: s.version,
238
- description: s.description,
239
- sources: s.sources,
240
- source_dirs: s.source_dirs,
241
- first_seen: now,
242
- last_seen: now,
243
- status: 'known',
244
- };
332
+ next[s.slug] = nextRecord;
245
333
  changes.added.push(s.slug);
246
334
  } else {
247
335
  const changed = old.version !== s.version
248
336
  || old.description !== s.description
249
- || JSON.stringify(old.sources || []) !== JSON.stringify(s.sources);
250
- next[s.slug] = {
251
- ...old,
252
- version: s.version,
253
- description: s.description,
254
- sources: s.sources,
255
- source_dirs: s.source_dirs,
256
- last_seen: now,
257
- };
337
+ || JSON.stringify(old.sources || []) !== JSON.stringify(s.sources)
338
+ || JSON.stringify(old.source_dirs || []) !== JSON.stringify(s.source_dirs)
339
+ || JSON.stringify(old.variants || []) !== JSON.stringify(variants)
340
+ || JSON.stringify(old.conflicts || []) !== JSON.stringify(conflicts);
341
+ next[s.slug] = nextRecord;
258
342
  if (changed) changes.updated.push(s.slug);
259
343
  }
260
344
  }
@@ -295,7 +379,14 @@ function formatInventory(registry, filePath) {
295
379
  for (const s of skills) {
296
380
  const ver = s.version ? 'v' + s.version : '-';
297
381
  const src = (s.sources || []).join(',');
298
- lines.push(`${s.slug} ${ver} ${s.description || ''} [${src}]`);
382
+ const versions = new Set((s.variants || []).map((variant) => variant.version).filter(Boolean));
383
+ const variantHint = versions.size > 1
384
+ ? ' {多副本: ' + (s.variants || [])
385
+ .filter((variant) => variant.version)
386
+ .map((variant) => (variant.source || '其他') + ' v' + variant.version)
387
+ .join(', ') + '}'
388
+ : '';
389
+ lines.push(`${s.slug} ${ver} ${s.description || ''} [${src}]${variantHint}`);
299
390
  }
300
391
  const rp = filePath || registryPath();
301
392
  lines.push(`共 ${skills.length} 个技能;注册表:${rp}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-skills",
3
- "version": "0.19.4",
3
+ "version": "0.19.5",
4
4
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -25,7 +25,7 @@ import subprocess
25
25
  import sys
26
26
  from pathlib import Path
27
27
 
28
- VERSION = "0.19.4"
28
+ VERSION = "0.19.5"
29
29
  TOOL_NAME = "yotta-skills"
30
30
  CN_NAME = "元阁"
31
31
  MCP_PROTOCOL_MODERN = "2026-07-28"
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-skills",
4
4
  "name": "元阁",
5
5
  "package": "@yottameta/yotta-skills",
6
- "version": "0.19.4",
6
+ "version": "0.19.5",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true