@yottameta/yotta-skills-plugin 0.25.1 → 0.26.0

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.
@@ -300,11 +300,30 @@ function classifyTarget(target, hubDir) {
300
300
  return { kind: 'missing' };
301
301
  }
302
302
  if (stat.isSymbolicLink()) {
303
- const real = safeRealpath(target);
303
+ let real = null;
304
+ let targetExists = false;
305
+ try {
306
+ real = fs.realpathSync.native ? fs.realpathSync.native(target) : fs.realpathSync(target);
307
+ targetExists = true;
308
+ } catch (_) {
309
+ try {
310
+ real = fs.realpathSync(target);
311
+ targetExists = true;
312
+ } catch (_) { /* broken link: fall back to the recorded link value */ }
313
+ }
314
+ if (!real) {
315
+ // A broken junction / symlink no longer resolves. Read the link value so
316
+ // an unlink can still tell whether it points into the hub (fail-closed
317
+ // cleanup path) without treating it as an arbitrary directory.
318
+ let linkValue = null;
319
+ try { linkValue = fs.readlinkSync(target); } catch (_) { linkValue = null; }
320
+ real = linkValue ? path.resolve(path.dirname(path.resolve(target)), linkValue) : path.resolve(target);
321
+ }
304
322
  return {
305
323
  kind: 'link',
306
324
  target: real,
307
325
  inHub: isInside(hubDir, real),
326
+ targetExists,
308
327
  };
309
328
  }
310
329
  if (stat.isDirectory()) return { kind: 'directory' };
