@yottameta/yotta-skills-plugin 0.27.1 → 0.28.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.
@@ -12,7 +12,7 @@
12
12
  "authentication": "ON_INSTALL"
13
13
  },
14
14
  "category": "Developer Tools",
15
- "version": "0.27.1",
15
+ "version": "0.28.0",
16
16
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
17
17
  "interface": {
18
18
  "displayName": "元阁 yotta-skills",
@@ -12,7 +12,7 @@
12
12
  "authentication": "ON_INSTALL"
13
13
  },
14
14
  "category": "Developer Tools",
15
- "version": "0.27.1",
15
+ "version": "0.28.0",
16
16
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
17
17
  "interface": {
18
18
  "displayName": "元阁 yotta-skills",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-skills-plugin",
3
- "version": "0.27.1",
3
+ "version": "0.28.0",
4
4
  "description": "YuanGe (元阁) — orchestration and routing for the YottaMeta skill family, packaged as a YottaMeta Agent Plugin and DeepSeek Harness profile bundle.",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "yotta-skills",
4
- "version": "0.27.1",
4
+ "version": "0.28.0",
5
5
  "description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
6
6
  "author": {
7
7
  "name": "YottaMeta",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-skills
3
- version: 0.27.1
4
- description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 本机技能 Hub + 运行时 hook 适配。Hub 层:hub hosts 只读发现本机已装智能体与技能目录(文件系统优先,不读元忆);hub install / update 把技能装到 ~/.yottaskills/hub 单点真源(清单 27 + 特殊家族 5,特殊家族跟随各自 npm latest);hub link --all 用 Windows junction / POSIX symlink 分发到全部已发现宿主,元技能在链接时收敛宿主旧副本(移入 ~/.yottaskills/trash 保留 7 天、版本闸门 Hub ≥ 宿主);hub unlink 只删链接、fail-closed;非元技能不参与更新(用户自行处理);hub status 显示来源、版本、链接与异常;view 启动本机技能枢纽面板(默认 127.0.0.1:8789,六视图 + 高级 CLI 页,写操作需页面令牌与确认);兼容 agentskills.io 技能格式、Vercel Labs skills CLI 的 .agents/skills 通用目录与 .skill-lock.json v3(只读)。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);M1 记忆裁决层:usage enable/mark 本地结构化记录 + decide-memory / MCP decide_memory 输出 promote / hold / demote 只读建议(需授权 provider;只建议不删除、不自动写元忆);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 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/decide_memory,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / usage 使用记录 / decide-memory 记忆裁决 / hub 技能 Hub / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、单点安装并分发到多个智能体、查看本机装了哪些智能体与技能目录、接管非元技能、按场景组合多个元技能、路由或判断该用哪些技能、判断哪些技能值得长期记忆、查看或记录技能使用信号、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/记忆裁决/技能该不该记住/盘点技能/查看已装技能/技能 Hub/单点安装/链接分发/接管技能/技能枢纽面板/图形化面板 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + M1 记忆裁决只读建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + Hub 本机真源与链接分发 + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆、不自动删除技能或记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
3
+ version: 0.28.0
4
+ description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 本机技能 Hub + 运行时 hook 适配。Hub 层:hub hosts 只读发现本机已装智能体与技能目录(文件系统优先,不读元忆;按已核实 / 自动发现 / 桥接分类);hub install / update 把技能装到 ~/.yottaskills/hub 单点真源(清单 27 + 特殊家族 5,特殊家族跟随各自 npm latest);hub link --all 用 Windows junction / POSIX symlink 分发到全部已核实宿主(默认范围;--include-discovered 显式纳入自动发现目录;锁 / 数据桥接目录永不链接),元技能在链接时收敛宿主旧副本(移入 ~/.yottaskills/trash 保留 7 天、版本闸门 Hub ≥ 宿主);hub unlink 只删链接、fail-closed;非元技能不参与更新(用户自行处理);hub status 显示来源、版本、链接与异常;view 启动本机技能枢纽面板(默认 127.0.0.1:8789,六视图 + 高级 CLI 页,写操作需页面令牌与确认);兼容 agentskills.io 技能格式、Vercel Labs skills CLI 的 .agents/skills 通用目录与 .skill-lock.json v3(只读)。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);M1 记忆裁决层:usage enable/mark 本地结构化记录 + decide-memory / MCP decide_memory 输出 promote / hold / demote 只读建议(需授权 provider;只建议不删除、不自动写元忆);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 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/decide_memory,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / usage 使用记录 / decide-memory 记忆裁决 / hub 技能 Hub / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、单点安装并分发到多个智能体、查看本机装了哪些智能体与技能目录、接管非元技能、按场景组合多个元技能、路由或判断该用哪些技能、判断哪些技能值得长期记忆、查看或记录技能使用信号、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/记忆裁决/技能该不该记住/盘点技能/查看已装技能/技能 Hub/单点安装/链接分发/接管技能/技能枢纽面板/图形化面板 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + M1 记忆裁决只读建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + Hub 本机真源与链接分发 + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆、不自动删除技能或记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
5
5
  license: MIT
6
6
  metadata:
7
7
  zh_name: 元阁
@@ -178,9 +178,12 @@ npx -y @yottameta/yotta-skills hub install
178
178
  npx -y @yottameta/yotta-skills hub adopt --scan
179
179
  npx -y @yottameta/yotta-skills hub adopt --apply
180
180
 
181
- # 4) 链接分发到全部已发现宿主(Windows junction / macOS·Linux symlink)
181
+ # 4) 链接分发到全部已核实宿主(默认范围;Windows junction / macOS·Linux symlink)
182
182
  npx -y @yottameta/yotta-skills hub link --all
183
183
 
184
+ # 4b) 显式把自动发现目录一并纳入(默认不链;锁 / 数据桥接目录永不链接)
185
+ npx -y @yottameta/yotta-skills hub link --all --include-discovered
186
+
184
187
  # 5) 查看来源、版本、链接宿主与异常
185
188
  npx -y @yottameta/yotta-skills hub status
186
189
  npx -y @yottameta/yotta-skills hub doctor
@@ -192,8 +195,15 @@ npx -y @yottameta/yotta-skills view
192
195
  npx -y @yottameta/yotta-skills hub unlink --all
193
196
  ```
194
197
 
198
+ 链接范围(0.28.0 起):`hub hosts` 把目录分为已核实 / 自动发现 / 桥接三类 ——
199
+ **默认范围只含已核实宿主**(映射经公开宿主表 / 官方文档 / 本机自证核实);自动发现
200
+ 目录需 `--include-discovered` 显式纳入;`XDG_STATE_HOME/skills`(skills CLI
201
+ 锁目录)与 `XDG_DATA_HOME/skills` 是锁 / 数据桥接,永不作为链接目标。
202
+ `hub doctor` 的 `link_scope` 检查只读报告范围外链接。逐智能体核实表见
203
+ 发布方内部核实记录;本机可随时用 `hub hosts` 复算分类与范围。
204
+
195
205
  宿主发现兼容 `agentskills.io` 技能格式与 Vercel Labs `skills` CLI 的
196
- `.agents/skills` 通用目录;已收录 79 条宿主映射,并只读兼容
206
+ `.agents/skills` 通用目录;已收录 81 条宿主映射,并只读兼容
197
207
  `.skill-lock.json` v3(`$XDG_STATE_HOME/skills/` 或 `~/.agents/`)。
198
208
  不读元忆,不重写官方锁文件,不静默降级为复制。
199
209
 
@@ -217,16 +227,16 @@ npx -y @yottameta/yotta-skills hub unlink --all
217
227
  | `hook bind --host <name> --manifest <file>` | 幂等注册 hook 声明;`hook unbind <id>` 反注册 |
218
228
  | `--inventory` | 盘点本机已装技能:扫描技能目录生成/更新注册表(自包含,不依赖元技能);`--json` 输出 JSON、`--project` 附扫项目级目录 |
219
229
  | `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(install / update 完成后 CLI 自动重扫;也可在会话开工等时机手动运行);`--json` 输出 JSON、`--project` 附扫项目级目录;`--rescan` 同义 |
220
- | `hub hosts` | 发现本机已装智能体与技能目录(文件系统 + 环境变量;不读元忆 / 注册表);`--json` 输出 JSON |
230
+ | `hub hosts` | 发现本机已装智能体与技能目录(文件系统 + 环境变量;不读元忆 / 注册表);按已核实 / 自动发现 / 桥接分类;`--json` 输出 JSON |
221
231
  | `hub install [skill...]` | 把元技能安装到 Hub 真源(清单 27 + 特殊家族 5;特殊家族跟随各自 npm latest;默认 `~/.yottaskills/hub`;`--hub` / `YOTTA_SKILLS_HUB` 覆盖) |
222
232
  | `hub update` | 更新 Hub 内的元技能;非元技能不参与(来源无统一安装源,用户自行处理) |
223
233
  | `hub adopt --scan` | 只读预演:扫描各宿主现有技能,输出收编候选、多副本冲突、已在 Hub 状态 |
224
234
  | `hub adopt --apply [--include <a,b>] [--force] [--in-place]` | 把选中技能收编进 Hub;输出明示范围与目标 Hub;默认复制保真 + 原目录保留;非元阁来源标注「无更新源」并对严格 YAML 风险只读告警;`--skip-scan` 显式提示跳过装前扫描 |
225
235
  | `hub refresh <slug> --from <path>` | 手动同步非元阁技能(重新收编 + 更新哈希 + 台账) |
226
- | `hub link --agent <id> \| --dir <dir> \| --all` | 把 Hub 技能链接到指定宿主 / 目录 / 全部已发现宿主;元技能链接时收敛旧副本(回收站 7 天);`--dry-run` 只预览(含将收敛明细);`--force` 可替换指向 Hub 之外的链接(仅移除链接本身) |
227
- | `hub unlink --agent <id> \| --dir <dir> \| --all` | 只删除链接;lstat + readlink 校验目标在 Hub 内,fail-closed |
236
+ | `hub link --agent <id> \| --dir <dir> \| --all [--include-discovered]` | 把 Hub 技能链接到指定宿主 / 目录 / 全部已核实宿主(默认范围);`--include-discovered` 显式纳入自动发现目录;元技能链接时收敛旧副本(回收站 7 天);`--dry-run` 只预览(含将收敛明细);`--force` 可替换指向 Hub 之外的链接(仅移除链接本身) |
237
+ | `hub unlink --agent <id> \| --dir <dir> \| --all [--include-discovered]` | 只删除链接;lstat + readlink 校验目标在 Hub 内,fail-closed;清理范围外链接用 `--dir <目录>` 精确指定 |
228
238
  | `hub status [--json]` | 查看 Hub 技能来源、版本、链接宿主、异常链接与宿主发现摘要 |
229
- | `hub doctor [--json]` | 检查断链 / Hub 目标缺失 / slug 不一致 / 单一真源(只读报告多份副本与版本参差)/ 目录权限;异常时退出码 1 |
239
+ | `hub doctor [--json]` | 检查断链 / Hub 目标缺失 / slug 不一致 / 单一真源(只读报告多份副本与版本参差)/ 链接范围(`link_scope` 只读报告范围外链接)/ 目录权限;异常时退出码 1 |
230
240
  | `view [--port <n>]` | 启动本机技能枢纽面板(默认 `127.0.0.1:8789`,仅本机监听):六视图(概览 / 宿主矩阵 / 收编向导 / 链接与体检 / 记录与回滚 / 路由与编排)+ 高级 CLI 页;可执行收编 / 链接 / 解除 / 回滚,写操作需页面会话令牌与破坏性确认,install / update / refresh 只给命令 |
231
241
  | `--route <需求摘要>` | 编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;可选本地 provider 增补 / 重排并返回 `confidence` / `reasons` / `summary` / `alternatives`;`--json` 输出 JSON、`--project` 附扫项目级目录 |
232
242
  | `usage status` | 查看本地使用记录开关与计数;默认关闭,不创建文件 |
@@ -205,7 +205,7 @@
205
205
  <span class="label" id="hostSummary"></span>
206
206
  </div>
207
207
  <div class="section panel">
208
- <table><thead><tr><th>宿主</th><th>技能目录</th><th>目录技能</th><th>指向 Hub</th><th>异常</th><th>状态</th></tr></thead><tbody id="hostRows"></tbody></table>
208
+ <table><thead><tr><th>宿主</th><th>技能目录</th><th>范围</th><th>目录技能</th><th>指向 Hub</th><th>异常</th><th>状态</th></tr></thead><tbody id="hostRows"></tbody></table>
209
209
  <div class="empty" id="hostEmpty" hidden>没有匹配的宿主目录。</div>
210
210
  </div>
211
211
  <div class="section panel"><h3>已安装标记(无技能目录的宿主)</h3><div class="list" id="hostMarks"></div></div>
@@ -235,7 +235,7 @@
235
235
  <div id="linkDoctor"></div>
236
236
  <div class="section panel">
237
237
  <h3>分发目标</h3>
238
- <div class="faint">把 Hub 技能链接到宿主目录(Windows junction / POSIX symlink);元技能链接时收敛旧副本(移入回收站,保留 7 天可恢复);外部技能同名目录默认跳过。</div>
238
+ <div class="faint">把 Hub 技能链接到宿主目录(Windows junction / POSIX symlink);默认范围 = 已核实宿主,自动发现目录需在 CLI 用 --include-discovered 显式纳入,锁 / 数据桥接目录永不链接;元技能链接时收敛旧副本(移入回收站,保留 7 天可恢复)。</div>
239
239
  <div class="list" id="linkTargets"></div>
240
240
  </div>
241
241
  <div class="section panel">
@@ -442,7 +442,7 @@
442
442
  var d = await api('/api/hosts');
443
443
  if (d.error) { toast(d.error); return; }
444
444
  state.hosts = d;
445
- $('#hostSummary').textContent = '目录 ' + d.summary.existing + ' / ' + d.summary.hosts + ' · 已装标记 ' + d.summary.installedMarks + ' · 链接 ' + d.summary.links;
445
+ $('#hostSummary').textContent = '目录 ' + d.summary.existing + ' / ' + d.summary.hosts + '(已核实 ' + d.summary.verified + ' · 自动发现 ' + d.summary.discovered + ' · 桥接 ' + d.summary.bridge + ')· 已装标记 ' + d.summary.installedMarks + ' · 链接 ' + d.summary.links;
446
446
  renderHosts();
447
447
  var marks = d.installed || [];
448
448
  $('#hostMarks').innerHTML = marks.length ? marks.map(function (m) {
@@ -459,8 +459,10 @@
459
459
  $('#hostEmpty').hidden = rows.length > 0;
460
460
  $('#hostRows').innerHTML = rows.map(function (h) {
461
461
  var status = !h.exists ? '<span class="badge">未创建</span>' : (h.brokenLinks ? '<span class="badge danger">有异常</span>' : '<span class="badge ok">可用</span>');
462
+ var scopeBadge = h.bridgeOnly ? '<span class="badge info">桥接·不链</span>' : h.verified ? '<span class="badge ok">已核实</span>' : '<span class="badge warn">自动发现</span>';
462
463
  return '<tr><td><div class="cell-main">' + esc(h.label) + '</div><div class="cell-sub">' + esc(h.agentId || (h.known ? '已知宿主' : h.detection || '自动发现')) + '</div></td>' +
463
464
  '<td><div class="mono">' + esc(h.dir) + '</div><button class="btn small" data-copy="' + esc(h.dir) + '">复制</button></td>' +
465
+ '<td>' + scopeBadge + '</td>' +
464
466
  '<td>' + esc(h.skillCount || 0) + '</td><td>' + esc(h.hubLinks || 0) + '</td><td>' + esc(h.brokenLinks || 0) + '</td><td>' + status + '</td></tr>';
465
467
  }).join('');
466
468
  }
@@ -550,7 +552,9 @@
550
552
  $('#linkDoctor').innerHTML = doctorBanner(doctor);
551
553
  var existing = (hosts.hosts || []).filter(function (h) { return h.exists; });
552
554
  $('#linkTargets').innerHTML = existing.length ? existing.map(function (h) {
553
- return '<div class="row"><div class="row-main"><div class="row-title">' + esc(h.label) + '</div><div class="row-meta mono">' + esc(h.dir) + '</div><div class="row-meta">目录技能 ' + esc(h.skillCount || 0) + ' · 指向 Hub ' + esc(h.hubLinks || 0) + (h.brokenLinks ? ' · 异常 ' + h.brokenLinks : '') + '</div></div><div class="row-actions"><button class="btn small" data-link-plan="' + esc(h.dir) + '" data-label="' + esc(h.label) + '">预览链接</button></div></div>';
555
+ var scopeBadge = h.bridgeOnly ? '<span class="badge info">桥接·不链接</span>' : h.verified ? '<span class="badge ok">已核实</span>' : '<span class="badge warn">自动发现</span>';
556
+ var action = h.bridgeOnly ? '' : '<button class="btn small" data-link-plan="' + esc(h.dir) + '" data-label="' + esc(h.label) + '">预览链接</button>';
557
+ return '<div class="row"><div class="row-main"><div class="row-title">' + esc(h.label) + ' ' + scopeBadge + '</div><div class="row-meta mono">' + esc(h.dir) + '</div><div class="row-meta">目录技能 ' + esc(h.skillCount || 0) + ' · 指向 Hub ' + esc(h.hubLinks || 0) + (h.brokenLinks ? ' · 异常 ' + h.brokenLinks : '') + '</div></div><div class="row-actions">' + action + '</div></div>';
554
558
  }).join('') : '<div class="empty">尚未发现可用的宿主技能目录。</div>';
555
559
  var rows = links.links || [];
556
560
  $('#linkEmpty').hidden = rows.length > 0;
@@ -698,7 +702,8 @@
698
702
  { cmd: 'yotta-skills hub adopt --apply', why: '执行收编(默认复制保真)' },
699
703
  ] },
700
704
  { name: '链接与体检', items: [
701
- { cmd: 'yotta-skills hub link --all', why: '把 Hub 技能链接到全部宿主' },
705
+ { cmd: 'yotta-skills hub link --all', why: '链接到已核实宿主(默认范围)' },
706
+ { cmd: 'yotta-skills hub link --all --include-discovered', why: '显式纳入自动发现目录' },
702
707
  { cmd: 'yotta-skills hub link --agent codex', why: '只链接到一个宿主' },
703
708
  { cmd: 'yotta-skills hub unlink --all', why: '只删链接,不动 Hub 真源' },
704
709
  { cmd: 'yotta-skills hub status', why: '查看技能 / 来源 / 链接' },
@@ -55,11 +55,11 @@ const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
55
55
  const { COPY_SKIP, copyDir } = require('../lib/copy-tree');
56
56
 
57
57
  const PKG_ROOT = path.join(__dirname, '..');
58
- let VERSION = '0.27.1';
58
+ let VERSION = '0.28.0';
59
59
  try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
60
60
 
61
61
  // @generated view-html:start
62
- const VIEW_HTML = "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow\">\n<meta name=\"yotta-view-token\" content=\"__YOTTA_VIEW_TOKEN__\">\n<link rel=\"icon\" href=\"data:,\">\n<title>元阁 · 技能枢纽</title>\n<style>\n :root{--bg:#0d1114;--bg2:#12171b;--panel:#171d22;--panel2:#1d242a;--line:rgba(148,190,170,.14);--line2:rgba(148,190,170,.28);\n --text:#e9efe9;--muted:#9aa8a0;--faint:#6d7b74;--acc:#5fbf8f;--acc-ink:#0d1a13;--warn:#e0b263;--danger:#e07a7a;--info:#8fb4de;\n --shadow:0 10px 28px rgba(0,0,0,.35)}\n body.light{--bg:#f2f5f1;--bg2:#e9efea;--panel:#ffffff;--panel2:#f6f9f5;--line:rgba(24,58,44,.12);--line2:rgba(24,58,44,.24);\n --text:#17211c;--muted:#5c6b63;--faint:#86958c;--acc:#2f7d59;--acc-ink:#ffffff;--warn:#9a6a14;--danger:#b23c3c;--info:#3c6ea5;\n --shadow:0 8px 22px rgba(24,58,44,.08)}\n *{box-sizing:border-box}[hidden]{display:none!important}\n html,body{margin:0;padding:0}\n body{background:var(--bg);color:var(--text);font:14px/1.6 \"Microsoft YaHei\",\"PingFang SC\",\"Noto Sans CJK SC\",system-ui,sans-serif;letter-spacing:0}\n button{font:inherit;color:inherit;cursor:pointer}\n input,textarea{font:inherit;color:var(--text);background:var(--panel2);border:1px solid var(--line2);border-radius:6px;padding:8px 11px;outline:0;width:100%}\n input:focus,textarea:focus{border-color:var(--acc)}\n textarea{min-height:76px;resize:vertical}\n code,kbd{font-family:ui-monospace,Consolas,\"Courier New\",monospace;font-size:12px}\n .sv{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}\n\n .shell{display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh}\n .sidebar{background:var(--bg2);border-right:1px solid var(--line);padding:20px 14px;display:flex;flex-direction:column;gap:20px;position:sticky;top:0;height:100vh;min-width:0}\n .brand{display:flex;align-items:center;gap:10px;padding:0 6px}\n .brand-mark{width:34px;height:34px;border-radius:8px;background:rgba(95,191,143,.14);border:1px solid var(--line2);display:grid;place-items:center;color:var(--acc)}\n .brand-name{font-size:15px;font-weight:600}\n .brand-sub{font-size:11px;color:var(--faint)}\n .nav{display:flex;flex-direction:column;gap:3px;min-width:0}\n .nav button{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);text-align:left}\n .nav button:hover{background:var(--panel);color:var(--text)}\n .nav button.active{background:var(--panel);color:var(--acc);border-color:var(--line)}\n .side-foot{margin-top:auto;display:flex;flex-direction:column;gap:8px;min-width:0}\n .privacy{display:flex;gap:8px;align-items:flex-start;padding:10px;border:1px solid var(--line);border-radius:8px;color:var(--muted);font-size:12px;background:var(--panel)}\n .privacy .sv{color:var(--acc);margin-top:2px}\n .ghost-btn{display:flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:6px;padding:8px}\n .ghost-btn:hover{color:var(--text);border-color:var(--line2)}\n .verline{padding:0 6px;color:var(--faint);font-size:11px}\n\n .main{min-width:0;padding:24px 28px 70px}\n .topbar{display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}\n .page-title{font-size:19px;font-weight:600;margin:0}\n .page-sub{color:var(--faint);font-size:12px;margin-top:2px}\n .spacer{flex:1}\n .chip{display:flex;align-items:center;gap:7px;padding:5px 10px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font-size:12px;white-space:nowrap}\n .dot{width:7px;height:7px;border-radius:50%;background:var(--acc)}\n\n .view{display:none}.view.active{display:block}\n .grid{display:grid;gap:12px}.g4{grid-template-columns:repeat(4,minmax(0,1fr))}.g2{grid-template-columns:repeat(2,minmax(0,1fr))}\n .panel{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:15px;min-width:0}\n .panel h3{margin:0 0 4px;font-size:14px}\n .section{margin-top:16px}\n .shead{display:flex;align-items:baseline;gap:10px;margin-bottom:9px;flex-wrap:wrap}\n .shead h2{font-size:14px;margin:0}\n .label{color:var(--muted);font-size:12px}\n .value{font-size:25px;font-weight:600;margin:7px 0 2px}\n .delta{font-size:12px;color:var(--muted)}\n .mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all}\n\n .toolbar{display:flex;gap:9px;align-items:center;flex-wrap:wrap}\n .search{display:flex;align-items:center;gap:8px;min-width:230px;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:7px 11px;color:var(--muted)}\n .search input{background:transparent;border:0;padding:0;width:100%}\n .btn{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border-radius:6px;border:1px solid var(--line2);background:transparent;color:var(--text);white-space:nowrap}\n .btn:hover{border-color:var(--acc)}\n .btn.primary{background:var(--acc);border-color:var(--acc);color:var(--acc-ink);font-weight:600}\n .btn.danger{border-color:rgba(224,122,122,.5);color:var(--danger)}\n .btn.small{padding:4px 9px;font-size:12px}\n .btn[disabled]{opacity:.45;cursor:not-allowed}\n .ibtn{width:29px;height:29px;display:grid;place-items:center;border:1px solid var(--line);background:transparent;border-radius:6px;color:var(--muted)}\n .ibtn:hover{color:var(--text);border-color:var(--line2)}\n\n table{width:100%;border-collapse:collapse}\n th,td{text-align:left;padding:10px 9px;border-bottom:1px solid var(--line);font-size:13px;vertical-align:top}\n th{color:var(--faint);font-weight:500;font-size:12px}\n tbody tr:hover{background:var(--panel2)}\n .cell-main{font-weight:600}\n .cell-sub{color:var(--faint);font-size:11px;word-break:break-all}\n\n .list{display:flex;flex-direction:column;gap:7px;margin-top:12px}\n .row{display:flex;align-items:center;gap:11px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);min-width:0}\n .row-main{flex:1;min-width:0}\n .row-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .row-meta{color:var(--faint);font-size:11px;margin-top:2px;word-break:break-all}\n .row-actions{display:flex;gap:6px;flex-wrap:wrap}\n .empty{color:var(--faint);text-align:center;padding:22px;border:1px dashed var(--line);border-radius:8px;margin-top:12px}\n\n .badge{display:inline-block;font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--line2);color:var(--muted);background:transparent;white-space:nowrap}\n .badge.ok{color:var(--acc);border-color:rgba(95,191,143,.45);background:rgba(95,191,143,.10)}\n .badge.warn{color:var(--warn);border-color:rgba(224,178,99,.45);background:rgba(224,178,99,.10)}\n .badge.danger{color:var(--danger);border-color:rgba(224,122,122,.45);background:rgba(224,122,122,.10)}\n .badge.info{color:var(--info);border-color:rgba(143,180,222,.45);background:rgba(143,180,222,.10)}\n .dot-mark{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;background:var(--faint)}\n .dot-mark.ok{background:var(--acc)}.dot-mark.warn{background:var(--warn)}.dot-mark.danger{background:var(--danger)}\n\n .banner{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid var(--line2);border-radius:8px;background:var(--panel2)}\n .banner.ok{border-color:rgba(95,191,143,.45)}\n .banner.danger{border-color:rgba(224,122,122,.45)}\n .banner .sv{margin-top:2px;color:var(--acc)}\n .banner.danger .sv{color:var(--danger)}\n .banner-title{font-weight:600}\n .banner-body{color:var(--muted);font-size:12px;margin-top:2px}\n\n .kv{display:grid;grid-template-columns:96px 1fr;gap:5px 10px;font-size:13px;margin:10px 0}\n .kv dt{color:var(--faint)}.kv dd{margin:0;word-break:break-all}\n .variants{border-top:1px dashed var(--line);margin-top:8px;padding-top:8px;display:flex;flex-direction:column;gap:5px}\n .variant{display:flex;gap:9px;align-items:baseline;font-size:12px;color:var(--muted);flex-wrap:wrap}\n .check{width:16px;height:16px;flex:none;accent-color:var(--acc)}\n details summary{cursor:pointer;color:var(--muted);font-size:12px}\n\n .actionbar{position:sticky;bottom:0;margin-top:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:11px 13px;border:1px solid var(--line2);border-radius:8px;background:var(--bg2);box-shadow:var(--shadow)}\n .result-list{display:flex;flex-direction:column;gap:6px;margin-top:10px}\n .result{display:flex;gap:9px;align-items:baseline;flex-wrap:wrap;padding:8px 10px;border:1px solid var(--line);border-radius:6px;font-size:12px}\n\n .mask{position:fixed;inset:0;background:rgba(0,0,0,.52);display:none;place-items:center;z-index:50;padding:18px}\n .mask.open{display:grid}\n .modal{width:min(600px,100%);max-height:86vh;overflow:auto;background:var(--panel);border:1px solid var(--line2);border-radius:8px;padding:18px;box-shadow:var(--shadow)}\n .modal h3{margin:0 0 10px;font-size:16px}\n .modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}\n\n .toasts{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);display:flex;flex-direction:column;gap:7px;align-items:center;z-index:60}\n .toast{background:var(--panel2);border:1px solid var(--line2);border-radius:999px;padding:8px 15px;box-shadow:var(--shadow);font-size:13px}\n\n .cmd-group{margin-top:14px}\n .cmd{display:flex;gap:10px;align-items:center;padding:9px 11px;border:1px solid var(--line);border-radius:6px;background:var(--panel2);margin-top:6px}\n .cmd code{flex:1;min-width:0;word-break:break-all;color:var(--text)}\n .cmd .why{color:var(--faint);font-size:11px}\n .note{color:var(--muted);font-size:12px;margin-top:8px}\n\n @media (max-width:980px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}}\n @media (max-width:760px){\n .shell{grid-template-columns:minmax(0,1fr)}\n .sidebar{position:static;height:auto;padding:13px;gap:11px}\n .nav{flex-direction:row;overflow-x:auto;width:100%;scrollbar-width:none;-webkit-overflow-scrolling:touch}\n .nav::-webkit-scrollbar{display:none}\n .nav button{white-space:nowrap;padding:7px 9px}\n .side-foot{margin-top:0;flex-direction:row;align-items:center}\n .privacy{display:none}\n .ghost-btn{flex:1;white-space:nowrap}\n .verline{white-space:nowrap;text-align:right}\n .main{padding:16px 14px 56px}\n .g4,.g2{grid-template-columns:minmax(0,1fr)}\n .search{min-width:100%}\n table{display:block;overflow-x:auto;white-space:nowrap}\n .row{flex-wrap:wrap}\n }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <aside class=\"sidebar\">\n <div class=\"brand\">\n <div class=\"brand-mark\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 3 4 7v4c0 4.4 3.4 8.4 8 10 4.6-1.6 8-5.6 8-10V7z\"/><path d=\"M9 12l2 2 4-4\"/></svg></div>\n <div><div class=\"brand-name\">元阁 · 技能枢纽</div><div class=\"brand-sub\">Hub 本地面板</div></div>\n </div>\n <nav class=\"nav\" id=\"nav\">\n <button data-view=\"overview\" class=\"active\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 10.5 12 3l9 7.5\"/><path d=\"M5 9.5V21h14V9.5\"/></svg>概览</button>\n <button data-view=\"hosts\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"7\" rx=\"2\"/><rect x=\"3\" y=\"13\" width=\"18\" height=\"7\" rx=\"2\"/><path d=\"M7 7.5h.01M7 16.5h.01\"/></svg>宿主矩阵</button>\n <button data-view=\"adopt\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 3v12\"/><path d=\"m7 10 5 5 5-5\"/><path d=\"M4 21h16\"/></svg>收编向导</button>\n <button data-view=\"links\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M10 13a5 5 0 0 0 7.1 0l2-2a5 5 0 0 0-7.1-7.1L11 4.9\"/><path d=\"M14 11a5 5 0 0 0-7.1 0l-2 2A5 5 0 0 0 12 20.1l1-1\"/></svg>链接与体检</button>\n <button data-view=\"records\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/><path d=\"M12 8v5l3 2\"/></svg>记录与回滚</button>\n <button data-view=\"route\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"6\" cy=\"6\" r=\"2.5\"/><circle cx=\"18\" cy=\"18\" r=\"2.5\"/><path d=\"M8.5 6H15a3 3 0 0 1 0 6h-6a3 3 0 0 0 0 6h6.5\"/></svg>路由与编排</button>\n <button data-view=\"cli\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"m4 7 5 5-5 5\"/><path d=\"M12 19h8\"/></svg>高级 CLI</button>\n </nav>\n <div class=\"side-foot\">\n <div class=\"privacy\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/><path d=\"m9 12 2 2 4-4\"/></svg><span>本机运行 · 零远程<br>写操作需页面令牌与确认</span></div>\n <button class=\"ghost-btn\" id=\"themeBtn\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/></svg>切换主题</button>\n <div class=\"verline\" id=\"verLine\"></div>\n </div>\n </aside>\n\n <main class=\"main\">\n <div class=\"topbar\">\n <div><h1 class=\"page-title\" id=\"pageTitle\">概览</h1><div class=\"page-sub\" id=\"pageSub\">Hub 里有什么、健康吗、最近做了什么</div></div>\n <div class=\"spacer\"></div>\n <button class=\"btn\" id=\"refreshBtn\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/></svg>刷新</button>\n <div class=\"chip\"><span class=\"dot\"></span>本机 · 127.0.0.1</div>\n </div>\n\n <section class=\"view active\" id=\"view-overview\">\n <div class=\"grid g4\" id=\"ovCards\"></div>\n <div class=\"section\" id=\"ovDoctor\"></div>\n <div class=\"section grid g2\">\n <div class=\"panel\"><h3>Hub 内容</h3><div class=\"faint\">技能真源在 Hub 目录,宿主目录通过链接分发。</div><div class=\"list\" id=\"ovSkills\"></div></div>\n <div class=\"panel\"><h3>最近动作</h3><div class=\"faint\">收编 / 链接 / 解除都写入 Hub 审计。</div><div class=\"list\" id=\"ovAudit\"></div></div>\n </div>\n <div class=\"section panel\">\n <h3>Hub 位置</h3>\n <div class=\"mono\" id=\"ovHubDir\">-</div>\n <div class=\"toolbar section\">\n <button class=\"btn small\" data-goto=\"adopt\">去收编</button>\n <button class=\"btn small\" data-goto=\"links\">去链接与体检</button>\n <button class=\"btn small\" data-goto=\"records\">查看记录</button>\n <button class=\"btn small\" id=\"copyHubDir\">复制路径</button>\n </div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-hosts\">\n <div class=\"toolbar\">\n <label class=\"search\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"hostSearch\" placeholder=\"过滤宿主 / 目录\"></label>\n <span class=\"label\" id=\"hostSummary\"></span>\n </div>\n <div class=\"section panel\">\n <table><thead><tr><th>宿主</th><th>技能目录</th><th>目录技能</th><th>指向 Hub</th><th>异常</th><th>状态</th></tr></thead><tbody id=\"hostRows\"></tbody></table>\n <div class=\"empty\" id=\"hostEmpty\" hidden>没有匹配的宿主目录。</div>\n </div>\n <div class=\"section panel\"><h3>已安装标记(无技能目录的宿主)</h3><div class=\"list\" id=\"hostMarks\"></div></div>\n </section>\n\n <section class=\"view\" id=\"view-adopt\">\n <div class=\"grid g4\" id=\"adoptCards\"></div>\n <div class=\"section toolbar\">\n <label class=\"search\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"adoptSearch\" placeholder=\"过滤技能 slug / 来源宿主\"></label>\n <button class=\"btn small\" id=\"adoptAll\">全选可收编</button>\n <button class=\"btn small\" id=\"adoptNone\">清空</button>\n <span class=\"spacer\"></span>\n <span class=\"label\" id=\"adoptScanInfo\"></span>\n </div>\n <div class=\"list\" id=\"adoptList\"></div>\n <div class=\"empty\" id=\"adoptEmpty\" hidden>没有可收编的候选。先在宿主里安装技能,或刷新宿主矩阵。</div>\n <div class=\"actionbar\">\n <span class=\"label\">已选 <b id=\"adoptCount\">0</b> 项</span>\n <span class=\"spacer\"></span>\n <span class=\"faint\">默认复制保真、原目录保留;冲突项默认跳过;执行时逐项运行元信扫描。</span>\n <button class=\"btn primary\" id=\"adoptApply\">收编所选</button>\n </div>\n <div class=\"section panel\" id=\"adoptResultsWrap\" hidden><h3>收编结果</h3><div class=\"result-list\" id=\"adoptResults\"></div></div>\n </section>\n\n <section class=\"view\" id=\"view-links\">\n <div id=\"linkDoctor\"></div>\n <div class=\"section panel\">\n <h3>分发目标</h3>\n <div class=\"faint\">把 Hub 技能链接到宿主目录(Windows junction / POSIX symlink);元技能链接时收敛旧副本(移入回收站,保留 7 天可恢复);外部技能同名目录默认跳过。</div>\n <div class=\"list\" id=\"linkTargets\"></div>\n </div>\n <div class=\"section panel\">\n <h3>当前链接</h3>\n <table><thead><tr><th>技能</th><th>宿主</th><th>目标</th><th>状态</th><th></th></tr></thead><tbody id=\"linkRows\"></tbody></table>\n <div class=\"empty\" id=\"linkEmpty\" hidden>还没有任何链接。</div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-records\">\n <div class=\"toolbar\">\n <button class=\"btn small\" data-tab=\"snapshots\">回滚快照</button>\n <button class=\"btn small\" data-tab=\"evidence\">安装 / 回滚证据</button>\n <button class=\"btn small\" data-tab=\"audit\">Hub 审计</button>\n <span class=\"spacer\"></span>\n <span class=\"label\" id=\"recordsInfo\"></span>\n </div>\n <div class=\"section panel\" id=\"recSnapshots\">\n <table><thead><tr><th>技能</th><th>版本</th><th>时间</th><th>来源目录</th><th>状态</th><th></th></tr></thead><tbody id=\"snapshotRows\"></tbody></table>\n <div class=\"empty\" id=\"snapshotEmpty\" hidden>没有安装快照。快照由 install / update 写入。</div>\n </div>\n <div class=\"section panel\" id=\"recEvidence\" hidden>\n <div class=\"list\" id=\"evidenceRows\"></div>\n <div class=\"empty\" id=\"evidenceEmpty\" hidden>没有证据记录。</div>\n </div>\n <div class=\"section panel\" id=\"recAudit\" hidden>\n <div class=\"list\" id=\"auditRows\"></div>\n <div class=\"empty\" id=\"auditEmpty\" hidden>没有 Hub 审计记录。</div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-route\">\n <div class=\"panel\">\n <h3>一句话需求 → 技能组合</h3>\n <div class=\"faint\">只读调用静态 playbook(7 组)+ 动态路由块;安装建议只给命令,不在网页执行。</div>\n <div class=\"toolbar section\">\n <label class=\"search\" style=\"flex:1\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"routeInput\" placeholder=\"例如:帮我做发布前质量检查\"></label>\n <button class=\"btn primary\" id=\"routeBtn\">给出组合</button>\n </div>\n </div>\n <div id=\"routeResult\" class=\"section\"></div>\n </section>\n\n <section class=\"view\" id=\"view-cli\">\n <div class=\"banner\">\n <svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"m4 7 5 5-5 5\"/><path d=\"M12 19h8\"/></svg>\n <div><div class=\"banner-title\">高级 CLI</div><div class=\"banner-body\">低频 / 长尾命令在这里复制到终端执行;网页不代替终端运行安装与更新。</div></div>\n </div>\n <div id=\"cliGroups\"></div>\n </section>\n </main>\n</div>\n\n<div class=\"mask\" id=\"mask\"><div class=\"modal\" id=\"modal\"></div></div>\n<div class=\"toasts\" id=\"toasts\"></div>\n\n<script>\n(function () {\n var TOKEN = (document.querySelector('meta[name=\"yotta-view-token\"]') || {}).content || '';\n var state = { view: 'overview', overview: null, hosts: null, adopt: null, selected: {}, records: null, tab: 'snapshots', hostFilter: '', adoptFilter: '' };\n\n function $(sel) { return document.querySelector(sel); }\n function esc(value) {\n return String(value === null || value === undefined ? '' : value)\n .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;').replace(/'/g, '&#39;');\n }\n function fmtTime(value) {\n var ms = typeof value === 'number' ? value : Date.parse(value);\n if (!ms || isNaN(ms)) return value ? String(value) : '-';\n var d = new Date(ms);\n function p(n) { return n < 10 ? '0' + n : '' + n; }\n return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());\n }\n function toast(text) {\n var el = document.createElement('div');\n el.className = 'toast';\n el.textContent = text;\n $('#toasts').appendChild(el);\n setTimeout(function () { el.remove(); }, 3200);\n }\n function copyText(text) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(function () { toast('已复制'); }, function () { fallbackCopy(text); });\n } else fallbackCopy(text);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text; document.body.appendChild(ta); ta.select();\n try { document.execCommand('copy'); toast('已复制'); } catch (e) { toast('复制失败,请手动选择'); }\n ta.remove();\n }\n async function api(path, opts) {\n opts = opts || {};\n var init = { method: opts.method || 'GET', headers: {} };\n if (opts.body !== undefined) {\n init.method = 'POST';\n init.headers['Content-Type'] = 'application/json';\n init.headers['X-Yotta-View-Token'] = TOKEN;\n init.body = JSON.stringify(opts.body);\n }\n var res, data = null;\n try { res = await fetch(path, init); } catch (e) { return { error: '无法连接本地服务:' + e.message }; }\n try { data = await res.json(); } catch (e) { data = null; }\n if (!res.ok) return data && data.error ? data : { error: '请求失败(HTTP ' + res.status + ')' };\n if (!data) return { error: '响应不是合法 JSON' };\n return data;\n }\n\n function openModal(title, bodyHtml, opts) {\n opts = opts || {};\n var html = '<h3>' + esc(title) + '</h3><div id=\"modalBody\">' + bodyHtml + '</div>' +\n '<div class=\"modal-actions\"><button class=\"btn\" data-modal=\"cancel\">' + esc(opts.cancelText || '取消') + '</button>' +\n (opts.okText === null ? '' : '<button class=\"btn ' + (opts.danger ? 'danger' : 'primary') + '\" data-modal=\"ok\">' + esc(opts.okText || '确认') + '</button>') +\n '</div>';\n $('#modal').innerHTML = html;\n $('#mask').classList.add('open');\n var ok = $('#modal').querySelector('[data-modal=\"ok\"]');\n $('#modal').querySelector('[data-modal=\"cancel\"]').onclick = closeModal;\n if (ok && opts.onOk) {\n ok.onclick = async function () {\n ok.disabled = true;\n try { await opts.onOk(); } finally { ok.disabled = false; }\n };\n }\n }\n function closeModal() { $('#mask').classList.remove('open'); $('#modal').innerHTML = ''; }\n function confirmModal(title, bodyHtml, okText, danger, onOk) {\n openModal(title, bodyHtml, { okText: okText, danger: danger, onOk: onOk });\n }\n $('#mask').addEventListener('click', function (e) { if (e.target === $('#mask')) closeModal(); });\n document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeModal(); });\n\n var VIEWS = {\n overview: { title: '概览', sub: 'Hub 里有什么、健康吗、最近做了什么', load: loadOverview },\n hosts: { title: '宿主矩阵', sub: '装了哪些宿主、技能目录在哪、Hub 技能链到了谁', load: loadHosts },\n adopt: { title: '收编向导', sub: '有哪些可收编、哪些是重复副本、收编后会发生什么', load: loadAdopt },\n links: { title: '链接与体检', sub: '断链 / 漂移 / 目标缺失在哪、怎么修', load: loadLinks },\n records: { title: '记录与回滚', sub: '安装、收编、链接都做了什么,怎么退回', load: loadRecords },\n route: { title: '路由与编排', sub: '一句话需求该用哪些技能、缺什么(只读)', load: null },\n cli: { title: '高级 CLI', sub: '低频命令速查与复制', load: null }\n };\n function switchView(name) {\n if (!VIEWS[name]) return;\n state.view = name;\n Array.prototype.forEach.call(document.querySelectorAll('#nav button'), function (b) { b.classList.toggle('active', b.dataset.view === name); });\n Array.prototype.forEach.call(document.querySelectorAll('.view'), function (v) { v.classList.toggle('active', v.id === 'view-' + name); });\n $('#pageTitle').textContent = VIEWS[name].title;\n $('#pageSub').textContent = VIEWS[name].sub;\n if (VIEWS[name].load) VIEWS[name].load();\n }\n $('#nav').addEventListener('click', function (e) {\n var b = e.target.closest('button[data-view]');\n if (b) switchView(b.dataset.view);\n });\n document.addEventListener('click', function (e) {\n var g = e.target.closest('[data-goto]');\n if (g) switchView(g.dataset.goto);\n });\n $('#refreshBtn').onclick = function () { var v = VIEWS[state.view]; if (v && v.load) v.load(); else toast('已刷新'); };\n $('#themeBtn').onclick = function () {\n document.body.classList.toggle('light');\n try { localStorage.setItem('yotta-hub-theme', document.body.classList.contains('light') ? 'light' : 'dark'); } catch (e) {}\n };\n try { if (localStorage.getItem('yotta-hub-theme') === 'light') document.body.classList.add('light'); } catch (e) {}\n\n function statCard(label, value, delta, tone) {\n return '<div class=\"panel\"><div class=\"label\">' + esc(label) + '</div><div class=\"value\">' + esc(value) + '</div><div class=\"delta ' + (tone ? 'badge ' + tone : '') + '\">' + esc(delta) + '</div></div>';\n }\n function doctorBanner(doctor) {\n if (!doctor) return '';\n if (doctor.ok) {\n return '<div class=\"banner ok\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/><path d=\"m9 12 2 2 4-4\"/></svg><div><div class=\"banner-title\">体检通过</div><div class=\"banner-body\">警告 ' + doctor.summary.warnings + ' / 信息 ' + doctor.summary.info + ';结构性错误 0。</div></div></div>';\n }\n var bad = (doctor.checks || []).filter(function (c) { return !c.ok; }).slice(0, 6).map(function (c) {\n return '<div class=\"result\"><span class=\"badge ' + (c.severity === 'error' ? 'danger' : 'warn') + '\">' + esc(c.severity === 'error' ? '错误' : '警告') + '</span><span>' + esc(c.message) + '</span>' + (c.hint ? '<span class=\"faint\">修复:' + esc(c.hint) + '</span>' : '') + '</div>';\n }).join('');\n return '<div class=\"banner danger\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/><path d=\"M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z\"/></svg><div style=\"flex:1\"><div class=\"banner-title\">体检未通过(错误 ' + doctor.summary.errors + ' / 警告 ' + doctor.summary.warnings + ')</div><div class=\"result-list\">' + bad + '</div></div></div>';\n }\n\n async function loadOverview() {\n var d = await api('/api/overview');\n if (d.error) { toast(d.error); return; }\n state.overview = d;\n $('#ovCards').innerHTML =\n statCard('Hub 技能', d.status.summary.skills, '外部来源 ' + d.status.summary.external, '') +\n statCard('已链接', d.status.summary.links, '宿主目录投影', 'ok') +\n statCard('异常链接', d.status.summary.brokenLinks, d.status.summary.brokenLinks ? '需要处理' : '全部正常', d.status.summary.brokenLinks ? 'danger' : 'ok') +\n statCard('Doctor', d.doctor.ok ? '通过' : '未通过', '错误 ' + d.doctor.summary.errors + ' / 警告 ' + d.doctor.summary.warnings, d.doctor.ok ? 'ok' : 'danger');\n $('#ovDoctor').innerHTML = doctorBanner(d.doctor);\n var skills = d.status.skills || [];\n $('#ovSkills').innerHTML = skills.length ? skills.slice(0, 8).map(function (s) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(s.slug) + '</div><div class=\"row-meta\">v' + esc(s.version || '-') + ' · ' + esc(s.origin === 'external' ? '外部来源' : '元阁家族') + ' · 链接 ' + (s.links || []).length + '</div></div><span class=\"badge ' + (s.status === 'present' ? 'ok' : 'danger') + '\">' + esc(s.status === 'present' ? '已入库' : '缺失') + '</span></div>';\n }).join('') : '<div class=\"empty\">Hub 还是空的。先在「收编向导」把已装技能收编进来。</div>';\n var audit = (d.audit && d.audit.recent) || [];\n $('#ovAudit').innerHTML = audit.length ? audit.slice().reverse().map(function (a) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(a.event || '动作') + ' · ' + esc(a.slug || '') + '</div><div class=\"row-meta\">' + esc(fmtTime(a.at)) + (a.verdict ? ' · ' + esc(a.verdict) : '') + '</div></div></div>';\n }).join('') : '<div class=\"empty\">还没有动作记录。</div>';\n $('#ovHubDir').textContent = d.hubDir;\n $('#copyHubDir').onclick = function () { copyText(d.hubDir); };\n }\n\n async function loadHosts() {\n var d = await api('/api/hosts');\n if (d.error) { toast(d.error); return; }\n state.hosts = d;\n $('#hostSummary').textContent = '目录 ' + d.summary.existing + ' / ' + d.summary.hosts + ' · 已装标记 ' + d.summary.installedMarks + ' · 链接 ' + d.summary.links;\n renderHosts();\n var marks = d.installed || [];\n $('#hostMarks').innerHTML = marks.length ? marks.map(function (m) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(m.label) + '</div><div class=\"row-meta\">' + esc(m.source || '') + (m.hasSkillsDir ? ' · 技能目录:' + esc(m.skillDir || '') : ' · 未发现技能目录') + '</div></div><span class=\"badge ' + (m.hasSkillsDir ? 'ok' : 'info') + '\">' + esc(m.hasSkillsDir ? '可链接' : '仅标记') + '</span></div>';\n }).join('') : '<div class=\"empty\">没有检测到额外宿主标记。</div>';\n }\n function renderHosts() {\n var d = state.hosts; if (!d) return;\n var q = state.hostFilter.toLowerCase();\n var rows = (d.hosts || []).filter(function (h) {\n if (!q) return true;\n return String(h.label || '').toLowerCase().includes(q) || String(h.dir || '').toLowerCase().includes(q);\n });\n $('#hostEmpty').hidden = rows.length > 0;\n $('#hostRows').innerHTML = rows.map(function (h) {\n var status = !h.exists ? '<span class=\"badge\">未创建</span>' : (h.brokenLinks ? '<span class=\"badge danger\">有异常</span>' : '<span class=\"badge ok\">可用</span>');\n return '<tr><td><div class=\"cell-main\">' + esc(h.label) + '</div><div class=\"cell-sub\">' + esc(h.agentId || (h.known ? '已知宿主' : h.detection || '自动发现')) + '</div></td>' +\n '<td><div class=\"mono\">' + esc(h.dir) + '</div><button class=\"btn small\" data-copy=\"' + esc(h.dir) + '\">复制</button></td>' +\n '<td>' + esc(h.skillCount || 0) + '</td><td>' + esc(h.hubLinks || 0) + '</td><td>' + esc(h.brokenLinks || 0) + '</td><td>' + status + '</td></tr>';\n }).join('');\n }\n $('#hostSearch').addEventListener('input', function (e) { state.hostFilter = e.target.value; renderHosts(); });\n $('#hostRows').addEventListener('click', function (e) {\n var b = e.target.closest('[data-copy]');\n if (b) copyText(b.dataset.copy);\n });\n\n async function loadAdopt() {\n var d = await api('/api/adopt/scan');\n if (d.error) { toast(d.error); return; }\n state.adopt = d;\n state.selected = {};\n $('#adoptCards').innerHTML =\n statCard('候选技能', d.summary.candidates, '已装宿主里的技能', '') +\n statCard('多副本冲突', d.summary.conflicts, '版本 / 哈希不一致', d.summary.conflicts ? 'warn' : 'ok') +\n statCard('已在 Hub', d.summary.alreadyInHub, '默认跳过', 'info') +\n statCard('扫描引擎', d.scan && d.scan.available ? '可用' : '未找到', d.scan && d.scan.available ? '收编时逐项扫描' : '可显式降级(CLI)', d.scan && d.scan.available ? 'ok' : 'warn');\n $('#adoptScanInfo').textContent = '生成于 ' + fmtTime(d.generatedAt);\n renderAdopt();\n }\n function renderAdopt() {\n var d = state.adopt; if (!d) return;\n var q = state.adoptFilter.toLowerCase();\n var candidates = (d.candidates || []).filter(function (c) {\n if (!q) return true;\n return String(c.slug || '').toLowerCase().includes(q) || String(c.sourceHost || '').toLowerCase().includes(q);\n });\n $('#adoptEmpty').hidden = candidates.length > 0;\n $('#adoptList').innerHTML = candidates.map(function (c) {\n var variants = (c.variants || []).map(function (v) {\n return '<div class=\"variant\"><span class=\"badge\">' + esc(v.host) + '</span><span class=\"mono\">' + esc(v.dir) + '</span><span>v' + esc(v.version || '-') + '</span></div>';\n }).join('');\n return '<div class=\"row\"><input class=\"check\" type=\"checkbox\" data-slug=\"' + esc(c.slug) + '\"' + (state.selected[c.slug] ? ' checked' : '') + (c.inHub ? ' disabled' : '') + '>' +\n '<div class=\"row-main\"><div class=\"row-title\">' + esc(c.slug) + ' <span class=\"badge\">v' + esc(c.version || '-') + '</span>' +\n (c.inHub ? ' <span class=\"badge info\">已在 Hub</span>' : '') +\n (c.conflict ? ' <span class=\"badge warn\">多副本冲突</span>' : '') + '</div>' +\n '<div class=\"row-meta\">来源:' + esc(c.sourceHost || '-') + ' · ' + esc(c.source || '') + '</div>' +\n (c.variants && c.variants.length > 1 ? '<details><summary>' + c.variants.length + ' 个副本</summary><div class=\"variants\">' + variants + '</div></details>' : '') +\n '</div></div>';\n }).join('');\n updateAdoptCount();\n }\n function updateAdoptCount() {\n var n = Object.keys(state.selected).filter(function (k) { return state.selected[k]; }).length;\n $('#adoptCount').textContent = String(n);\n $('#adoptApply').disabled = n === 0;\n }\n $('#adoptSearch').addEventListener('input', function (e) { state.adoptFilter = e.target.value; renderAdopt(); });\n $('#adoptList').addEventListener('change', function (e) {\n var box = e.target.closest('input[data-slug]');\n if (!box) return;\n if (box.checked) state.selected[box.dataset.slug] = true; else delete state.selected[box.dataset.slug];\n updateAdoptCount();\n });\n $('#adoptAll').onclick = function () {\n ((state.adopt && state.adopt.candidates) || []).forEach(function (c) { if (!c.inHub) state.selected[c.slug] = true; });\n renderAdopt();\n };\n $('#adoptNone').onclick = function () { state.selected = {}; renderAdopt(); };\n $('#adoptApply').onclick = function () {\n var slugs = Object.keys(state.selected).filter(function (k) { return state.selected[k]; });\n if (!slugs.length) { toast('还没有选择技能'); return; }\n var preview = slugs.slice(0, 24).join('、') + (slugs.length > 24 ? ' 等 ' + slugs.length + ' 项' : '');\n confirmModal('收编 ' + slugs.length + ' 个技能到 Hub?', '<p class=\"label\">将复制到 Hub(原目录保留);已在 Hub 或冲突项按默认策略处理;执行时逐项运行元信扫描,high / critical 会阻断。</p><div class=\"mono\">' + esc(preview) + '</div>', '开始收编', false, async function () {\n var r = await api('/api/adopt/apply', { body: { include: slugs } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n var order = { imported: 0, blocked: 1, conflict: 2, skip: 3, error: 4 };\n var results = (r.results || []).slice().sort(function (a, b) { return (order[a.status] || 9) - (order[b.status] || 9); });\n $('#adoptResultsWrap').hidden = false;\n $('#adoptResults').innerHTML = results.map(function (x) {\n var tone = x.status === 'imported' ? 'ok' : x.status === 'error' || x.status === 'blocked' ? 'danger' : 'warn';\n return '<div class=\"result\"><span class=\"badge ' + tone + '\">' + esc(x.status) + '</span><b>' + esc(x.slug) + '</b><span>' + esc(x.note || '') + '</span>' + (x.scanVerdict ? '<span class=\"faint\">verdict: ' + esc(x.scanVerdict) + '</span>' : '') + '</div>';\n }).join('');\n toast('收编完成:导入 ' + (r.summary ? r.summary.imported : 0) + ' / 阻断 ' + (r.summary ? r.summary.blocked : 0));\n loadOverview();\n loadAdopt();\n });\n };\n\n async function loadLinks() {\n var results = await Promise.all([api('/api/doctor'), api('/api/links'), api('/api/hosts')]);\n var doctor = results[0], links = results[1], hosts = results[2];\n if (doctor.error || links.error || hosts.error) { toast((doctor.error || links.error || hosts.error)); return; }\n $('#linkDoctor').innerHTML = doctorBanner(doctor);\n var existing = (hosts.hosts || []).filter(function (h) { return h.exists; });\n $('#linkTargets').innerHTML = existing.length ? existing.map(function (h) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(h.label) + '</div><div class=\"row-meta mono\">' + esc(h.dir) + '</div><div class=\"row-meta\">目录技能 ' + esc(h.skillCount || 0) + ' · 指向 Hub ' + esc(h.hubLinks || 0) + (h.brokenLinks ? ' · 异常 ' + h.brokenLinks : '') + '</div></div><div class=\"row-actions\"><button class=\"btn small\" data-link-plan=\"' + esc(h.dir) + '\" data-label=\"' + esc(h.label) + '\">预览链接</button></div></div>';\n }).join('') : '<div class=\"empty\">尚未发现可用的宿主技能目录。</div>';\n var rows = links.links || [];\n $('#linkEmpty').hidden = rows.length > 0;\n $('#linkRows').innerHTML = rows.map(function (l) {\n var tone = l.status === 'ok' ? 'ok' : l.status === 'broken' ? 'danger' : 'warn';\n return '<tr><td><div class=\"cell-main\">' + esc(l.slug) + '</div><div class=\"cell-sub\">' + esc(l.label || l.agent || '-') + '</div></td>' +\n '<td class=\"mono\">' + esc(l.dir) + '</td><td class=\"mono\">' + esc(l.target) + '</td>' +\n '<td><span class=\"badge ' + tone + '\">' + esc(l.status) + '</span></td>' +\n '<td><button class=\"btn small danger\" data-unlink=\"' + esc(l.target) + '\" data-dir=\"' + esc(l.dir) + '\" data-slug=\"' + esc(l.slug) + '\">解除</button></td></tr>';\n }).join('');\n }\n $('#linkTargets').addEventListener('click', async function (e) {\n var b = e.target.closest('[data-link-plan]');\n if (!b) return;\n var dir = b.dataset.linkPlan;\n var plan = await api('/api/links/plan?dir=' + encodeURIComponent(dir));\n if (plan.error) { toast(plan.error); return; }\n var would = plan.results.filter(function (r) { return r.status === 'would-link'; });\n var converge = plan.results.filter(function (r) { return r.status === 'would-converge'; });\n var skipped = plan.results.filter(function (r) { return r.status === 'skipped'; });\n var conflicts = plan.results.filter(function (r) { return r.status === 'conflict'; });\n var convergeMoves = converge.reduce(function (n, r) { return n + ((r.moves && r.moves.length) || 0); }, 0);\n var linkable = would.length + converge.length;\n var body = '<p class=\"label\">目标:' + esc(plan.label || '') + ' · <span class=\"mono\">' + esc(dir) + '</span></p>' +\n '<div class=\"result\"><span class=\"badge ok\">将建立</span><b>' + linkable + '</b><span>个链接</span></div>' +\n (converge.length ? '<div class=\"result\"><span class=\"badge warn\">将收敛</span><b>' + convergeMoves + '</b><span>份旧副本 → 回收站(保留 7 天,可恢复):' + esc(converge.map(function (r) { return r.slug; }).join('、')) + '</span></div>' : '') +\n (skipped.length ? '<div class=\"result\"><span class=\"badge warn\">跳过</span><b>' + skipped.length + '</b><span>项(Hub 版本旧于宿主 / 版本无法解析):' + esc(skipped.map(function (r) { return r.slug; }).join('、')) + '</span></div>' : '') +\n (conflicts.length ? '<div class=\"result\"><span class=\"badge warn\">跳过冲突</span><b>' + conflicts.length + '</b><span>个同名目录 / 链接(默认不覆盖)</span></div>' : '') +\n (linkable ? '<details><summary>查看明细</summary><div class=\"variants\">' + would.concat(converge).map(function (r) { return '<div class=\"variant\"><span class=\"mono\">' + esc(r.slug) + '</span></div>'; }).join('') + '</div></details>' : '<p class=\"faint\">没有可建立的链接。</p>');\n openModal('链接预览', body, {\n okText: linkable ? '执行链接' : null,\n onOk: async function () {\n var r = await api('/api/links/apply', { body: { targetDir: dir } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n var done = (r.results || []).filter(function (x) { return x.status === 'linked'; }).length;\n var moved = (r.results || []).reduce(function (n, x) { return n + ((x.moved && x.moved.length) || 0); }, 0);\n toast('已建立 ' + done + ' 个链接' + (moved ? ';收敛 ' + moved + ' 份旧副本(回收站 7 天)' : ''));\n loadLinks();\n },\n });\n });\n $('#linkRows').addEventListener('click', async function (e) {\n var b = e.target.closest('[data-unlink]');\n if (!b) return;\n var target = b.dataset.unlink;\n var dir = b.dataset.dir;\n var slug = b.dataset.slug;\n confirmModal('解除链接 ' + slug + '?', '<p class=\"label\">只删除宿主目录中的链接,Hub 真源保留。目标不是指向 Hub 的链接时会 fail-closed 拒绝。</p><div class=\"mono\">' + esc(target) + '</div>', '解除链接', true, async function () {\n var r = await api('/api/links/remove', { body: { targetDir: dir, slugs: [slug], confirm: 'unlink' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已解除 ' + slug);\n loadLinks();\n });\n });\n\n async function loadRecords() {\n var d = await api('/api/records?limit=200');\n if (d.error) { toast(d.error); return; }\n state.records = d;\n var valid = (d.snapshots || []).filter(function (s) { return s.valid; }).length;\n $('#recordsInfo').textContent = '快照 ' + valid + ' / ' + (d.snapshots || []).length + ' · 证据 ' + (d.evidence.entries || []).length + ' · 审计 ' + (d.audit.entries || []).length;\n renderSnapshots();\n var ev = (d.evidence.entries || []).slice().reverse();\n $('#evidenceEmpty').hidden = ev.length > 0;\n $('#evidenceRows').innerHTML = ev.map(function (x) {\n var tone = x.decision === 'ok' ? 'ok' : x.decision === 'fail' ? 'danger' : 'info';\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(x.event || 'event') + ' · ' + esc(x.skill || x.slug || '-') + '</div><div class=\"row-meta\">' + esc(fmtTime(x.ts || x.at)) + (x.version ? ' · v' + esc(x.version) : '') + (x.via ? ' · via ' + esc(x.via) : '') + '</div></div><span class=\"badge ' + tone + '\">' + esc(x.decision || '记录') + '</span></div>';\n }).join('');\n var aud = (d.audit.entries || []).slice().reverse();\n $('#auditEmpty').hidden = aud.length > 0;\n $('#auditRows').innerHTML = aud.map(function (x) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(x.event || 'audit') + ' · ' + esc(x.slug || '') + '</div><div class=\"row-meta\">' + esc(fmtTime(x.at)) + (x.verdict ? ' · ' + esc(x.verdict) : '') + (x.sourceHost ? ' · ' + esc(x.sourceHost) : '') + '</div></div></div>';\n }).join('');\n }\n function renderSnapshots() {\n var d = state.records; if (!d) return;\n var rows = d.snapshots || [];\n $('#snapshotEmpty').hidden = rows.length > 0;\n $('#snapshotRows').innerHTML = rows.map(function (s) {\n var canRollback = s.valid && s.source;\n return '<tr><td><div class=\"cell-main\">' + esc(s.slug) + '</div><div class=\"cell-sub\">' + esc(s.name) + '</div></td>' +\n '<td>v' + esc(s.version || '-') + '</td><td>' + esc(fmtTime(s.createdAt || s.mtime_ms)) + '</td><td class=\"mono\">' + esc(s.source || '-') + '</td>' +\n '<td>' + (s.valid ? '<span class=\"badge ok\">可用</span>' : '<span class=\"badge danger\">不可用</span>') + (s.legacy ? ' <span class=\"badge warn\">旧格式</span>' : '') + (s.reason ? '<div class=\"cell-sub\">' + esc(s.reason) + '</div>' : '') + '</td>' +\n '<td>' + (canRollback ? '<button class=\"btn small danger\" data-rollback=\"' + esc(s.path) + '\" data-slug=\"' + esc(s.slug) + '\" data-version=\"' + esc(s.version || '') + '\" data-source=\"' + esc(s.source || '') + '\">回滚</button>' : '<span class=\"faint\">CLI 处理</span>') + '</td></tr>';\n }).join('');\n }\n document.querySelectorAll('#view-records [data-tab]').forEach(function (b) {\n b.onclick = function () {\n state.tab = b.dataset.tab;\n Array.prototype.forEach.call(document.querySelectorAll('#view-records [data-tab]'), function (x) { x.classList.toggle('primary', x === b); });\n $('#recSnapshots').hidden = state.tab !== 'snapshots';\n $('#recEvidence').hidden = state.tab !== 'evidence';\n $('#recAudit').hidden = state.tab !== 'audit';\n };\n });\n document.querySelector('#view-records [data-tab=\"snapshots\"]').classList.add('primary');\n $('#snapshotRows').addEventListener('click', function (e) {\n var b = e.target.closest('[data-rollback]');\n if (!b) return;\n var slug = b.dataset.slug, snap = b.dataset.rollback, source = b.dataset.source;\n confirmModal('回滚 ' + slug + ' 到 v' + b.dataset.version + '?', '<p class=\"label\">将用快照覆盖安装来源目录;恢复后自动体检并写入证据。</p><div class=\"mono\">' + esc(source) + '</div>', '执行回滚', true, async function () {\n var r = await api('/api/rollback/apply', { body: { slug: slug, snapshot: snap, confirm: 'rollback' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast(r.ok ? '回滚完成(doctor 通过)' : '回滚完成,但体检未通过');\n loadRecords();\n loadOverview();\n });\n });\n\n $('#routeBtn').onclick = runRoute;\n $('#routeInput').addEventListener('keydown', function (e) { if (e.key === 'Enter') runRoute(); });\n async function runRoute() {\n var request = $('#routeInput').value.trim();\n if (!request) { toast('先输入一句需求'); return; }\n var d = await api('/api/route?request=' + encodeURIComponent(request));\n if (d.error) { toast(d.error); return; }\n var skills = (d.skills || []).map(function (s) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + s.order + '. ' + esc(s.slug) + ' <span class=\"badge ' + (s.installed ? 'ok' : 'warn') + '\">' + esc(s.installed ? '已装' + (s.version ? ' v' + s.version : '') : '缺失') + '</span></div><div class=\"row-meta\">' + esc(s.role || '') + '</div></div></div>';\n }).join('');\n var missing = (d.missing_skills || []).length\n ? '<div class=\"section panel\"><h3>缺失技能</h3><div class=\"toolbar\"><span class=\"mono\">' + esc((d.missing_skills || []).map(function (s) { return s.slug; }).join(', ')) + '</span><button class=\"btn small\" data-copy-cmd=\"' + esc(d.install_command || '') + '\">复制安装命令</button></div><div class=\"note\">安装与信任判断由你决定;元信装前扫描默认启用。</div></div>'\n : '';\n var others = (d.other_skill_candidates || []).length\n ? '<div class=\"section panel\"><h3>其他已装技能候选</h3>' + d.other_skill_candidates.map(function (c) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(c.slug) + ' <span class=\"badge info\">v' + esc(c.version) + '</span></div><div class=\"row-meta\">' + esc((c.matched_terms || []).join('、')) + ' · ' + esc(c.description || '') + '</div></div></div>';\n }).join('') + '<div class=\"note\">只读 frontmatter 机械匹配;使用前请自行确认。</div></div>'\n : '';\n $('#routeResult').innerHTML = '<div class=\"panel\"><div class=\"shead\"><h2>' + esc(d.playbook.name) + '</h2><span class=\"badge\">置信度 ' + esc(d.confidence) + '</span></div>' +\n '<div class=\"label\">' + esc(d.playbook.intent) + '</div><div class=\"list\">' + skills + '</div>' +\n '<div class=\"note\">动态路由:' + esc((d.dynamic && d.dynamic.status) || '未启用') + ' · 匹配词:' + esc((d.matched_keywords || []).join('、') || '无') + '</div></div>' + missing + others;\n }\n $('#routeResult').addEventListener('click', function (e) {\n var b = e.target.closest('[data-copy-cmd]');\n if (b) copyText(b.dataset.copyCmd);\n });\n\n var CLI_GROUPS = [\n { name: 'Hub 发现与安装', items: [\n { cmd: 'yotta-skills hub hosts', why: '发现本机宿主与技能目录' },\n { cmd: 'yotta-skills hub install', why: '把元阁家族装进 Hub 真源' },\n { cmd: 'yotta-skills hub adopt --scan', why: '只读预演收编候选与冲突' },\n { cmd: 'yotta-skills hub adopt --apply', why: '执行收编(默认复制保真)' },\n ] },\n { name: '链接与体检', items: [\n { cmd: 'yotta-skills hub link --all', why: '把 Hub 技能链接到全部宿主' },\n { cmd: 'yotta-skills hub link --agent codex', why: '只链接到一个宿主' },\n { cmd: 'yotta-skills hub unlink --all', why: '只删链接,不动 Hub 真源' },\n { cmd: 'yotta-skills hub status', why: '查看技能 / 来源 / 链接' },\n { cmd: 'yotta-skills hub doctor', why: '检查断链 / 缺失 / slug 不一致' },\n ] },\n { name: '安装与更新', items: [\n { cmd: 'yotta-skills update --installed-only --dir <技能目录>', why: '只维护已装元技能' },\n { cmd: 'yotta-skills update --check', why: '只读检查更新' },\n { cmd: 'yotta-skills rollback --list', why: '查看可回滚快照' },\n { cmd: 'yotta-skills rollback --slug <技能> --dir <技能目录>', why: '回滚单个技能' },\n ] },\n { name: '路由与使用', items: [\n { cmd: 'yotta-skills --route \"帮我做发布前质量检查\"', why: '静态编排路由 + 动态路由块' },\n { cmd: 'yotta-skills --list', why: '列出全家技能与版本' },\n { cmd: 'yotta-skills --inventory --json', why: '盘点本机已装技能' },\n ] },\n ];\n $('#cliGroups').innerHTML = CLI_GROUPS.map(function (g) {\n return '<div class=\"panel cmd-group\"><h3>' + esc(g.name) + '</h3>' + g.items.map(function (item) {\n return '<div class=\"cmd\"><code>' + esc(item.cmd) + '</code><span class=\"why\">' + esc(item.why) + '</span><button class=\"btn small\" data-copy-cmd=\"' + esc(item.cmd) + '\">复制</button></div>';\n }).join('') + '</div>';\n }).join('');\n document.addEventListener('click', function (e) {\n var b = e.target.closest('#cliGroups [data-copy-cmd]');\n if (b) copyText(b.dataset.copyCmd);\n });\n\n (async function boot() {\n var s = await api('/api/status');\n if (s && !s.error) $('#verLine').textContent = 'v' + s.version + ' · ' + (s.standard || '');\n else $('#verLine').textContent = '未连接本地服务';\n loadOverview();\n })();\n})();\n</script>\n</body>\n</html>\n";
62
+ const VIEW_HTML = "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex,nofollow\">\n<meta name=\"yotta-view-token\" content=\"__YOTTA_VIEW_TOKEN__\">\n<link rel=\"icon\" href=\"data:,\">\n<title>元阁 · 技能枢纽</title>\n<style>\n :root{--bg:#0d1114;--bg2:#12171b;--panel:#171d22;--panel2:#1d242a;--line:rgba(148,190,170,.14);--line2:rgba(148,190,170,.28);\n --text:#e9efe9;--muted:#9aa8a0;--faint:#6d7b74;--acc:#5fbf8f;--acc-ink:#0d1a13;--warn:#e0b263;--danger:#e07a7a;--info:#8fb4de;\n --shadow:0 10px 28px rgba(0,0,0,.35)}\n body.light{--bg:#f2f5f1;--bg2:#e9efea;--panel:#ffffff;--panel2:#f6f9f5;--line:rgba(24,58,44,.12);--line2:rgba(24,58,44,.24);\n --text:#17211c;--muted:#5c6b63;--faint:#86958c;--acc:#2f7d59;--acc-ink:#ffffff;--warn:#9a6a14;--danger:#b23c3c;--info:#3c6ea5;\n --shadow:0 8px 22px rgba(24,58,44,.08)}\n *{box-sizing:border-box}[hidden]{display:none!important}\n html,body{margin:0;padding:0}\n body{background:var(--bg);color:var(--text);font:14px/1.6 \"Microsoft YaHei\",\"PingFang SC\",\"Noto Sans CJK SC\",system-ui,sans-serif;letter-spacing:0}\n button{font:inherit;color:inherit;cursor:pointer}\n input,textarea{font:inherit;color:var(--text);background:var(--panel2);border:1px solid var(--line2);border-radius:6px;padding:8px 11px;outline:0;width:100%}\n input:focus,textarea:focus{border-color:var(--acc)}\n textarea{min-height:76px;resize:vertical}\n code,kbd{font-family:ui-monospace,Consolas,\"Courier New\",monospace;font-size:12px}\n .sv{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}\n\n .shell{display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh}\n .sidebar{background:var(--bg2);border-right:1px solid var(--line);padding:20px 14px;display:flex;flex-direction:column;gap:20px;position:sticky;top:0;height:100vh;min-width:0}\n .brand{display:flex;align-items:center;gap:10px;padding:0 6px}\n .brand-mark{width:34px;height:34px;border-radius:8px;background:rgba(95,191,143,.14);border:1px solid var(--line2);display:grid;place-items:center;color:var(--acc)}\n .brand-name{font-size:15px;font-weight:600}\n .brand-sub{font-size:11px;color:var(--faint)}\n .nav{display:flex;flex-direction:column;gap:3px;min-width:0}\n .nav button{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);text-align:left}\n .nav button:hover{background:var(--panel);color:var(--text)}\n .nav button.active{background:var(--panel);color:var(--acc);border-color:var(--line)}\n .side-foot{margin-top:auto;display:flex;flex-direction:column;gap:8px;min-width:0}\n .privacy{display:flex;gap:8px;align-items:flex-start;padding:10px;border:1px solid var(--line);border-radius:8px;color:var(--muted);font-size:12px;background:var(--panel)}\n .privacy .sv{color:var(--acc);margin-top:2px}\n .ghost-btn{display:flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:6px;padding:8px}\n .ghost-btn:hover{color:var(--text);border-color:var(--line2)}\n .verline{padding:0 6px;color:var(--faint);font-size:11px}\n\n .main{min-width:0;padding:24px 28px 70px}\n .topbar{display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}\n .page-title{font-size:19px;font-weight:600;margin:0}\n .page-sub{color:var(--faint);font-size:12px;margin-top:2px}\n .spacer{flex:1}\n .chip{display:flex;align-items:center;gap:7px;padding:5px 10px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font-size:12px;white-space:nowrap}\n .dot{width:7px;height:7px;border-radius:50%;background:var(--acc)}\n\n .view{display:none}.view.active{display:block}\n .grid{display:grid;gap:12px}.g4{grid-template-columns:repeat(4,minmax(0,1fr))}.g2{grid-template-columns:repeat(2,minmax(0,1fr))}\n .panel{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:15px;min-width:0}\n .panel h3{margin:0 0 4px;font-size:14px}\n .section{margin-top:16px}\n .shead{display:flex;align-items:baseline;gap:10px;margin-bottom:9px;flex-wrap:wrap}\n .shead h2{font-size:14px;margin:0}\n .label{color:var(--muted);font-size:12px}\n .value{font-size:25px;font-weight:600;margin:7px 0 2px}\n .delta{font-size:12px;color:var(--muted)}\n .mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all}\n\n .toolbar{display:flex;gap:9px;align-items:center;flex-wrap:wrap}\n .search{display:flex;align-items:center;gap:8px;min-width:230px;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:7px 11px;color:var(--muted)}\n .search input{background:transparent;border:0;padding:0;width:100%}\n .btn{display:inline-flex;align-items:center;gap:7px;padding:7px 13px;border-radius:6px;border:1px solid var(--line2);background:transparent;color:var(--text);white-space:nowrap}\n .btn:hover{border-color:var(--acc)}\n .btn.primary{background:var(--acc);border-color:var(--acc);color:var(--acc-ink);font-weight:600}\n .btn.danger{border-color:rgba(224,122,122,.5);color:var(--danger)}\n .btn.small{padding:4px 9px;font-size:12px}\n .btn[disabled]{opacity:.45;cursor:not-allowed}\n .ibtn{width:29px;height:29px;display:grid;place-items:center;border:1px solid var(--line);background:transparent;border-radius:6px;color:var(--muted)}\n .ibtn:hover{color:var(--text);border-color:var(--line2)}\n\n table{width:100%;border-collapse:collapse}\n th,td{text-align:left;padding:10px 9px;border-bottom:1px solid var(--line);font-size:13px;vertical-align:top}\n th{color:var(--faint);font-weight:500;font-size:12px}\n tbody tr:hover{background:var(--panel2)}\n .cell-main{font-weight:600}\n .cell-sub{color:var(--faint);font-size:11px;word-break:break-all}\n\n .list{display:flex;flex-direction:column;gap:7px;margin-top:12px}\n .row{display:flex;align-items:center;gap:11px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);min-width:0}\n .row-main{flex:1;min-width:0}\n .row-title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .row-meta{color:var(--faint);font-size:11px;margin-top:2px;word-break:break-all}\n .row-actions{display:flex;gap:6px;flex-wrap:wrap}\n .empty{color:var(--faint);text-align:center;padding:22px;border:1px dashed var(--line);border-radius:8px;margin-top:12px}\n\n .badge{display:inline-block;font-size:11px;padding:1px 8px;border-radius:999px;border:1px solid var(--line2);color:var(--muted);background:transparent;white-space:nowrap}\n .badge.ok{color:var(--acc);border-color:rgba(95,191,143,.45);background:rgba(95,191,143,.10)}\n .badge.warn{color:var(--warn);border-color:rgba(224,178,99,.45);background:rgba(224,178,99,.10)}\n .badge.danger{color:var(--danger);border-color:rgba(224,122,122,.45);background:rgba(224,122,122,.10)}\n .badge.info{color:var(--info);border-color:rgba(143,180,222,.45);background:rgba(143,180,222,.10)}\n .dot-mark{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;background:var(--faint)}\n .dot-mark.ok{background:var(--acc)}.dot-mark.warn{background:var(--warn)}.dot-mark.danger{background:var(--danger)}\n\n .banner{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border:1px solid var(--line2);border-radius:8px;background:var(--panel2)}\n .banner.ok{border-color:rgba(95,191,143,.45)}\n .banner.danger{border-color:rgba(224,122,122,.45)}\n .banner .sv{margin-top:2px;color:var(--acc)}\n .banner.danger .sv{color:var(--danger)}\n .banner-title{font-weight:600}\n .banner-body{color:var(--muted);font-size:12px;margin-top:2px}\n\n .kv{display:grid;grid-template-columns:96px 1fr;gap:5px 10px;font-size:13px;margin:10px 0}\n .kv dt{color:var(--faint)}.kv dd{margin:0;word-break:break-all}\n .variants{border-top:1px dashed var(--line);margin-top:8px;padding-top:8px;display:flex;flex-direction:column;gap:5px}\n .variant{display:flex;gap:9px;align-items:baseline;font-size:12px;color:var(--muted);flex-wrap:wrap}\n .check{width:16px;height:16px;flex:none;accent-color:var(--acc)}\n details summary{cursor:pointer;color:var(--muted);font-size:12px}\n\n .actionbar{position:sticky;bottom:0;margin-top:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:11px 13px;border:1px solid var(--line2);border-radius:8px;background:var(--bg2);box-shadow:var(--shadow)}\n .result-list{display:flex;flex-direction:column;gap:6px;margin-top:10px}\n .result{display:flex;gap:9px;align-items:baseline;flex-wrap:wrap;padding:8px 10px;border:1px solid var(--line);border-radius:6px;font-size:12px}\n\n .mask{position:fixed;inset:0;background:rgba(0,0,0,.52);display:none;place-items:center;z-index:50;padding:18px}\n .mask.open{display:grid}\n .modal{width:min(600px,100%);max-height:86vh;overflow:auto;background:var(--panel);border:1px solid var(--line2);border-radius:8px;padding:18px;box-shadow:var(--shadow)}\n .modal h3{margin:0 0 10px;font-size:16px}\n .modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}\n\n .toasts{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);display:flex;flex-direction:column;gap:7px;align-items:center;z-index:60}\n .toast{background:var(--panel2);border:1px solid var(--line2);border-radius:999px;padding:8px 15px;box-shadow:var(--shadow);font-size:13px}\n\n .cmd-group{margin-top:14px}\n .cmd{display:flex;gap:10px;align-items:center;padding:9px 11px;border:1px solid var(--line);border-radius:6px;background:var(--panel2);margin-top:6px}\n .cmd code{flex:1;min-width:0;word-break:break-all;color:var(--text)}\n .cmd .why{color:var(--faint);font-size:11px}\n .note{color:var(--muted);font-size:12px;margin-top:8px}\n\n @media (max-width:980px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}}\n @media (max-width:760px){\n .shell{grid-template-columns:minmax(0,1fr)}\n .sidebar{position:static;height:auto;padding:13px;gap:11px}\n .nav{flex-direction:row;overflow-x:auto;width:100%;scrollbar-width:none;-webkit-overflow-scrolling:touch}\n .nav::-webkit-scrollbar{display:none}\n .nav button{white-space:nowrap;padding:7px 9px}\n .side-foot{margin-top:0;flex-direction:row;align-items:center}\n .privacy{display:none}\n .ghost-btn{flex:1;white-space:nowrap}\n .verline{white-space:nowrap;text-align:right}\n .main{padding:16px 14px 56px}\n .g4,.g2{grid-template-columns:minmax(0,1fr)}\n .search{min-width:100%}\n table{display:block;overflow-x:auto;white-space:nowrap}\n .row{flex-wrap:wrap}\n }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <aside class=\"sidebar\">\n <div class=\"brand\">\n <div class=\"brand-mark\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 3 4 7v4c0 4.4 3.4 8.4 8 10 4.6-1.6 8-5.6 8-10V7z\"/><path d=\"M9 12l2 2 4-4\"/></svg></div>\n <div><div class=\"brand-name\">元阁 · 技能枢纽</div><div class=\"brand-sub\">Hub 本地面板</div></div>\n </div>\n <nav class=\"nav\" id=\"nav\">\n <button data-view=\"overview\" class=\"active\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 10.5 12 3l9 7.5\"/><path d=\"M5 9.5V21h14V9.5\"/></svg>概览</button>\n <button data-view=\"hosts\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"7\" rx=\"2\"/><rect x=\"3\" y=\"13\" width=\"18\" height=\"7\" rx=\"2\"/><path d=\"M7 7.5h.01M7 16.5h.01\"/></svg>宿主矩阵</button>\n <button data-view=\"adopt\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 3v12\"/><path d=\"m7 10 5 5 5-5\"/><path d=\"M4 21h16\"/></svg>收编向导</button>\n <button data-view=\"links\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M10 13a5 5 0 0 0 7.1 0l2-2a5 5 0 0 0-7.1-7.1L11 4.9\"/><path d=\"M14 11a5 5 0 0 0-7.1 0l-2 2A5 5 0 0 0 12 20.1l1-1\"/></svg>链接与体检</button>\n <button data-view=\"records\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/><path d=\"M12 8v5l3 2\"/></svg>记录与回滚</button>\n <button data-view=\"route\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"6\" cy=\"6\" r=\"2.5\"/><circle cx=\"18\" cy=\"18\" r=\"2.5\"/><path d=\"M8.5 6H15a3 3 0 0 1 0 6h-6a3 3 0 0 0 0 6h6.5\"/></svg>路由与编排</button>\n <button data-view=\"cli\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"m4 7 5 5-5 5\"/><path d=\"M12 19h8\"/></svg>高级 CLI</button>\n </nav>\n <div class=\"side-foot\">\n <div class=\"privacy\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/><path d=\"m9 12 2 2 4-4\"/></svg><span>本机运行 · 零远程<br>写操作需页面令牌与确认</span></div>\n <button class=\"ghost-btn\" id=\"themeBtn\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/></svg>切换主题</button>\n <div class=\"verline\" id=\"verLine\"></div>\n </div>\n </aside>\n\n <main class=\"main\">\n <div class=\"topbar\">\n <div><h1 class=\"page-title\" id=\"pageTitle\">概览</h1><div class=\"page-sub\" id=\"pageSub\">Hub 里有什么、健康吗、最近做了什么</div></div>\n <div class=\"spacer\"></div>\n <button class=\"btn\" id=\"refreshBtn\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M3 12a9 9 0 1 0 3-6.7\"/><path d=\"M3 4v5h5\"/></svg>刷新</button>\n <div class=\"chip\"><span class=\"dot\"></span>本机 · 127.0.0.1</div>\n </div>\n\n <section class=\"view active\" id=\"view-overview\">\n <div class=\"grid g4\" id=\"ovCards\"></div>\n <div class=\"section\" id=\"ovDoctor\"></div>\n <div class=\"section grid g2\">\n <div class=\"panel\"><h3>Hub 内容</h3><div class=\"faint\">技能真源在 Hub 目录,宿主目录通过链接分发。</div><div class=\"list\" id=\"ovSkills\"></div></div>\n <div class=\"panel\"><h3>最近动作</h3><div class=\"faint\">收编 / 链接 / 解除都写入 Hub 审计。</div><div class=\"list\" id=\"ovAudit\"></div></div>\n </div>\n <div class=\"section panel\">\n <h3>Hub 位置</h3>\n <div class=\"mono\" id=\"ovHubDir\">-</div>\n <div class=\"toolbar section\">\n <button class=\"btn small\" data-goto=\"adopt\">去收编</button>\n <button class=\"btn small\" data-goto=\"links\">去链接与体检</button>\n <button class=\"btn small\" data-goto=\"records\">查看记录</button>\n <button class=\"btn small\" id=\"copyHubDir\">复制路径</button>\n </div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-hosts\">\n <div class=\"toolbar\">\n <label class=\"search\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"hostSearch\" placeholder=\"过滤宿主 / 目录\"></label>\n <span class=\"label\" id=\"hostSummary\"></span>\n </div>\n <div class=\"section panel\">\n <table><thead><tr><th>宿主</th><th>技能目录</th><th>范围</th><th>目录技能</th><th>指向 Hub</th><th>异常</th><th>状态</th></tr></thead><tbody id=\"hostRows\"></tbody></table>\n <div class=\"empty\" id=\"hostEmpty\" hidden>没有匹配的宿主目录。</div>\n </div>\n <div class=\"section panel\"><h3>已安装标记(无技能目录的宿主)</h3><div class=\"list\" id=\"hostMarks\"></div></div>\n </section>\n\n <section class=\"view\" id=\"view-adopt\">\n <div class=\"grid g4\" id=\"adoptCards\"></div>\n <div class=\"section toolbar\">\n <label class=\"search\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"adoptSearch\" placeholder=\"过滤技能 slug / 来源宿主\"></label>\n <button class=\"btn small\" id=\"adoptAll\">全选可收编</button>\n <button class=\"btn small\" id=\"adoptNone\">清空</button>\n <span class=\"spacer\"></span>\n <span class=\"label\" id=\"adoptScanInfo\"></span>\n </div>\n <div class=\"list\" id=\"adoptList\"></div>\n <div class=\"empty\" id=\"adoptEmpty\" hidden>没有可收编的候选。先在宿主里安装技能,或刷新宿主矩阵。</div>\n <div class=\"actionbar\">\n <span class=\"label\">已选 <b id=\"adoptCount\">0</b> 项</span>\n <span class=\"spacer\"></span>\n <span class=\"faint\">默认复制保真、原目录保留;冲突项默认跳过;执行时逐项运行元信扫描。</span>\n <button class=\"btn primary\" id=\"adoptApply\">收编所选</button>\n </div>\n <div class=\"section panel\" id=\"adoptResultsWrap\" hidden><h3>收编结果</h3><div class=\"result-list\" id=\"adoptResults\"></div></div>\n </section>\n\n <section class=\"view\" id=\"view-links\">\n <div id=\"linkDoctor\"></div>\n <div class=\"section panel\">\n <h3>分发目标</h3>\n <div class=\"faint\">把 Hub 技能链接到宿主目录(Windows junction / POSIX symlink);默认范围 = 已核实宿主,自动发现目录需在 CLI 用 --include-discovered 显式纳入,锁 / 数据桥接目录永不链接;元技能链接时收敛旧副本(移入回收站,保留 7 天可恢复)。</div>\n <div class=\"list\" id=\"linkTargets\"></div>\n </div>\n <div class=\"section panel\">\n <h3>当前链接</h3>\n <table><thead><tr><th>技能</th><th>宿主</th><th>目标</th><th>状态</th><th></th></tr></thead><tbody id=\"linkRows\"></tbody></table>\n <div class=\"empty\" id=\"linkEmpty\" hidden>还没有任何链接。</div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-records\">\n <div class=\"toolbar\">\n <button class=\"btn small\" data-tab=\"snapshots\">回滚快照</button>\n <button class=\"btn small\" data-tab=\"evidence\">安装 / 回滚证据</button>\n <button class=\"btn small\" data-tab=\"audit\">Hub 审计</button>\n <span class=\"spacer\"></span>\n <span class=\"label\" id=\"recordsInfo\"></span>\n </div>\n <div class=\"section panel\" id=\"recSnapshots\">\n <table><thead><tr><th>技能</th><th>版本</th><th>时间</th><th>来源目录</th><th>状态</th><th></th></tr></thead><tbody id=\"snapshotRows\"></tbody></table>\n <div class=\"empty\" id=\"snapshotEmpty\" hidden>没有安装快照。快照由 install / update 写入。</div>\n </div>\n <div class=\"section panel\" id=\"recEvidence\" hidden>\n <div class=\"list\" id=\"evidenceRows\"></div>\n <div class=\"empty\" id=\"evidenceEmpty\" hidden>没有证据记录。</div>\n </div>\n <div class=\"section panel\" id=\"recAudit\" hidden>\n <div class=\"list\" id=\"auditRows\"></div>\n <div class=\"empty\" id=\"auditEmpty\" hidden>没有 Hub 审计记录。</div>\n </div>\n </section>\n\n <section class=\"view\" id=\"view-route\">\n <div class=\"panel\">\n <h3>一句话需求 → 技能组合</h3>\n <div class=\"faint\">只读调用静态 playbook(7 组)+ 动态路由块;安装建议只给命令,不在网页执行。</div>\n <div class=\"toolbar section\">\n <label class=\"search\" style=\"flex:1\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/></svg><input id=\"routeInput\" placeholder=\"例如:帮我做发布前质量检查\"></label>\n <button class=\"btn primary\" id=\"routeBtn\">给出组合</button>\n </div>\n </div>\n <div id=\"routeResult\" class=\"section\"></div>\n </section>\n\n <section class=\"view\" id=\"view-cli\">\n <div class=\"banner\">\n <svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"m4 7 5 5-5 5\"/><path d=\"M12 19h8\"/></svg>\n <div><div class=\"banner-title\">高级 CLI</div><div class=\"banner-body\">低频 / 长尾命令在这里复制到终端执行;网页不代替终端运行安装与更新。</div></div>\n </div>\n <div id=\"cliGroups\"></div>\n </section>\n </main>\n</div>\n\n<div class=\"mask\" id=\"mask\"><div class=\"modal\" id=\"modal\"></div></div>\n<div class=\"toasts\" id=\"toasts\"></div>\n\n<script>\n(function () {\n var TOKEN = (document.querySelector('meta[name=\"yotta-view-token\"]') || {}).content || '';\n var state = { view: 'overview', overview: null, hosts: null, adopt: null, selected: {}, records: null, tab: 'snapshots', hostFilter: '', adoptFilter: '' };\n\n function $(sel) { return document.querySelector(sel); }\n function esc(value) {\n return String(value === null || value === undefined ? '' : value)\n .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;').replace(/'/g, '&#39;');\n }\n function fmtTime(value) {\n var ms = typeof value === 'number' ? value : Date.parse(value);\n if (!ms || isNaN(ms)) return value ? String(value) : '-';\n var d = new Date(ms);\n function p(n) { return n < 10 ? '0' + n : '' + n; }\n return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());\n }\n function toast(text) {\n var el = document.createElement('div');\n el.className = 'toast';\n el.textContent = text;\n $('#toasts').appendChild(el);\n setTimeout(function () { el.remove(); }, 3200);\n }\n function copyText(text) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(function () { toast('已复制'); }, function () { fallbackCopy(text); });\n } else fallbackCopy(text);\n }\n function fallbackCopy(text) {\n var ta = document.createElement('textarea');\n ta.value = text; document.body.appendChild(ta); ta.select();\n try { document.execCommand('copy'); toast('已复制'); } catch (e) { toast('复制失败,请手动选择'); }\n ta.remove();\n }\n async function api(path, opts) {\n opts = opts || {};\n var init = { method: opts.method || 'GET', headers: {} };\n if (opts.body !== undefined) {\n init.method = 'POST';\n init.headers['Content-Type'] = 'application/json';\n init.headers['X-Yotta-View-Token'] = TOKEN;\n init.body = JSON.stringify(opts.body);\n }\n var res, data = null;\n try { res = await fetch(path, init); } catch (e) { return { error: '无法连接本地服务:' + e.message }; }\n try { data = await res.json(); } catch (e) { data = null; }\n if (!res.ok) return data && data.error ? data : { error: '请求失败(HTTP ' + res.status + ')' };\n if (!data) return { error: '响应不是合法 JSON' };\n return data;\n }\n\n function openModal(title, bodyHtml, opts) {\n opts = opts || {};\n var html = '<h3>' + esc(title) + '</h3><div id=\"modalBody\">' + bodyHtml + '</div>' +\n '<div class=\"modal-actions\"><button class=\"btn\" data-modal=\"cancel\">' + esc(opts.cancelText || '取消') + '</button>' +\n (opts.okText === null ? '' : '<button class=\"btn ' + (opts.danger ? 'danger' : 'primary') + '\" data-modal=\"ok\">' + esc(opts.okText || '确认') + '</button>') +\n '</div>';\n $('#modal').innerHTML = html;\n $('#mask').classList.add('open');\n var ok = $('#modal').querySelector('[data-modal=\"ok\"]');\n $('#modal').querySelector('[data-modal=\"cancel\"]').onclick = closeModal;\n if (ok && opts.onOk) {\n ok.onclick = async function () {\n ok.disabled = true;\n try { await opts.onOk(); } finally { ok.disabled = false; }\n };\n }\n }\n function closeModal() { $('#mask').classList.remove('open'); $('#modal').innerHTML = ''; }\n function confirmModal(title, bodyHtml, okText, danger, onOk) {\n openModal(title, bodyHtml, { okText: okText, danger: danger, onOk: onOk });\n }\n $('#mask').addEventListener('click', function (e) { if (e.target === $('#mask')) closeModal(); });\n document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeModal(); });\n\n var VIEWS = {\n overview: { title: '概览', sub: 'Hub 里有什么、健康吗、最近做了什么', load: loadOverview },\n hosts: { title: '宿主矩阵', sub: '装了哪些宿主、技能目录在哪、Hub 技能链到了谁', load: loadHosts },\n adopt: { title: '收编向导', sub: '有哪些可收编、哪些是重复副本、收编后会发生什么', load: loadAdopt },\n links: { title: '链接与体检', sub: '断链 / 漂移 / 目标缺失在哪、怎么修', load: loadLinks },\n records: { title: '记录与回滚', sub: '安装、收编、链接都做了什么,怎么退回', load: loadRecords },\n route: { title: '路由与编排', sub: '一句话需求该用哪些技能、缺什么(只读)', load: null },\n cli: { title: '高级 CLI', sub: '低频命令速查与复制', load: null }\n };\n function switchView(name) {\n if (!VIEWS[name]) return;\n state.view = name;\n Array.prototype.forEach.call(document.querySelectorAll('#nav button'), function (b) { b.classList.toggle('active', b.dataset.view === name); });\n Array.prototype.forEach.call(document.querySelectorAll('.view'), function (v) { v.classList.toggle('active', v.id === 'view-' + name); });\n $('#pageTitle').textContent = VIEWS[name].title;\n $('#pageSub').textContent = VIEWS[name].sub;\n if (VIEWS[name].load) VIEWS[name].load();\n }\n $('#nav').addEventListener('click', function (e) {\n var b = e.target.closest('button[data-view]');\n if (b) switchView(b.dataset.view);\n });\n document.addEventListener('click', function (e) {\n var g = e.target.closest('[data-goto]');\n if (g) switchView(g.dataset.goto);\n });\n $('#refreshBtn').onclick = function () { var v = VIEWS[state.view]; if (v && v.load) v.load(); else toast('已刷新'); };\n $('#themeBtn').onclick = function () {\n document.body.classList.toggle('light');\n try { localStorage.setItem('yotta-hub-theme', document.body.classList.contains('light') ? 'light' : 'dark'); } catch (e) {}\n };\n try { if (localStorage.getItem('yotta-hub-theme') === 'light') document.body.classList.add('light'); } catch (e) {}\n\n function statCard(label, value, delta, tone) {\n return '<div class=\"panel\"><div class=\"label\">' + esc(label) + '</div><div class=\"value\">' + esc(value) + '</div><div class=\"delta ' + (tone ? 'badge ' + tone : '') + '\">' + esc(delta) + '</div></div>';\n }\n function doctorBanner(doctor) {\n if (!doctor) return '';\n if (doctor.ok) {\n return '<div class=\"banner ok\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/><path d=\"m9 12 2 2 4-4\"/></svg><div><div class=\"banner-title\">体检通过</div><div class=\"banner-body\">警告 ' + doctor.summary.warnings + ' / 信息 ' + doctor.summary.info + ';结构性错误 0。</div></div></div>';\n }\n var bad = (doctor.checks || []).filter(function (c) { return !c.ok; }).slice(0, 6).map(function (c) {\n return '<div class=\"result\"><span class=\"badge ' + (c.severity === 'error' ? 'danger' : 'warn') + '\">' + esc(c.severity === 'error' ? '错误' : '警告') + '</span><span>' + esc(c.message) + '</span>' + (c.hint ? '<span class=\"faint\">修复:' + esc(c.hint) + '</span>' : '') + '</div>';\n }).join('');\n return '<div class=\"banner danger\"><svg class=\"sv\" viewBox=\"0 0 24 24\"><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/><path d=\"M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z\"/></svg><div style=\"flex:1\"><div class=\"banner-title\">体检未通过(错误 ' + doctor.summary.errors + ' / 警告 ' + doctor.summary.warnings + ')</div><div class=\"result-list\">' + bad + '</div></div></div>';\n }\n\n async function loadOverview() {\n var d = await api('/api/overview');\n if (d.error) { toast(d.error); return; }\n state.overview = d;\n $('#ovCards').innerHTML =\n statCard('Hub 技能', d.status.summary.skills, '外部来源 ' + d.status.summary.external, '') +\n statCard('已链接', d.status.summary.links, '宿主目录投影', 'ok') +\n statCard('异常链接', d.status.summary.brokenLinks, d.status.summary.brokenLinks ? '需要处理' : '全部正常', d.status.summary.brokenLinks ? 'danger' : 'ok') +\n statCard('Doctor', d.doctor.ok ? '通过' : '未通过', '错误 ' + d.doctor.summary.errors + ' / 警告 ' + d.doctor.summary.warnings, d.doctor.ok ? 'ok' : 'danger');\n $('#ovDoctor').innerHTML = doctorBanner(d.doctor);\n var skills = d.status.skills || [];\n $('#ovSkills').innerHTML = skills.length ? skills.slice(0, 8).map(function (s) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(s.slug) + '</div><div class=\"row-meta\">v' + esc(s.version || '-') + ' · ' + esc(s.origin === 'external' ? '外部来源' : '元阁家族') + ' · 链接 ' + (s.links || []).length + '</div></div><span class=\"badge ' + (s.status === 'present' ? 'ok' : 'danger') + '\">' + esc(s.status === 'present' ? '已入库' : '缺失') + '</span></div>';\n }).join('') : '<div class=\"empty\">Hub 还是空的。先在「收编向导」把已装技能收编进来。</div>';\n var audit = (d.audit && d.audit.recent) || [];\n $('#ovAudit').innerHTML = audit.length ? audit.slice().reverse().map(function (a) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(a.event || '动作') + ' · ' + esc(a.slug || '') + '</div><div class=\"row-meta\">' + esc(fmtTime(a.at)) + (a.verdict ? ' · ' + esc(a.verdict) : '') + '</div></div></div>';\n }).join('') : '<div class=\"empty\">还没有动作记录。</div>';\n $('#ovHubDir').textContent = d.hubDir;\n $('#copyHubDir').onclick = function () { copyText(d.hubDir); };\n }\n\n async function loadHosts() {\n var d = await api('/api/hosts');\n if (d.error) { toast(d.error); return; }\n state.hosts = d;\n $('#hostSummary').textContent = '目录 ' + d.summary.existing + ' / ' + d.summary.hosts + '(已核实 ' + d.summary.verified + ' · 自动发现 ' + d.summary.discovered + ' · 桥接 ' + d.summary.bridge + ')· 已装标记 ' + d.summary.installedMarks + ' · 链接 ' + d.summary.links;\n renderHosts();\n var marks = d.installed || [];\n $('#hostMarks').innerHTML = marks.length ? marks.map(function (m) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(m.label) + '</div><div class=\"row-meta\">' + esc(m.source || '') + (m.hasSkillsDir ? ' · 技能目录:' + esc(m.skillDir || '') : ' · 未发现技能目录') + '</div></div><span class=\"badge ' + (m.hasSkillsDir ? 'ok' : 'info') + '\">' + esc(m.hasSkillsDir ? '可链接' : '仅标记') + '</span></div>';\n }).join('') : '<div class=\"empty\">没有检测到额外宿主标记。</div>';\n }\n function renderHosts() {\n var d = state.hosts; if (!d) return;\n var q = state.hostFilter.toLowerCase();\n var rows = (d.hosts || []).filter(function (h) {\n if (!q) return true;\n return String(h.label || '').toLowerCase().includes(q) || String(h.dir || '').toLowerCase().includes(q);\n });\n $('#hostEmpty').hidden = rows.length > 0;\n $('#hostRows').innerHTML = rows.map(function (h) {\n var status = !h.exists ? '<span class=\"badge\">未创建</span>' : (h.brokenLinks ? '<span class=\"badge danger\">有异常</span>' : '<span class=\"badge ok\">可用</span>');\n var scopeBadge = h.bridgeOnly ? '<span class=\"badge info\">桥接·不链</span>' : h.verified ? '<span class=\"badge ok\">已核实</span>' : '<span class=\"badge warn\">自动发现</span>';\n return '<tr><td><div class=\"cell-main\">' + esc(h.label) + '</div><div class=\"cell-sub\">' + esc(h.agentId || (h.known ? '已知宿主' : h.detection || '自动发现')) + '</div></td>' +\n '<td><div class=\"mono\">' + esc(h.dir) + '</div><button class=\"btn small\" data-copy=\"' + esc(h.dir) + '\">复制</button></td>' +\n '<td>' + scopeBadge + '</td>' +\n '<td>' + esc(h.skillCount || 0) + '</td><td>' + esc(h.hubLinks || 0) + '</td><td>' + esc(h.brokenLinks || 0) + '</td><td>' + status + '</td></tr>';\n }).join('');\n }\n $('#hostSearch').addEventListener('input', function (e) { state.hostFilter = e.target.value; renderHosts(); });\n $('#hostRows').addEventListener('click', function (e) {\n var b = e.target.closest('[data-copy]');\n if (b) copyText(b.dataset.copy);\n });\n\n async function loadAdopt() {\n var d = await api('/api/adopt/scan');\n if (d.error) { toast(d.error); return; }\n state.adopt = d;\n state.selected = {};\n $('#adoptCards').innerHTML =\n statCard('候选技能', d.summary.candidates, '已装宿主里的技能', '') +\n statCard('多副本冲突', d.summary.conflicts, '版本 / 哈希不一致', d.summary.conflicts ? 'warn' : 'ok') +\n statCard('已在 Hub', d.summary.alreadyInHub, '默认跳过', 'info') +\n statCard('扫描引擎', d.scan && d.scan.available ? '可用' : '未找到', d.scan && d.scan.available ? '收编时逐项扫描' : '可显式降级(CLI)', d.scan && d.scan.available ? 'ok' : 'warn');\n $('#adoptScanInfo').textContent = '生成于 ' + fmtTime(d.generatedAt);\n renderAdopt();\n }\n function renderAdopt() {\n var d = state.adopt; if (!d) return;\n var q = state.adoptFilter.toLowerCase();\n var candidates = (d.candidates || []).filter(function (c) {\n if (!q) return true;\n return String(c.slug || '').toLowerCase().includes(q) || String(c.sourceHost || '').toLowerCase().includes(q);\n });\n $('#adoptEmpty').hidden = candidates.length > 0;\n $('#adoptList').innerHTML = candidates.map(function (c) {\n var variants = (c.variants || []).map(function (v) {\n return '<div class=\"variant\"><span class=\"badge\">' + esc(v.host) + '</span><span class=\"mono\">' + esc(v.dir) + '</span><span>v' + esc(v.version || '-') + '</span></div>';\n }).join('');\n return '<div class=\"row\"><input class=\"check\" type=\"checkbox\" data-slug=\"' + esc(c.slug) + '\"' + (state.selected[c.slug] ? ' checked' : '') + (c.inHub ? ' disabled' : '') + '>' +\n '<div class=\"row-main\"><div class=\"row-title\">' + esc(c.slug) + ' <span class=\"badge\">v' + esc(c.version || '-') + '</span>' +\n (c.inHub ? ' <span class=\"badge info\">已在 Hub</span>' : '') +\n (c.conflict ? ' <span class=\"badge warn\">多副本冲突</span>' : '') + '</div>' +\n '<div class=\"row-meta\">来源:' + esc(c.sourceHost || '-') + ' · ' + esc(c.source || '') + '</div>' +\n (c.variants && c.variants.length > 1 ? '<details><summary>' + c.variants.length + ' 个副本</summary><div class=\"variants\">' + variants + '</div></details>' : '') +\n '</div></div>';\n }).join('');\n updateAdoptCount();\n }\n function updateAdoptCount() {\n var n = Object.keys(state.selected).filter(function (k) { return state.selected[k]; }).length;\n $('#adoptCount').textContent = String(n);\n $('#adoptApply').disabled = n === 0;\n }\n $('#adoptSearch').addEventListener('input', function (e) { state.adoptFilter = e.target.value; renderAdopt(); });\n $('#adoptList').addEventListener('change', function (e) {\n var box = e.target.closest('input[data-slug]');\n if (!box) return;\n if (box.checked) state.selected[box.dataset.slug] = true; else delete state.selected[box.dataset.slug];\n updateAdoptCount();\n });\n $('#adoptAll').onclick = function () {\n ((state.adopt && state.adopt.candidates) || []).forEach(function (c) { if (!c.inHub) state.selected[c.slug] = true; });\n renderAdopt();\n };\n $('#adoptNone').onclick = function () { state.selected = {}; renderAdopt(); };\n $('#adoptApply').onclick = function () {\n var slugs = Object.keys(state.selected).filter(function (k) { return state.selected[k]; });\n if (!slugs.length) { toast('还没有选择技能'); return; }\n var preview = slugs.slice(0, 24).join('、') + (slugs.length > 24 ? ' 等 ' + slugs.length + ' 项' : '');\n confirmModal('收编 ' + slugs.length + ' 个技能到 Hub?', '<p class=\"label\">将复制到 Hub(原目录保留);已在 Hub 或冲突项按默认策略处理;执行时逐项运行元信扫描,high / critical 会阻断。</p><div class=\"mono\">' + esc(preview) + '</div>', '开始收编', false, async function () {\n var r = await api('/api/adopt/apply', { body: { include: slugs } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n var order = { imported: 0, blocked: 1, conflict: 2, skip: 3, error: 4 };\n var results = (r.results || []).slice().sort(function (a, b) { return (order[a.status] || 9) - (order[b.status] || 9); });\n $('#adoptResultsWrap').hidden = false;\n $('#adoptResults').innerHTML = results.map(function (x) {\n var tone = x.status === 'imported' ? 'ok' : x.status === 'error' || x.status === 'blocked' ? 'danger' : 'warn';\n return '<div class=\"result\"><span class=\"badge ' + tone + '\">' + esc(x.status) + '</span><b>' + esc(x.slug) + '</b><span>' + esc(x.note || '') + '</span>' + (x.scanVerdict ? '<span class=\"faint\">verdict: ' + esc(x.scanVerdict) + '</span>' : '') + '</div>';\n }).join('');\n toast('收编完成:导入 ' + (r.summary ? r.summary.imported : 0) + ' / 阻断 ' + (r.summary ? r.summary.blocked : 0));\n loadOverview();\n loadAdopt();\n });\n };\n\n async function loadLinks() {\n var results = await Promise.all([api('/api/doctor'), api('/api/links'), api('/api/hosts')]);\n var doctor = results[0], links = results[1], hosts = results[2];\n if (doctor.error || links.error || hosts.error) { toast((doctor.error || links.error || hosts.error)); return; }\n $('#linkDoctor').innerHTML = doctorBanner(doctor);\n var existing = (hosts.hosts || []).filter(function (h) { return h.exists; });\n $('#linkTargets').innerHTML = existing.length ? existing.map(function (h) {\n var scopeBadge = h.bridgeOnly ? '<span class=\"badge info\">桥接·不链接</span>' : h.verified ? '<span class=\"badge ok\">已核实</span>' : '<span class=\"badge warn\">自动发现</span>';\n var action = h.bridgeOnly ? '' : '<button class=\"btn small\" data-link-plan=\"' + esc(h.dir) + '\" data-label=\"' + esc(h.label) + '\">预览链接</button>';\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(h.label) + ' ' + scopeBadge + '</div><div class=\"row-meta mono\">' + esc(h.dir) + '</div><div class=\"row-meta\">目录技能 ' + esc(h.skillCount || 0) + ' · 指向 Hub ' + esc(h.hubLinks || 0) + (h.brokenLinks ? ' · 异常 ' + h.brokenLinks : '') + '</div></div><div class=\"row-actions\">' + action + '</div></div>';\n }).join('') : '<div class=\"empty\">尚未发现可用的宿主技能目录。</div>';\n var rows = links.links || [];\n $('#linkEmpty').hidden = rows.length > 0;\n $('#linkRows').innerHTML = rows.map(function (l) {\n var tone = l.status === 'ok' ? 'ok' : l.status === 'broken' ? 'danger' : 'warn';\n return '<tr><td><div class=\"cell-main\">' + esc(l.slug) + '</div><div class=\"cell-sub\">' + esc(l.label || l.agent || '-') + '</div></td>' +\n '<td class=\"mono\">' + esc(l.dir) + '</td><td class=\"mono\">' + esc(l.target) + '</td>' +\n '<td><span class=\"badge ' + tone + '\">' + esc(l.status) + '</span></td>' +\n '<td><button class=\"btn small danger\" data-unlink=\"' + esc(l.target) + '\" data-dir=\"' + esc(l.dir) + '\" data-slug=\"' + esc(l.slug) + '\">解除</button></td></tr>';\n }).join('');\n }\n $('#linkTargets').addEventListener('click', async function (e) {\n var b = e.target.closest('[data-link-plan]');\n if (!b) return;\n var dir = b.dataset.linkPlan;\n var plan = await api('/api/links/plan?dir=' + encodeURIComponent(dir));\n if (plan.error) { toast(plan.error); return; }\n var would = plan.results.filter(function (r) { return r.status === 'would-link'; });\n var converge = plan.results.filter(function (r) { return r.status === 'would-converge'; });\n var skipped = plan.results.filter(function (r) { return r.status === 'skipped'; });\n var conflicts = plan.results.filter(function (r) { return r.status === 'conflict'; });\n var convergeMoves = converge.reduce(function (n, r) { return n + ((r.moves && r.moves.length) || 0); }, 0);\n var linkable = would.length + converge.length;\n var body = '<p class=\"label\">目标:' + esc(plan.label || '') + ' · <span class=\"mono\">' + esc(dir) + '</span></p>' +\n '<div class=\"result\"><span class=\"badge ok\">将建立</span><b>' + linkable + '</b><span>个链接</span></div>' +\n (converge.length ? '<div class=\"result\"><span class=\"badge warn\">将收敛</span><b>' + convergeMoves + '</b><span>份旧副本 → 回收站(保留 7 天,可恢复):' + esc(converge.map(function (r) { return r.slug; }).join('、')) + '</span></div>' : '') +\n (skipped.length ? '<div class=\"result\"><span class=\"badge warn\">跳过</span><b>' + skipped.length + '</b><span>项(Hub 版本旧于宿主 / 版本无法解析):' + esc(skipped.map(function (r) { return r.slug; }).join('、')) + '</span></div>' : '') +\n (conflicts.length ? '<div class=\"result\"><span class=\"badge warn\">跳过冲突</span><b>' + conflicts.length + '</b><span>个同名目录 / 链接(默认不覆盖)</span></div>' : '') +\n (linkable ? '<details><summary>查看明细</summary><div class=\"variants\">' + would.concat(converge).map(function (r) { return '<div class=\"variant\"><span class=\"mono\">' + esc(r.slug) + '</span></div>'; }).join('') + '</div></details>' : '<p class=\"faint\">没有可建立的链接。</p>');\n openModal('链接预览', body, {\n okText: linkable ? '执行链接' : null,\n onOk: async function () {\n var r = await api('/api/links/apply', { body: { targetDir: dir } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n var done = (r.results || []).filter(function (x) { return x.status === 'linked'; }).length;\n var moved = (r.results || []).reduce(function (n, x) { return n + ((x.moved && x.moved.length) || 0); }, 0);\n toast('已建立 ' + done + ' 个链接' + (moved ? ';收敛 ' + moved + ' 份旧副本(回收站 7 天)' : ''));\n loadLinks();\n },\n });\n });\n $('#linkRows').addEventListener('click', async function (e) {\n var b = e.target.closest('[data-unlink]');\n if (!b) return;\n var target = b.dataset.unlink;\n var dir = b.dataset.dir;\n var slug = b.dataset.slug;\n confirmModal('解除链接 ' + slug + '?', '<p class=\"label\">只删除宿主目录中的链接,Hub 真源保留。目标不是指向 Hub 的链接时会 fail-closed 拒绝。</p><div class=\"mono\">' + esc(target) + '</div>', '解除链接', true, async function () {\n var r = await api('/api/links/remove', { body: { targetDir: dir, slugs: [slug], confirm: 'unlink' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已解除 ' + slug);\n loadLinks();\n });\n });\n\n async function loadRecords() {\n var d = await api('/api/records?limit=200');\n if (d.error) { toast(d.error); return; }\n state.records = d;\n var valid = (d.snapshots || []).filter(function (s) { return s.valid; }).length;\n $('#recordsInfo').textContent = '快照 ' + valid + ' / ' + (d.snapshots || []).length + ' · 证据 ' + (d.evidence.entries || []).length + ' · 审计 ' + (d.audit.entries || []).length;\n renderSnapshots();\n var ev = (d.evidence.entries || []).slice().reverse();\n $('#evidenceEmpty').hidden = ev.length > 0;\n $('#evidenceRows').innerHTML = ev.map(function (x) {\n var tone = x.decision === 'ok' ? 'ok' : x.decision === 'fail' ? 'danger' : 'info';\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(x.event || 'event') + ' · ' + esc(x.skill || x.slug || '-') + '</div><div class=\"row-meta\">' + esc(fmtTime(x.ts || x.at)) + (x.version ? ' · v' + esc(x.version) : '') + (x.via ? ' · via ' + esc(x.via) : '') + '</div></div><span class=\"badge ' + tone + '\">' + esc(x.decision || '记录') + '</span></div>';\n }).join('');\n var aud = (d.audit.entries || []).slice().reverse();\n $('#auditEmpty').hidden = aud.length > 0;\n $('#auditRows').innerHTML = aud.map(function (x) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(x.event || 'audit') + ' · ' + esc(x.slug || '') + '</div><div class=\"row-meta\">' + esc(fmtTime(x.at)) + (x.verdict ? ' · ' + esc(x.verdict) : '') + (x.sourceHost ? ' · ' + esc(x.sourceHost) : '') + '</div></div></div>';\n }).join('');\n }\n function renderSnapshots() {\n var d = state.records; if (!d) return;\n var rows = d.snapshots || [];\n $('#snapshotEmpty').hidden = rows.length > 0;\n $('#snapshotRows').innerHTML = rows.map(function (s) {\n var canRollback = s.valid && s.source;\n return '<tr><td><div class=\"cell-main\">' + esc(s.slug) + '</div><div class=\"cell-sub\">' + esc(s.name) + '</div></td>' +\n '<td>v' + esc(s.version || '-') + '</td><td>' + esc(fmtTime(s.createdAt || s.mtime_ms)) + '</td><td class=\"mono\">' + esc(s.source || '-') + '</td>' +\n '<td>' + (s.valid ? '<span class=\"badge ok\">可用</span>' : '<span class=\"badge danger\">不可用</span>') + (s.legacy ? ' <span class=\"badge warn\">旧格式</span>' : '') + (s.reason ? '<div class=\"cell-sub\">' + esc(s.reason) + '</div>' : '') + '</td>' +\n '<td>' + (canRollback ? '<button class=\"btn small danger\" data-rollback=\"' + esc(s.path) + '\" data-slug=\"' + esc(s.slug) + '\" data-version=\"' + esc(s.version || '') + '\" data-source=\"' + esc(s.source || '') + '\">回滚</button>' : '<span class=\"faint\">CLI 处理</span>') + '</td></tr>';\n }).join('');\n }\n document.querySelectorAll('#view-records [data-tab]').forEach(function (b) {\n b.onclick = function () {\n state.tab = b.dataset.tab;\n Array.prototype.forEach.call(document.querySelectorAll('#view-records [data-tab]'), function (x) { x.classList.toggle('primary', x === b); });\n $('#recSnapshots').hidden = state.tab !== 'snapshots';\n $('#recEvidence').hidden = state.tab !== 'evidence';\n $('#recAudit').hidden = state.tab !== 'audit';\n };\n });\n document.querySelector('#view-records [data-tab=\"snapshots\"]').classList.add('primary');\n $('#snapshotRows').addEventListener('click', function (e) {\n var b = e.target.closest('[data-rollback]');\n if (!b) return;\n var slug = b.dataset.slug, snap = b.dataset.rollback, source = b.dataset.source;\n confirmModal('回滚 ' + slug + ' 到 v' + b.dataset.version + '?', '<p class=\"label\">将用快照覆盖安装来源目录;恢复后自动体检并写入证据。</p><div class=\"mono\">' + esc(source) + '</div>', '执行回滚', true, async function () {\n var r = await api('/api/rollback/apply', { body: { slug: slug, snapshot: snap, confirm: 'rollback' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast(r.ok ? '回滚完成(doctor 通过)' : '回滚完成,但体检未通过');\n loadRecords();\n loadOverview();\n });\n });\n\n $('#routeBtn').onclick = runRoute;\n $('#routeInput').addEventListener('keydown', function (e) { if (e.key === 'Enter') runRoute(); });\n async function runRoute() {\n var request = $('#routeInput').value.trim();\n if (!request) { toast('先输入一句需求'); return; }\n var d = await api('/api/route?request=' + encodeURIComponent(request));\n if (d.error) { toast(d.error); return; }\n var skills = (d.skills || []).map(function (s) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + s.order + '. ' + esc(s.slug) + ' <span class=\"badge ' + (s.installed ? 'ok' : 'warn') + '\">' + esc(s.installed ? '已装' + (s.version ? ' v' + s.version : '') : '缺失') + '</span></div><div class=\"row-meta\">' + esc(s.role || '') + '</div></div></div>';\n }).join('');\n var missing = (d.missing_skills || []).length\n ? '<div class=\"section panel\"><h3>缺失技能</h3><div class=\"toolbar\"><span class=\"mono\">' + esc((d.missing_skills || []).map(function (s) { return s.slug; }).join(', ')) + '</span><button class=\"btn small\" data-copy-cmd=\"' + esc(d.install_command || '') + '\">复制安装命令</button></div><div class=\"note\">安装与信任判断由你决定;元信装前扫描默认启用。</div></div>'\n : '';\n var others = (d.other_skill_candidates || []).length\n ? '<div class=\"section panel\"><h3>其他已装技能候选</h3>' + d.other_skill_candidates.map(function (c) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(c.slug) + ' <span class=\"badge info\">v' + esc(c.version) + '</span></div><div class=\"row-meta\">' + esc((c.matched_terms || []).join('、')) + ' · ' + esc(c.description || '') + '</div></div></div>';\n }).join('') + '<div class=\"note\">只读 frontmatter 机械匹配;使用前请自行确认。</div></div>'\n : '';\n $('#routeResult').innerHTML = '<div class=\"panel\"><div class=\"shead\"><h2>' + esc(d.playbook.name) + '</h2><span class=\"badge\">置信度 ' + esc(d.confidence) + '</span></div>' +\n '<div class=\"label\">' + esc(d.playbook.intent) + '</div><div class=\"list\">' + skills + '</div>' +\n '<div class=\"note\">动态路由:' + esc((d.dynamic && d.dynamic.status) || '未启用') + ' · 匹配词:' + esc((d.matched_keywords || []).join('、') || '无') + '</div></div>' + missing + others;\n }\n $('#routeResult').addEventListener('click', function (e) {\n var b = e.target.closest('[data-copy-cmd]');\n if (b) copyText(b.dataset.copyCmd);\n });\n\n var CLI_GROUPS = [\n { name: 'Hub 发现与安装', items: [\n { cmd: 'yotta-skills hub hosts', why: '发现本机宿主与技能目录' },\n { cmd: 'yotta-skills hub install', why: '把元阁家族装进 Hub 真源' },\n { cmd: 'yotta-skills hub adopt --scan', why: '只读预演收编候选与冲突' },\n { cmd: 'yotta-skills hub adopt --apply', why: '执行收编(默认复制保真)' },\n ] },\n { name: '链接与体检', items: [\n { cmd: 'yotta-skills hub link --all', why: '链接到已核实宿主(默认范围)' },\n { cmd: 'yotta-skills hub link --all --include-discovered', why: '显式纳入自动发现目录' },\n { cmd: 'yotta-skills hub link --agent codex', why: '只链接到一个宿主' },\n { cmd: 'yotta-skills hub unlink --all', why: '只删链接,不动 Hub 真源' },\n { cmd: 'yotta-skills hub status', why: '查看技能 / 来源 / 链接' },\n { cmd: 'yotta-skills hub doctor', why: '检查断链 / 缺失 / slug 不一致' },\n ] },\n { name: '安装与更新', items: [\n { cmd: 'yotta-skills update --installed-only --dir <技能目录>', why: '只维护已装元技能' },\n { cmd: 'yotta-skills update --check', why: '只读检查更新' },\n { cmd: 'yotta-skills rollback --list', why: '查看可回滚快照' },\n { cmd: 'yotta-skills rollback --slug <技能> --dir <技能目录>', why: '回滚单个技能' },\n ] },\n { name: '路由与使用', items: [\n { cmd: 'yotta-skills --route \"帮我做发布前质量检查\"', why: '静态编排路由 + 动态路由块' },\n { cmd: 'yotta-skills --list', why: '列出全家技能与版本' },\n { cmd: 'yotta-skills --inventory --json', why: '盘点本机已装技能' },\n ] },\n ];\n $('#cliGroups').innerHTML = CLI_GROUPS.map(function (g) {\n return '<div class=\"panel cmd-group\"><h3>' + esc(g.name) + '</h3>' + g.items.map(function (item) {\n return '<div class=\"cmd\"><code>' + esc(item.cmd) + '</code><span class=\"why\">' + esc(item.why) + '</span><button class=\"btn small\" data-copy-cmd=\"' + esc(item.cmd) + '\">复制</button></div>';\n }).join('') + '</div>';\n }).join('');\n document.addEventListener('click', function (e) {\n var b = e.target.closest('#cliGroups [data-copy-cmd]');\n if (b) copyText(b.dataset.copyCmd);\n });\n\n (async function boot() {\n var s = await api('/api/status');\n if (s && !s.error) $('#verLine').textContent = 'v' + s.version + ' · ' + (s.standard || '');\n else $('#verLine').textContent = '未连接本地服务';\n loadOverview();\n })();\n})();\n</script>\n</body>\n</html>\n";
63
63
  // @generated view-html:end
64
64
 
65
65
  function loadManifest() {
@@ -159,7 +159,7 @@ function parseArgs(argv) {
159
159
  host: null, event: null, manifest: null, context: null,
160
160
  signal: null, yes: false, explain: false, promote: false,
161
161
  only: [], domain: null, installedOnly: false,
162
- hub: null, all: false, inPlace: false, allowUnverified: false,
162
+ hub: null, all: false, includeDiscovered: false, inPlace: false, allowUnverified: false,
163
163
  from: null, include: [], as: null, skillsCli: null,
164
164
  scan: false, apply: false,
165
165
  port: null,
@@ -214,6 +214,7 @@ function parseArgs(argv) {
214
214
  else if (a === '--installed-only') opts.installedOnly = true;
215
215
  else if (a === '--hub') opts.hub = take('--hub');
216
216
  else if (a === '--all') opts.all = true;
217
+ else if (a === '--include-discovered') opts.includeDiscovered = true;
217
218
  else if (a === '--in-place') opts.inPlace = true;
218
219
  else if (a === '--scan') opts.scan = true;
219
220
  else if (a === '--apply') opts.apply = true;
@@ -279,10 +280,10 @@ function parseArgs(argv) {
279
280
  if (opts.port !== null && opts.command !== 'view') {
280
281
  die('--port 只能与 view 一起使用', 2, '示例:yotta-skills view --port 8789。');
281
282
  }
282
- if ((opts.all || opts.inPlace || opts.allowUnverified || opts.from || opts.include.length || opts.as || opts.scan || opts.apply)
283
+ if ((opts.all || opts.includeDiscovered || opts.inPlace || opts.allowUnverified || opts.from || opts.include.length || opts.as || opts.scan || opts.apply)
283
284
  && opts.command !== 'hub') {
284
- die('--all / --in-place / --allow-unverified / --from / --include / --as / --scan / --apply 只能与 hub 命令一起使用', 2,
285
- 'Hub 用法:yotta-skills hub link --all 或 yotta-skills hub adopt --scan。');
285
+ die('--all / --include-discovered / --in-place / --allow-unverified / --from / --include / --as / --scan / --apply 只能与 hub 命令一起使用', 2,
286
+ 'Hub 用法:yotta-skills hub link --all --include-discovered 或 yotta-skills hub adopt --scan。');
286
287
  }
287
288
  if (opts.domain && !domainValues().includes(opts.domain)) {
288
289
  die('未知 domain: ' + opts.domain + '。可用: ' + domainValues().join(', '), 2, 'domain 对齐家族索引 9 类;用 yotta-skills --list 查看技能清单。');
@@ -1357,13 +1358,14 @@ function printHelp() {
1357
1358
  out(' yotta-skills hook evaluate --event <event> --manifest <file> 评估 hook 声明并留证');
1358
1359
  out(' yotta-skills hook bind --manifest <file> 注册 hook 声明(幂等)');
1359
1360
  out(' yotta-skills hook unbind <binding-id> 反注册 hook 声明');
1360
- out(' yotta-skills hub hosts 发现本机已装智能体与技能目录(不读元忆)');
1361
+ out(' yotta-skills hub hosts 发现本机已装智能体与技能目录(分类:已核实 / 自动发现 / 桥接)');
1361
1362
  out(' yotta-skills hub install [skill...] 把元技能装进本机 Hub(清单 27 + 特殊家族 5;单点真源)');
1362
1363
  out(' yotta-skills hub update 更新 Hub 内的元技能(非元技能不参与,用户自行处理)');
1363
1364
  out(' yotta-skills hub adopt --scan 只读预演:扫描各宿主现有技能,输出收编候选与冲突');
1364
1365
  out(' yotta-skills hub adopt --apply 把选中技能复制进 Hub(默认保留原目录;--in-place 原地登记)');
1365
1366
  out(' yotta-skills hub refresh <slug> --from <path> 手动同步非元阁技能(无统一更新源)');
1366
- out(' yotta-skills hub link --all 把 Hub 技能链接到全部已发现宿主;元技能链接时收敛旧副本(回收站 7 天)');
1367
+ out(' yotta-skills hub link --all 链接到全部已核实宿主;元技能链接时收敛旧副本(回收站 7 天)');
1368
+ out(' yotta-skills hub link --all --include-discovered 显式把自动发现目录一并纳入链接范围');
1367
1369
  out(' yotta-skills hub link --agent <id> 只链接到一个宿主(未知宿主用 --dir)');
1368
1370
  out(' yotta-skills hub unlink --all 只删除链接,不动 Hub 真源(fail-closed)');
1369
1371
  out(' yotta-skills hub status 查看 Hub 技能 / 来源 / 链接 / 宿主');
@@ -1402,7 +1404,8 @@ function printHelp() {
1402
1404
  out(' --promote decide-memory 只写本地建议文件(不写元忆)');
1403
1405
  out(' --hub <path> Hub 目录(默认 ~/.yottaskills/hub;YOTTA_SKILLS_HUB 覆盖)');
1404
1406
  out(' --port <n> view 面板端口(默认 8789;仅 127.0.0.1 监听)');
1405
- out(' --all hub link / unlink 时作用于全部已发现宿主');
1407
+ out(' --all hub link / unlink 时作用于全部已核实宿主(默认范围)');
1408
+ out(' --include-discovered hub link / unlink --all 时纳入自动发现目录(默认不链)');
1406
1409
  out(' --scan hub adopt 只读预演(默认行为)');
1407
1410
  out(' --apply hub adopt 执行收编');
1408
1411
  out(' --in-place hub adopt 原地登记,不复制(原目录删除即断链)');
@@ -1881,20 +1884,59 @@ function universalSkillDirs() {
1881
1884
  ];
1882
1885
  }
1883
1886
 
1887
+ function normalizeTargetKey(dir) {
1888
+ const resolved = path.resolve(dir);
1889
+ let real = resolved;
1890
+ try {
1891
+ real = fs.realpathSync.native(resolved);
1892
+ } catch (_) { /* missing path: fall back to the resolved spelling */ }
1893
+ return process.platform === 'win32' ? real.toLowerCase() : real;
1894
+ }
1895
+
1896
+ function hubScopeSummary(discovery) {
1897
+ const existing = (discovery.hosts || []).filter((host) => host.exists);
1898
+ return {
1899
+ verified: existing.filter((host) => host.verified && !host.bridgeOnly).length,
1900
+ discovered: existing.filter((host) => !host.verified && !host.bridgeOnly).length,
1901
+ bridge: existing.filter((host) => host.bridgeOnly).length,
1902
+ };
1903
+ }
1904
+
1905
+ function hubScopeLine(opts, discovery) {
1906
+ const scope = hubScopeSummary(discovery);
1907
+ return '范围: 已核实 ' + scope.verified + ' 个目标' +
1908
+ (opts.includeDiscovered
1909
+ ? '(含自动发现 ' + scope.discovered + ' 个)'
1910
+ : ';跳过自动发现 ' + scope.discovered + ' 个(--include-discovered 纳入)') +
1911
+ ';跳过桥接 ' + scope.bridge + ' 个(锁 / 数据目录永不链接)';
1912
+ }
1913
+
1884
1914
  function hubTargetDirs(opts, discovery) {
1885
1915
  const targets = [];
1886
1916
  const seen = new Set();
1887
- const add = (dir, agentId, label) => {
1917
+ const add = (dir, agentId, label, meta) => {
1888
1918
  if (!dir) return;
1889
1919
  const resolved = path.resolve(dir);
1890
- const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
1920
+ const key = normalizeTargetKey(resolved);
1891
1921
  if (seen.has(key)) return;
1892
1922
  seen.add(key);
1893
- targets.push({ dir: resolved, agentId: agentId || null, label: label || '指定目录' });
1923
+ targets.push({
1924
+ dir: resolved,
1925
+ agentId: agentId || null,
1926
+ label: label || '指定目录',
1927
+ verified: Boolean(meta && meta.verified),
1928
+ bridgeOnly: Boolean(meta && meta.bridgeOnly),
1929
+ detection: (meta && meta.detection) || 'mapping',
1930
+ explicit: Boolean(meta && meta.explicit),
1931
+ });
1894
1932
  };
1895
1933
 
1896
1934
  if (opts.dir) {
1897
- add(opts.dir, null, '指定目录');
1935
+ if (agentDirsLib.isBridgeOnlyDir(opts.dir, { homeDir: os.homedir(), env: process.env })) {
1936
+ die('该目录是锁 / 数据桥接目录(XDG_STATE_HOME/skills 或 XDG_DATA_HOME/skills),永不作为链接目标', 2,
1937
+ '如需链接真实宿主目录,请改用 --agent <id> 或其它 --dir 路径。');
1938
+ }
1939
+ add(opts.dir, null, '指定目录', { explicit: true, verified: true });
1898
1940
  return targets;
1899
1941
  }
1900
1942
  if (opts.agent) {
@@ -1903,7 +1945,7 @@ function hubTargetDirs(opts, discovery) {
1903
1945
  die('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';或改用 --dir <路径>。',
1904
1946
  2, '先运行 yotta-skills hub hosts 查看本机实际技能目录。');
1905
1947
  }
1906
- add(resolveUserDir(info.dirs[0]), opts.agent, info.label);
1948
+ add(resolveUserDir(info.dirs[0]), opts.agent, info.label, { explicit: true, verified: info.verified !== false });
1907
1949
  return targets;
1908
1950
  }
1909
1951
  if (!opts.all) {
@@ -1911,10 +1953,12 @@ function hubTargetDirs(opts, discovery) {
1911
1953
  '示例:yotta-skills hub link --all;或 yotta-skills hub link --agent codex。');
1912
1954
  }
1913
1955
 
1914
- for (const item of universalSkillDirs()) add(item.dir, item.agentId, item.label);
1956
+ for (const item of universalSkillDirs()) add(item.dir, item.agentId, item.label, { verified: true });
1915
1957
  for (const host of discovery.hosts) {
1916
1958
  if (!host.exists) continue;
1917
- add(host.dir, host.agentId, host.label);
1959
+ if (host.bridgeOnly) continue;
1960
+ if (!opts.includeDiscovered && !host.verified) continue;
1961
+ add(host.dir, host.agentId, host.label, host);
1918
1962
  }
1919
1963
  return targets;
1920
1964
  }
@@ -1924,10 +1968,16 @@ function hubHostLines(discovery) {
1924
1968
  const existing = discovery.hosts.filter((host) => host.exists);
1925
1969
  const missing = discovery.hosts.filter((host) => !host.exists);
1926
1970
  for (const host of existing) {
1927
- lines.push(' ' + (host.known ? '✔' : '?') + ' ' +
1971
+ const marker = host.bridgeOnly ? '·' : host.verified ? '✔' : '?';
1972
+ const scope = host.bridgeOnly
1973
+ ? ' [桥接·不链接]'
1974
+ : host.verified
1975
+ ? ' [已核实]'
1976
+ : host.known ? ' [未核实·默认不链]' : ' [自动发现·默认不链]';
1977
+ lines.push(' ' + marker + ' ' +
1928
1978
  String(host.label).padEnd(28) + host.dir.padEnd(64) +
1929
1979
  String(host.skillCount).padStart(3) + ' 个技能' +
1930
- (host.known ? ' [已收录]' : ' [自动发现]'));
1980
+ scope);
1931
1981
  }
1932
1982
  const noSkillDir = discovery.installed.filter((item) => !item.hasSkillsDir);
1933
1983
  for (const item of noSkillDir) {
@@ -1942,9 +1992,12 @@ function printHubHosts(discovery, json) {
1942
1992
  return;
1943
1993
  }
1944
1994
  const view = hubHostLines(discovery);
1995
+ const scope = hubScopeSummary(discovery);
1945
1996
  out('yotta-skills(元阁)v' + VERSION + ' —— Hub 宿主发现(文件系统优先,不读元忆)');
1946
- out('本机发现技能目录 ' + view.existing.length + ' 个 / 已装应用标记 ' + discovery.installed.length +
1997
+ out('本机发现技能目录 ' + view.existing.length + ' 个(已核实 ' + scope.verified +
1998
+ ' / 自动发现 ' + scope.discovered + ' / 桥接 ' + scope.bridge + ')/ 已装应用标记 ' + discovery.installed.length +
1947
1999
  ' 个 / 未发现技能目录 ' + view.noSkillDir.length + ' 个');
2000
+ out('默认链接范围 = 已核实目录;--include-discovered 显式纳入自动发现目录;锁 / 数据桥接目录永不链接。');
1948
2001
  for (const line of view.lines) out(line);
1949
2002
  if (view.missing.length > 0) {
1950
2003
  out('');
@@ -2132,6 +2185,7 @@ function runHub(opts) {
2132
2185
 
2133
2186
  if (action === 'link') {
2134
2187
  const targets = hubTargetDirs(opts, discovery);
2188
+ if (opts.all && !opts.json) out(hubScopeLine(opts, discovery));
2135
2189
  let failed = 0;
2136
2190
  const targetPayloads = [];
2137
2191
  for (const target of targets) {
@@ -2177,6 +2231,7 @@ function runHub(opts) {
2177
2231
 
2178
2232
  if (action === 'unlink') {
2179
2233
  const targets = hubTargetDirs(opts, discovery);
2234
+ if (opts.all && !opts.json) out(hubScopeLine(opts, discovery));
2180
2235
  let failed = 0;
2181
2236
  const targetPayloads = [];
2182
2237
  for (const target of targets) {
@@ -2217,6 +2272,7 @@ function runHub(opts) {
2217
2272
  homeDir: os.homedir(),
2218
2273
  env: process.env,
2219
2274
  discovery,
2275
+ linkScopeDirs: hubTargetDirs({ all: true }, discovery).map((target) => target.dir),
2220
2276
  });
2221
2277
  printHubDoctor(payload, opts.json);
2222
2278
  if (!payload.ok) process.exitCode = 1;
@@ -40,7 +40,7 @@ const AGENT_DIRS = {
40
40
  fx: { label: 'fx', dirs: ['.fx/skills'] },
41
41
  gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
42
42
  copilot: { label: 'GitHub Copilot', dirs: ['.copilot/skills', '.agents/skills'] },
43
- goose: { label: 'Goose', dirs: ['.config/goose/skills', '.goose/skills'] },
43
+ goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
44
44
  grok: { label: 'Grok Build', dirs: ['.grok/skills'] },
45
45
  hermes: { label: 'Hermes Agent', dirs: ['.hermes/skills'] },
46
46
  'inference-sh': { label: 'inference.sh', dirs: ['.inferencesh/skills'] },
@@ -54,6 +54,7 @@ const AGENT_DIRS = {
54
54
  kode: { label: 'Kode', dirs: ['.kode/skills'] },
55
55
  lingma: { label: 'Lingma', dirs: ['.lingma/skills'] },
56
56
  mcpjam: { label: 'MCPJam', dirs: ['.mcpjam/skills'] },
57
+ mimocode: { label: 'MiMo Code', dirs: ['.config/mimocode/skills'] },
57
58
  minimax: { label: 'MiniMax Code', dirs: ['.minimax/skills'] },
58
59
  'mistral-vibe': { label: 'Mistral Vibe', dirs: ['.vibe/skills'] },
59
60
  moxby: { label: 'Moxby', dirs: ['.moxby/skills'] },
@@ -68,6 +69,7 @@ const AGENT_DIRS = {
68
69
  'qoder-cn': { label: 'Qoder CN', dirs: ['.qoder-cn/skills'] },
69
70
  qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
70
71
  reasonix: { label: 'Reasonix', dirs: ['.reasonix/skills'] },
72
+ replit: { label: 'Replit', dirs: ['.config/agents/skills'] },
71
73
  rovodev: { label: 'Rovo Dev', dirs: ['.rovodev/skills'] },
72
74
  roo: { label: 'Roo Code', dirs: ['.roo/skills'] },
73
75
  'sarvam-code': { label: 'Sarvam Code', dirs: ['.sarvam/skills', '.agents/skills'] },
@@ -85,14 +87,30 @@ const AGENT_DIRS = {
85
87
  pochi: { label: 'Pochi', dirs: ['.pochi/skills'] },
86
88
  adal: { label: 'AdaL', dirs: ['.adal/skills'] },
87
89
  dsh: { label: 'DSH / DeepSeek Harness', dirs: ['.dsh/skills'] },
88
- yottacode: { label: 'YottaCode', dirs: ['.yottacode/skills'] },
89
- box: { label: 'Box Agent', dirs: ['.box-agent/skills'] },
90
+ yottacode: { label: 'YottaCode', dirs: ['.yottacode/skills'], verified: false },
91
+ box: { label: 'Box Agent', dirs: ['.box-agent/skills'], verified: false },
90
92
  lmstudio: { label: 'LM Studio', dirs: ['.lmstudio/skills'] },
91
93
  ccswitch: { label: 'CC Switch', dirs: ['.cc-switch/skills'] },
92
94
  agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
93
- universal: { label: 'Universal .agents', dirs: ['.agents/skills'] },
95
+ universal: { label: 'Universal .agents', dirs: ['.config/agents/skills', '.agents/skills'] },
94
96
  };
95
97
 
98
+ /**
99
+ * Rel paths that follow XDG_CONFIG_HOME when it is set.
100
+ *
101
+ * Verified against the upstream Vercel Labs `skills` CLI v1.7.0 table
102
+ * (2026-10-03): only amp / universal / replit (`configHome/agents/skills`),
103
+ * devin (`configHome/devin/skills`) and opencode (`configHome/opencode/skills`)
104
+ * resolve through the config home. Everything else uses the literal
105
+ * `~/.config/...` path even when XDG_CONFIG_HOME is set (crush / kimchi per the
106
+ * upstream table; goose follows the machine-verified `~/.config/goose`).
107
+ */
108
+ const XDG_CONFIG_RELS = new Set([
109
+ '.config/agents/skills',
110
+ '.config/devin/skills',
111
+ '.config/opencode/skills',
112
+ ]);
113
+
96
114
  function envPath(env, key) {
97
115
  const value = env && env[key];
98
116
  return value ? String(value) : null;
@@ -119,8 +137,11 @@ function resolveUserDir(rel, options) {
119
137
  return path.join(envPath(env, 'CODEX_HOME') || path.join(home, '.codex'), 'skills');
120
138
  }
121
139
  if (rel.startsWith('.config/')) {
122
- const base = envPath(env, 'XDG_CONFIG_HOME') || path.join(home, '.config');
123
- return path.join(base, ...rel.slice('.config/'.length).split('/'));
140
+ const xdg = envPath(env, 'XDG_CONFIG_HOME');
141
+ if (xdg && XDG_CONFIG_RELS.has(rel)) {
142
+ return path.join(xdg, ...rel.slice('.config/'.length).split('/'));
143
+ }
144
+ return path.join(home, rel);
124
145
  }
125
146
  if (rel === '.openclaw/skills') {
126
147
  return path.join(envPath(env, 'OPENCLAW_STATE_DIR') || path.join(home, '.openclaw'), 'skills');
@@ -150,16 +171,16 @@ function resolveUserDir(rel, options) {
150
171
  function knownRoots(options) {
151
172
  const roots = [];
152
173
  const seen = new Set();
153
- const add = (dir, agentId, label) => {
174
+ const add = (dir, agentId, label, verified) => {
154
175
  if (!dir) return;
155
176
  const resolved = path.resolve(dir);
156
177
  if (seen.has(resolved)) return;
157
178
  seen.add(resolved);
158
- roots.push({ dir: resolved, agentId, label, known: true });
179
+ roots.push({ dir: resolved, agentId, label, known: true, verified: verified !== false });
159
180
  };
160
181
  for (const [agentId, info] of Object.entries(AGENT_DIRS)) {
161
182
  for (const rel of info.dirs) {
162
- add(resolveUserDir(rel, options), agentId, info.label);
183
+ add(resolveUserDir(rel, options), agentId, info.label, info.verified !== false);
163
184
  }
164
185
  }
165
186
  return roots;
@@ -169,15 +190,25 @@ function knownRoots(options) {
169
190
  function envRoots(options) {
170
191
  const env = envObject(options);
171
192
  const roots = [];
172
- const add = (dir, agentId, label) => {
193
+ const add = (dir, agentId, label, extra) => {
173
194
  if (!dir) return;
174
- roots.push({ dir: path.resolve(dir), agentId: agentId || null, label: label || '环境变量指定目录', known: false, env: true });
195
+ roots.push({
196
+ dir: path.resolve(dir),
197
+ agentId: agentId || null,
198
+ label: label || '环境变量指定目录',
199
+ known: false,
200
+ env: true,
201
+ verified: true,
202
+ ...(extra || {}),
203
+ });
175
204
  };
176
205
 
177
206
  add(envPath(env, 'CODEX_HOME') && path.join(env.CODEX_HOME, 'skills'), 'codex', 'Codex(CODEX_HOME)');
178
207
  add(envPath(env, 'XDG_CONFIG_HOME') && path.join(env.XDG_CONFIG_HOME, 'opencode', 'skills'), 'opencode', 'OpenCode(XDG_CONFIG_HOME)');
179
- add(envPath(env, 'XDG_STATE_HOME') && path.join(env.XDG_STATE_HOME, 'skills'), null, 'XDG_STATE_HOME/skills');
180
- add(envPath(env, 'XDG_DATA_HOME') && path.join(env.XDG_DATA_HOME, 'skills'), null, 'XDG_DATA_HOME/skills');
208
+ add(envPath(env, 'XDG_STATE_HOME') && path.join(env.XDG_STATE_HOME, 'skills'), null,
209
+ 'XDG_STATE_HOME/skills(锁桥接,不链接)', { bridgeOnly: true });
210
+ add(envPath(env, 'XDG_DATA_HOME') && path.join(env.XDG_DATA_HOME, 'skills'), null,
211
+ 'XDG_DATA_HOME/skills(数据桥接,不链接)', { bridgeOnly: true });
181
212
  add(envPath(env, 'DSH_HOME') && path.join(env.DSH_HOME, 'skills'), 'dsh', 'DSH(DSH_HOME)');
182
213
  add(envPath(env, 'DSH_AGENTS_HOME') && path.join(env.DSH_AGENTS_HOME, 'skills'), 'dsh', 'DSH Agents(DSH_AGENTS_HOME)');
183
214
  add(envPath(env, 'OPENCLAW_STATE_DIR') && path.join(env.OPENCLAW_STATE_DIR, 'skills'), 'openclaw', 'OpenClaw(OPENCLAW_STATE_DIR)');
@@ -188,14 +219,40 @@ function envRoots(options) {
188
219
  .split(path.delimiter)
189
220
  .map((item) => item.trim())
190
221
  .filter(Boolean);
191
- for (const dir of extra) add(dir, null, 'YOTTA_SKILLS_DISCOVERY_ROOTS');
222
+ for (const dir of extra) add(dir, null, 'YOTTA_SKILLS_DISCOVERY_ROOTS', { verified: false });
192
223
 
193
224
  return roots;
194
225
  }
195
226
 
227
+ /**
228
+ * Lock / data bridge directories: the official `skills` CLI keeps its lock in
229
+ * `$XDG_STATE_HOME/skills`, and `$XDG_DATA_HOME/skills` is a data bridge.
230
+ * Neither is a host skill-loading directory, so both are never link targets.
231
+ */
232
+ function bridgeOnlyDirs(options) {
233
+ const env = envObject(options);
234
+ const dirs = [];
235
+ if (envPath(env, 'XDG_STATE_HOME')) dirs.push(path.join(env.XDG_STATE_HOME, 'skills'));
236
+ if (envPath(env, 'XDG_DATA_HOME')) dirs.push(path.join(env.XDG_DATA_HOME, 'skills'));
237
+ return dirs.map((dir) => path.resolve(dir));
238
+ }
239
+
240
+ function isBridgeOnlyDir(dir, options) {
241
+ if (!dir) return false;
242
+ const target = path.resolve(dir);
243
+ for (const bridge of bridgeOnlyDirs(options)) {
244
+ const relative = path.relative(bridge, target);
245
+ if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) return true;
246
+ }
247
+ return false;
248
+ }
249
+
196
250
  module.exports = {
197
251
  AGENT_DIRS,
252
+ XDG_CONFIG_RELS,
198
253
  resolveUserDir,
199
254
  knownRoots,
200
255
  envRoots,
256
+ bridgeOnlyDirs,
257
+ isBridgeOnlyDir,
201
258
  };
@@ -101,6 +101,8 @@ function addRoot(list, seen, dir, meta) {
101
101
  if (meta && meta.known && existing) existing.known = true;
102
102
  if (meta && meta.agentId && !existing.agentId) existing.agentId = meta.agentId;
103
103
  if (meta && meta.label && existing.label === '自动发现目录') existing.label = meta.label;
104
+ if (meta && meta.verified) existing.verified = true;
105
+ if (meta && meta.bridgeOnly) existing.bridgeOnly = true;
104
106
  return;
105
107
  }
106
108
  const record = {
@@ -109,6 +111,8 @@ function addRoot(list, seen, dir, meta) {
109
111
  label: (meta && meta.label) || '自动发现目录',
110
112
  known: Boolean(meta && meta.known),
111
113
  env: Boolean(meta && meta.env),
114
+ verified: Boolean(meta && meta.verified),
115
+ bridgeOnly: Boolean(meta && meta.bridgeOnly),
112
116
  detection: (meta && meta.detection) || 'mapping',
113
117
  exists: isDirectory(resolved),
114
118
  };
@@ -169,8 +173,8 @@ function commonRoots(options) {
169
173
  add(env.ProgramFiles, 2);
170
174
  add(env['ProgramFiles(x86)'], 2);
171
175
  add(env.XDG_CONFIG_HOME, 3);
172
- add(env.XDG_STATE_HOME, 3);
173
- add(env.XDG_DATA_HOME, 3);
176
+ // XDG_STATE_HOME / XDG_DATA_HOME are lock / data bridges, not skill roots:
177
+ // never scan them for link targets (see agent-dirs envRoots bridgeOnly).
174
178
  add(env.DSH_HOME, 3);
175
179
  add(env.DSH_AGENTS_HOME, 3);
176
180
  add(env.OPENCLAW_STATE_DIR, 2);
@@ -212,6 +212,9 @@ function hostsPayload(ctx) {
212
212
  summary: {
213
213
  hosts: hosts.length,
214
214
  existing: hosts.filter((host) => host.exists).length,
215
+ verified: hosts.filter((host) => host.exists && host.verified && !host.bridgeOnly).length,
216
+ discovered: hosts.filter((host) => host.exists && !host.verified && !host.bridgeOnly).length,
217
+ bridge: hosts.filter((host) => host.exists && host.bridgeOnly).length,
215
218
  installedMarks: found.installed.length,
216
219
  links: links.length,
217
220
  brokenLinks: links.filter((link) => link.status !== 'ok').length,
@@ -245,6 +248,9 @@ function linkPlanPayload(ctx, targetDir) {
245
248
  if (!host) {
246
249
  return { error: '目标目录不在已发现的宿主目录中;请先在宿主矩阵中确认该目录。', code: 400 };
247
250
  }
251
+ if (host.bridgeOnly) {
252
+ return { error: '该目录是锁 / 数据桥接目录(XDG_STATE_HOME/skills 或 XDG_DATA_HOME/skills),永不作为链接目标。', code: 400 };
253
+ }
248
254
  const result = hubLib.linkSkills({
249
255
  hubDir: ctx.hubDir,
250
256
  targetDir: host.dir,
@@ -407,6 +413,9 @@ function writeLink(ctx, body) {
407
413
  const targetDir = String(body.targetDir || '');
408
414
  const host = found.hosts.find((item) => sameDir(item.dir, targetDir));
409
415
  if (!host) return { code: 400, payload: { error: '目标目录不在已发现的宿主目录中。' } };
416
+ if (host.bridgeOnly) {
417
+ return { code: 400, payload: { error: '该目录是锁 / 数据桥接目录(XDG_STATE_HOME/skills 或 XDG_DATA_HOME/skills),永不作为链接目标。' } };
418
+ }
410
419
  if (body.force) {
411
420
  return {
412
421
  code: 400,
@@ -154,6 +154,11 @@ function samePath(left, right) {
154
154
  return a === b;
155
155
  }
156
156
 
157
+ function pathKey(dir) {
158
+ const real = safeRealpath(dir);
159
+ return process.platform === 'win32' ? real.toLowerCase() : real;
160
+ }
161
+
157
162
  function isInside(root, target) {
158
163
  const base = safeRealpath(root);
159
164
  const candidate = safeRealpath(target);
@@ -1164,6 +1169,23 @@ function doctor(options) {
1164
1169
  link.status === 'ok' ? null : '运行 hub link --all 重建,或 hub unlink 清理断链');
1165
1170
  }
1166
1171
 
1172
+ const scopeDirs = Array.isArray(opts.linkScopeDirs) ? opts.linkScopeDirs : null;
1173
+ if (scopeDirs && scopeDirs.length > 0) {
1174
+ const scope = new Set(scopeDirs.map((dir) => pathKey(dir)));
1175
+ const outside = new Map();
1176
+ for (const link of links) {
1177
+ if (!link.dir) continue;
1178
+ if (scope.has(pathKey(link.dir))) continue;
1179
+ if (!outside.has(link.dir)) outside.set(link.dir, []);
1180
+ outside.get(link.dir).push(link.slug);
1181
+ }
1182
+ for (const [dir, slugs] of outside) {
1183
+ add('link_scope:' + dir, false, 'warning',
1184
+ '链接位于默认范围之外: ' + dir + '(' + slugs.length + ' 条)',
1185
+ '默认范围 = 已核实宿主;如需保留请用 hub link --dir 或 --include-discovered 显式重建,否则用 hub unlink --dir "' + dir + '" 清理');
1186
+ }
1187
+ }
1188
+
1167
1189
  try {
1168
1190
  singleSourceChecks(opts, add);
1169
1191
  } catch (_) { /* 只读检查失败不影响 doctor 主流程 */ }
@@ -34,6 +34,23 @@ symlink),升级只改真源,已链接宿主即时生效。
34
34
  落地后跑一次 `hub link --all` 即可完成现存收敛。`hub doctor` 的
35
35
  `single_source:*` 检查只读报告宿主中的多份副本与版本参差,不自动修改。
36
36
 
37
+ ## 链接范围(0.28.0 起)
38
+
39
+ `hub hosts` 把发现的目录分为三类,`hub link --all` / `hub unlink --all` 的
40
+ **默认范围只含已核实宿主**:
41
+
42
+ - **已核实(verified)**:映射经公开宿主表(Vercel Labs `skills@1.7.0`)、官方
43
+ 文档或本机自证(应用数据 / 配置 / 管理痕迹)核实 —— 默认链接;
44
+ - **自动发现(discovered)**:文件系统启发式扫到的目录 —— 默认不链,
45
+ `--include-discovered` 显式纳入;
46
+ - **桥接(bridge)**:`XDG_STATE_HOME/skills`(官方 skills CLI 锁目录)与
47
+ `XDG_DATA_HOME/skills`(数据桥接)—— 永不作为链接目标,`--dir` 指向时
48
+ 也会被拒绝(fail-closed)。
49
+
50
+ 未核实映射(如无文档 / 无自证的宿主)默认不链,可用 `--dir <目录>` 显式指定。
51
+ `hub doctor` 的 `link_scope:*` 检查只读报告「位于默认范围之外」的已建链接,
52
+ 并给出 `hub unlink --dir <目录>` 的清理提示;`hub hosts` 输出三类的本机计数。
53
+
37
54
  ## 命令
38
55
 
39
56
  ```bash
@@ -55,11 +72,14 @@ npx -y @yottameta/yotta-skills hub adopt --apply
55
72
  # 非元阁来源技能没有统一更新源:手动从指定目录刷新
56
73
  npx -y @yottameta/yotta-skills hub refresh my-skill --from <技能目录>
57
74
 
58
- # 分发到全部已发现宿主:元技能链接时收敛旧副本(回收站 7 天);
75
+ # 分发到全部已核实宿主(默认范围):元技能链接时收敛旧副本(回收站 7 天);
59
76
  # 外部技能同名真目录默认跳过(--force 才备份并替换)
60
77
  npx -y @yottameta/yotta-skills hub link --all
61
78
 
62
- # 只删除链接,不动 Hub 真源
79
+ # 显式把自动发现目录一并纳入(桥接目录永不链接)
80
+ npx -y @yottameta/yotta-skills hub link --all --include-discovered
81
+
82
+ # 只删除链接,不动 Hub 真源;清理范围外链接用 --dir <目录> 精确指定
63
83
  npx -y @yottameta/yotta-skills hub unlink --all
64
84
 
65
85
  # 查看来源、版本、链接宿主与异常
@@ -69,11 +89,14 @@ npx -y @yottameta/yotta-skills hub doctor
69
89
 
70
90
  ## 宿主发现口径
71
91
 
72
- 1. 已收录宿主映射(兼容 Vercel Labs `skills` CLI / SkillCat 的公开宿主表);
92
+ 1. 已收录宿主映射(81 条;兼容 Vercel Labs `skills` CLI / SkillCat 的公开宿主表,
93
+ 经 0.28.0 逐智能体核实:XDG 解析只覆盖 skills CLI 表确认的 `.config/agents`、
94
+ `.config/devin`、`.config/opencode` 三类 rel,其余 `.config/...` 按字面
95
+ `~/.config` 解析);
73
96
  2. 环境变量覆盖(`CODEX_HOME`、`XDG_CONFIG_HOME`、`DSH_HOME`、
74
97
  `OPENCLAW_STATE_DIR`、`CLAUDE_CONFIG_DIR` 等);
75
98
  3. 常见配置根启发式扫描;
76
- 4. `YOTTA_SKILLS_DISCOVERY_ROOTS` 显式补充;
99
+ 4. `YOTTA_SKILLS_DISCOVERY_ROOTS` 显式补充(自动发现类,需 `--include-discovered`);
77
100
  5. `--dir <path>` 精确指定非标准宿主。
78
101
 
79
102
  自动发现默认排除临时目录、`.bak`、candidate / staging、插件构建目录等噪声。
@@ -27,7 +27,7 @@ import subprocess
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
- VERSION = "0.27.1"
30
+ VERSION = "0.28.0"
31
31
  TOOL_NAME = "yotta-skills"
32
32
  CN_NAME = "元阁"
33
33
  MCP_PROTOCOL_MODERN = "2026-07-28"