@@ -333,7 +352,7 @@ function linkSkills(options) {
333
352
  const target = path.join(targetDir, slug);
334
353
  const current = classifyTarget(target, hubDir);
335
354
  let backup = null;
336
- if (current.kind === 'link' && current.inHub && samePath(current.target, skill.dir)) {
355
+ if (current.kind === 'link' && current.inHub && current.targetExists !== false && samePath(current.target, skill.dir)) {
337
356
  results.push({ slug, status: 'linked', target, note: '已链接到 hub' });
338
357
  continue;
339
358
  }
@@ -462,9 +481,9 @@ function linkStatus(hubDir) {
462
481
  const current = classifyTarget(item.target, hubDir);
463
482
  return {
464
483
  ...item,
465
- status: current.kind === 'link' && current.inHub ? 'ok'
466
- : current.kind === 'missing' ? 'broken'
467
- : 'drift',
484
+ status: current.kind === 'link'
485
+ ? (!current.inHub ? 'drift' : (current.targetExists === false ? 'broken' : 'ok'))
486
+ : current.kind === 'missing' ? 'broken' : 'drift',
468
487
  actual: current,
469
488
  };
470
489
  });
@@ -80,10 +80,10 @@ function readVersion(root) {
80
80
  function validateSnapshot(snapshot) {
81
81
  const root = path.resolve(snapshot || '');
82
82
  if (!snapshot || !fs.existsSync(root)) {
83
- return { ok: false, version: null, digest: null, legacy: false, reason: '快照目录不存在' };
83
+ return { ok: false, version: null, digest: null, legacy: false, reason: '快照目录不存在', source: null, createdAt: null };
84
84
  }
85
85
  if (!fs.statSync(root).isDirectory()) {
86
- return { ok: false, version: null, digest: null, legacy: false, reason: '快照不是目录' };
86
+ return { ok: false, version: null, digest: null, legacy: false, reason: '快照不是目录', source: null, createdAt: null };
87
87
  }
88
88
  const metaFile = root + '.meta.json';
89
89
  if (fs.existsSync(metaFile)) {
@@ -91,13 +91,21 @@ function validateSnapshot(snapshot) {
91
91
  try {
92
92
  metadata = JSON.parse(fs.readFileSync(metaFile, 'utf8'));
93
93
  } catch (error) {
94
- return { ok: false, version: null, digest: null, legacy: false, reason: '快照元数据无法解析: ' + error.message };
94
+ return { ok: false, version: null, digest: null, legacy: false, reason: '快照元数据无法解析: ' + error.message, source: null, createdAt: null };
95
95
  }
96
96
  let current;
97
97
  try {
98
98
  current = computeTreeDigest(root);
99
99
  } catch (error) {
100
- return { ok: false, version: metadata.version || null, digest: metadata.digest || null, legacy: false, reason: '快照不可读: ' + error.message };
100
+ return {
101
+ ok: false,
102
+ version: metadata.version || null,
103
+ digest: metadata.digest || null,
104
+ legacy: false,
105
+ reason: '快照不可读: ' + error.message,
106
+ source: metadata.source || null,
107
+ createdAt: metadata.created_at || null,
108
+ };
101
109
  }
102
110
  if (!metadata.digest || current.digest !== metadata.digest) {
103
111
  return {
@@ -106,6 +114,8 @@ function validateSnapshot(snapshot) {
106
114
  digest: metadata.digest || null,
107
115
  legacy: false,
108
116
  reason: '快照摘要不一致,可能已被修改',
117
+ source: metadata.source || null,
118
+ createdAt: metadata.created_at || null,
109
119
  };
110
120
  }
111
121
  return {
@@ -114,14 +124,16 @@ function validateSnapshot(snapshot) {
114
124
  digest: metadata.digest,
115
125
  legacy: false,
116
126
  reason: null,
127
+ source: metadata.source || null,
128
+ createdAt: metadata.created_at || null,
117
129
  };
118
130
  }
119
131
 
120
132
  const version = readVersion(root);
121
133
  if (!fs.existsSync(path.join(root, 'SKILL.md'))) {
122
- return { ok: false, version, digest: null, legacy: true, reason: '旧快照缺少 SKILL.md' };
134
+ return { ok: false, version, digest: null, legacy: true, reason: '旧快照缺少 SKILL.md', source: null, createdAt: null };
123
135
  }
124
- return { ok: true, version, digest: null, legacy: true, reason: null };
136
+ return { ok: true, version, digest: null, legacy: true, reason: null, source: null, createdAt: null };
125
137
  }
126
138
 
127
139
  function createSnapshot(target, options) {
@@ -191,6 +203,8 @@ function listSnapshots(homeDir, slug) {
191
203
  version: validation.version,
192
204
  legacy: validation.legacy,
193
205
  reason: validation.reason,
206
+ source: validation.source || null,
207
+ createdAt: validation.createdAt || null,
194
208
  mtime_ms: mtimeMs,
195
209
  });
196
210
  }
@@ -170,7 +170,7 @@ function opencodeUserDir() {
170
170
  return agentDirs.resolveUserDir('.config/opencode/skills', { homeDir: os.homedir(), env: process.env });
171
171
  }
172
172
 
173
- /** OpenClaw / QClaw 用户级技能目录:$OPENCLAW_STATE_DIR/skills(默认 ~/.openclaw/skills)。 */
173
+ /** OpenClaw 用户级技能目录:$OPENCLAW_STATE_DIR/skills(默认 ~/.openclaw/skills)。 */
174
174
  function openclawUserDir() {
175
175
  return agentDirs.resolveUserDir('.openclaw/skills', { homeDir: os.homedir(), env: process.env });
176
176
  }
@@ -59,6 +59,22 @@ npx -y @yottameta/yotta-skills hub doctor
59
59
  - 官方锁文件:只读兼容 Vercel Labs `skills` CLI 的 `.skill-lock.json` v3
60
60
  (`$XDG_STATE_HOME/skills/` 或 `~/.agents/`)。
61
61
 
62
+ ## 本地面板(yotta-skills view)
63
+
64
+ ```bash
65
+ npx -y @yottameta/yotta-skills view
66
+ npx -y @yottameta/yotta-skills view --port 8790
67
+ ```
68
+
69
+ 面板只在 `127.0.0.1`(默认 8789)监听,零远程资源、零遥测,只读写 Hub
70
+ 目录与台账 / 证据文件,不读元忆、不改宿主全局配置。六个视图:概览、宿主矩阵、
71
+ 收编向导、链接与体检、记录与回滚、路由与编排,另附高级 CLI 页。
72
+
73
+ 动作边界:收编 / 链接 / 解除 / 回滚可执行(预览 → 确认 → 执行 → 证据);
74
+ install / update / refresh 只在高级 CLI 页给出可复制命令,不在网页执行。
75
+ 写操作需要页面会话令牌,解除与回滚需要破坏性确认串;收编始终逐项运行元信
76
+ 扫描,high / critical 阻断。
77
+
62
78
  ## 安全边界
63
79
 
64
80
  - `unlink` 只删除 lstat 确认为链接、且 readlink 目标位于 Hub 内的路径。
@@ -27,7 +27,7 @@ import subprocess
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
- VERSION = "0.25.1"
30
+ VERSION = "0.26.0"
31
31
  TOOL_NAME = "yotta-skills"
32
32
  CN_NAME = "元阁"
33
33
  MCP_PROTOCOL_MODERN = "2026-07-28"