@yottameta/yotta-skills-plugin 0.30.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-skills/SKILL.md +1 -1
- package/skills/yotta-skills/assets/view.html +2 -16
- package/skills/yotta-skills/bin/yotta-skills.js +2 -2
- package/skills/yotta-skills/lib/cli-help.js +102 -48
- package/skills/yotta-skills/lib/hub-view-server.js +2 -1
- package/skills/yotta-skills/references/skill-list.md +2 -2
- package/skills/yotta-skills/scan-policy.json +3 -3
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +1 -1
- package/skills/yotta-skills/skills.json +3 -3
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Developer Tools",
|
|
15
|
-
"version": "0.30.
|
|
15
|
+
"version": "0.30.2",
|
|
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.30.
|
|
15
|
+
"version": "0.30.2",
|
|
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.30.
|
|
3
|
+
"version": "0.30.2",
|
|
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.30.
|
|
4
|
+
"version": "0.30.2",
|
|
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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-skills
|
|
3
|
-
version: 0.30.
|
|
3
|
+
version: 0.30.2
|
|
4
4
|
description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 本机技能 Hub + 运行时 hook 适配。Hub 层:hub hosts 只读发现本机已装智能体与技能目录(文件系统优先,不读元忆;已核实 / 自动发现 / 桥接分类 + 状态细分:可用 / 残留(实体未确认)/ 未创建 / 仅标记 / 隐藏标记(ignored,只影响显示));hub hosts add/remove/rename/list/mark/exclude/include/set 注册自定义宿主目录、手动标记、不接管开关与目录覆盖(只注册不建目录、移除不删目录;残留清理 hub hosts remove --purge 默认预览、目录入回收站 7 天;YottaCode 不纳入接管);install-self / where 把元阁管理引擎独立安装(默认 ~/.yottaskills/yotta-skills,--dir 指定)并查看运行位置;hub config get/set/clear/rollback 持久化 Hub 位置(config.json;有技能时 clear 默认阻断、--yes 放行;优先级 --hub > YOTTA_SKILLS_HUB > config > 默认;--move 迁移旧 Hub + 重链 + 回收站;lastMigration 记录 + 一键回滚 = 位置回退;仅切换且旧 Hub 非空时记录未迁移供面板一键迁移);hub cleanup-backups 清理历史备份 / 暂存残留(回收站 7 天);hub install / update 把技能装到 ~/.yottaskills/hub 单点真源(清单 27 + 特殊家族 5,特殊家族跟随各自 npm latest;update / hub update 只升不降,--force 才显式降级);hub link --all 用 Windows junction / POSIX symlink 分发到全部已核实宿主(默认范围;--include-discovered 显式纳入自动发现目录;锁 / 数据桥接目录永不链接),元技能在链接时收敛宿主旧副本(移入 ~/.yottaskills/trash 保留 7 天、版本闸门 Hub ≥ 宿主);hub unlink 只删链接、fail-closed(--dir 无 slug 时以文件系统为真源清理该目录全部 Hub 链接,其它位置链接不动);非元技能不参与更新(用户自行处理);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-self 独立安装 / where 位置查看 / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、单点安装并分发到多个智能体、查看本机装了哪些智能体与技能目录、接管非元技能、按场景组合多个元技能、路由或判断该用哪些技能、判断哪些技能值得长期记忆、查看或记录技能使用信号、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/记忆裁决/技能该不该记住/盘点技能/查看已装技能/技能 Hub/单点安装/链接分发/接管技能/技能枢纽面板/图形化面板/独立安装元阁/查看元阁位置/持久化 Hub 位置/换 Hub 目录/迁移 Hub/迁移回滚/回滚 Hub 位置/清理备份残留/自定义宿主/残留清理/宿主状态 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + M1 记忆裁决只读建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + Hub 本机真源与链接分发 + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆、不自动删除技能或记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
@@ -1244,21 +1244,7 @@
|
|
|
1244
1244
|
if (b) copyText(b.dataset.copyCmd);
|
|
1245
1245
|
});
|
|
1246
1246
|
|
|
1247
|
-
/* 高级 CLI:/api/help 单一真源(与 CLI --help
|
|
1248
|
-
function findHelpEntry(groups, ref) {
|
|
1249
|
-
var seg = String(ref).split(' ');
|
|
1250
|
-
for (var i = 0; i < groups.length; i++) {
|
|
1251
|
-
var cmds = groups[i].commands || [];
|
|
1252
|
-
for (var j = 0; j < cmds.length; j++) {
|
|
1253
|
-
var c = cmds[j];
|
|
1254
|
-
if (c.name !== seg[0]) continue;
|
|
1255
|
-
if (seg.length === 1) return c;
|
|
1256
|
-
var subs = c.subcommands || [];
|
|
1257
|
-
for (var k = 0; k < subs.length; k++) if (subs[k].name === seg[1]) return subs[k];
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
return null;
|
|
1261
|
-
}
|
|
1247
|
+
/* 高级 CLI:/api/help 单一真源(与 CLI --help 同源;速查解析由服务端按最深前缀完成) */
|
|
1262
1248
|
function renderCliHelp(d) {
|
|
1263
1249
|
if (!d || d.error) { $('#cliMeta').textContent = '帮助加载失败'; return; }
|
|
1264
1250
|
var total = 0, optTotal = (d.options || []).length;
|
|
@@ -1272,7 +1258,7 @@
|
|
|
1272
1258
|
$('#cliMeta').textContent = 'v' + (d.version || '-') + ' · 命令 ' + total + ' · 选项 ' + optTotal;
|
|
1273
1259
|
$('#cliQuick').innerHTML = (d.quick || []).map(function (g) {
|
|
1274
1260
|
return '<div class="cmd-group"><h3>' + esc(g.group) + '</h3>' + (g.items || []).map(function (item) {
|
|
1275
|
-
var meta =
|
|
1261
|
+
var meta = item.meta || {};
|
|
1276
1262
|
return '<div class="cmd"><code>' + esc(item.example) + '</code><span class="why">' + esc(meta.what || '') +
|
|
1277
1263
|
(item.note ? ' · ' + esc(item.note) : '') + '</span>' +
|
|
1278
1264
|
(item.danger ? '<span class="badge danger">危险</span>' : '') +
|
|
@@ -62,11 +62,11 @@ const { COPY_SKIP, copyDir } = require('../lib/copy-tree');
|
|
|
62
62
|
const { versionRelation } = require('../lib/skills-scan');
|
|
63
63
|
|
|
64
64
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
65
|
-
let VERSION = '0.30.
|
|
65
|
+
let VERSION = '0.30.2';
|
|
66
66
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
67
67
|
|
|
68
68
|
// @generated view-html:start
|
|
69
|
-
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 .loadbar{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:13px;padding:10px 13px;border:1px dashed var(--line2);border-radius:8px;margin:0 0 14px;background:var(--panel)}\n .spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--line2);border-top-color:var(--acc);animation:spin .8s linear infinite;flex:none}\n @keyframes spin{to{transform:rotate(360deg)}}\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 .chips{display:flex;gap:6px;flex-wrap:wrap}\n .chip{cursor:pointer;background:transparent}\n .chip.active{color:var(--acc);border-color:rgba(95,191,143,.45);background:rgba(95,191,143,.10)}\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 .cmd-detail{border:1px solid var(--line);border-radius:6px;background:var(--panel2);margin-top:8px;padding:0 11px;min-width:0}\n .cmd-detail summary{cursor:pointer;padding:9px 0;display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}\n .cmd-detail .note{padding:0 0 8px}\n .cmd-detail .cmd{margin:0 0 8px}\n\n /* 0.30.0 UX-05:术语速查 */\n .glossary{border:1px solid var(--line);border-radius:8px;background:var(--panel);padding:0 12px}\n .glossary summary{padding:10px 0}\n .glossary-body{display:grid;gap:6px;padding:0 0 12px;font-size:12px;color:var(--muted)}\n .glossary-body b{color:var(--text)}\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 <div class=\"loadbar\" id=\"loadbar\" hidden><span class=\"spinner\"></span><span id=\"loadbarText\">正在加载…</span></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>\n <label class=\"search section\" style=\"min-width:100%\"><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=\"ovSkillFilter\" placeholder=\"过滤技能(输入后显示全部匹配)\"></label>\n <div class=\"list\" id=\"ovSkills\"></div>\n </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=\"faint\" id=\"ovHubMeta\"></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 <button class=\"btn small\" id=\"hubConfigBtn\">设置新位置</button>\n <button class=\"btn small\" id=\"hubConfigClearBtn\" hidden title=\"清除持久 Hub 位置覆盖,回落默认解析;当前 Hub 有技能时会先展示影响面\">清除覆盖</button>\n </div>\n </div>\n <div class=\"section\" id=\"ovSwitchBanner\" hidden></div>\n <div class=\"section panel\" id=\"ovMigration\" hidden>\n <h3>最近一次迁移</h3>\n <div id=\"ovMigrationBody\"></div>\n <div class=\"toolbar section\">\n <button class=\"btn small danger\" id=\"migrationRollbackBtn\">回滚到原位置</button>\n <button class=\"btn small\" id=\"copyMigrationTrash\">复制回收站路径</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 <div class=\"chips\" id=\"hostStateChips\"></div>\n <button class=\"btn small\" id=\"hostAddBtn\">添加自定义目录</button>\n </div>\n <details class=\"glossary section\">\n <summary>术语速查</summary>\n <div class=\"glossary-body\">\n <div><b>可用</b>:宿主实体确认、技能目录存在(默认分发范围)。</div>\n <div><b>残留</b>:宿主实体未确认(疑似已卸载);可用「清理」把目录移入回收站。</div>\n <div><b>仅标记</b>:检测到宿主已安装,但技能目录尚未创建。</div>\n <div><b>隐藏标记</b>:只影响列表显示,不拦截任何行为(ignored)。</div>\n <div><b>不接管</b>:发现 / 显示 / 链接三层全部跳过;已有链接不动(真正拦截行为的开关)。</div>\n <div><b>清理</b>:解除指向 Hub 的链接 + 目录移入回收站(保留 7 天可恢复)。</div>\n <div><b>桥接</b>:锁 / 数据目录(如 XDG state),永不作为链接目标。</div>\n </div>\n </details>\n <div class=\"section panel\">\n <div class=\"faint\" id=\"hostSummary\"></div>\n <table><thead><tr><th>宿主</th><th>技能目录</th><th>范围</th><th>目录技能</th><th title=\"只统计指向当前生效 Hub 的链接(文件系统为真源);指向其它位置的链接单独标注(配置错位预警)\">指向当前 Hub</th><th>异常</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 <div class=\"section panel\" id=\"hostExcludedWrap\" hidden>\n <h3>不接管名单</h3>\n <div class=\"faint\">这些宿主 / 目录在发现、显示、链接三层全部跳过(--include-discovered 也不纳入)。</div>\n <div class=\"list\" id=\"hostExcluded\"></div>\n </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 class=\"section panel\">\n <div class=\"shead\"><h2>命令速查</h2><span class=\"faint\" id=\"cliMeta\">加载中…</span></div>\n <div id=\"cliQuick\"></div>\n </div>\n <div class=\"section panel\">\n <details id=\"cliFullWrap\">\n <summary>完整命令与参数(点击展开)</summary>\n <div id=\"cliFull\"></div>\n </details>\n </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: '', hostStateFilter: '', adoptFilter: '', ovSkillFilter: '' };\n\n function $(sel) { return document.querySelector(sel); }\n function esc(value) {\n return String(value === null || value === undefined ? '' : value)\n .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n .replace(/\"/g, '"').replace(/'/g, ''');\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: '全量命令速查与复制(与 CLI --help 同源)', load: null }\n };\n var loadSeq = 0;\n function showLoad(text) {\n loadSeq += 1;\n $('#loadbarText').textContent = text;\n $('#loadbar').hidden = false;\n $('#refreshBtn').disabled = true;\n return loadSeq;\n }\n function hideLoad(seq) {\n if (seq !== undefined && seq !== loadSeq) return;\n $('#loadbar').hidden = true;\n $('#refreshBtn').disabled = false;\n }\n async function runLoad(name) {\n var v = VIEWS[name];\n if (!v) return;\n if (!v.load) { loadSeq += 1; hideLoad(); return; }\n var seq = showLoad('正在加载' + v.title + '…');\n try { await v.load(); } finally { hideLoad(seq); }\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 runLoad(name);\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) runLoad(state.view); 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 renderOverviewSkills();\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 var hubInfo = d.hub || {};\n var sourceNames = { flag: '--hub 参数', env: 'YOTTA_SKILLS_HUB 环境变量', config: 'config.json 配置', default: '默认位置' };\n var metaParts = ['来源: ' + (sourceNames[hubInfo.source] || hubInfo.source || '默认位置')];\n if (hubInfo.configured) metaParts.push('配置覆盖: ' + hubInfo.configured);\n if (hubInfo.restartRequired) metaParts.push('配置已指向 ' + (hubInfo.effectiveNow || '') + ',重启 view 后生效');\n $('#ovHubMeta').textContent = metaParts.join(' · ');\n $('#hubConfigClearBtn').hidden = !hubInfo.configured;\n renderMigrationBlock(d);\n }\n\n /* 0.29.5 S1/S2:迁移向导(迁移 / 仅切换)+ 最近迁移 + 一键回滚(与 CLI 同内核) */\n function renderMigrationBlock(d) {\n var mig = (d.migration && d.migration.last) || null;\n var pending = (d.migration && d.migration.switchPending) || null;\n var banner = $('#ovSwitchBanner');\n if (pending) {\n banner.hidden = false;\n banner.innerHTML = '<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>' +\n '<div><div class=\"banner-title\">旧 Hub 还有 ' + esc(pending.skills) + ' 个技能未迁移</div>' +\n '<div class=\"banner-body\">旧位置 <span class=\"mono\">' + esc(pending.from) + '</span> · 当前位置 <span class=\"mono\">' + esc(d.hubDir) + '</span></div>' +\n '<div class=\"toolbar section\"><button class=\"btn small primary\" id=\"switchMigrateNow\">一键迁移到当前位置</button>' +\n '<button class=\"btn small\" id=\"switchMigrateOpen\">迁移到其它位置…</button></div></div></div>';\n $('#switchMigrateNow').onclick = function () {\n confirmModal('迁移旧 Hub 到当前位置?',\n '<p class=\"label\">将把 <span class=\"mono\">' + esc(pending.from) + '</span>(' + esc(pending.skills) + ' 个技能)复制到 <span class=\"mono\">' + esc(d.hubDir) + '</span>,逐技能校验后切换配置、重链全部宿主,旧 Hub 入回收站(7 天可回滚)。</p>',\n '开始迁移', false, async function () {\n await executeHubMigration({ hub: d.hubDir, mode: 'move' }, { from: pending.from });\n });\n };\n $('#switchMigrateOpen').onclick = function () { openHubConfigWizard(); };\n } else {\n banner.hidden = true;\n banner.innerHTML = '';\n }\n var box = $('#ovMigration');\n if (!mig) {\n box.hidden = true;\n return;\n }\n box.hidden = false;\n var kind = mig.kind === 'rollback' ? '回滚' : '迁移';\n var lines = [\n '最近一次' + kind + ':' + esc(mig.from) + ' → ' + esc(mig.to),\n '时间 ' + esc(fmtTime(mig.at)) + ' · 技能 ' + esc(mig.verifiedSkills) + ' · 重链 ' + esc(mig.relinkDirs) + ' 个宿主目录',\n ];\n if (mig.trashedTo) lines.push('回收站 ' + esc(mig.trashedTo) + '(剩余 ' + esc(mig.daysLeft) + ' 天)');\n if (mig.incomplete > 0) lines.push('⚠ 重链未全部完成(' + esc(mig.incomplete) + ' 项);旧 Hub 保留在 ' + esc(mig.oldHubKept || mig.from));\n if (mig.trashError) lines.push('⚠ 旧 Hub 移入回收站失败(已保留原位):' + esc(mig.trashError));\n $('#ovMigrationBody').innerHTML = '<div class=\"faint\">' + lines.join('<br>') + '</div>';\n $('#copyMigrationTrash').hidden = !mig.trashedTo;\n $('#copyMigrationTrash').onclick = function () { copyText(mig.trashedTo); };\n $('#migrationRollbackBtn').onclick = rollbackFlow;\n }\n\n function rewireModalButtons(opts) {\n var ok = $('#modal').querySelector('[data-modal=\"ok\"]');\n var cancel = $('#modal').querySelector('[data-modal=\"cancel\"]');\n if (ok && opts.okText) { ok.textContent = opts.okText; ok.disabled = false; ok.onclick = opts.onOk || closeModal; }\n if (ok && opts.hideOk) ok.hidden = true;\n if (cancel && opts.cancelText) { cancel.textContent = opts.cancelText; cancel.onclick = opts.onCancel || closeModal; }\n }\n\n function showMigrationResult(result) {\n var move = result.move || {};\n var done = !move.incomplete && !move.trashError;\n $('#modalBody').innerHTML =\n '<div class=\"result\"><span class=\"badge ' + (done ? 'ok' : 'danger') + '\">' + (done ? '迁移完成' : '部分完成') + '</span><span>' + esc(result.previous || '') + ' → ' + esc(result.hub || '') + '</span></div>' +\n '<div class=\"result\"><span class=\"badge ok\">校验</span><span>' + esc(move.verifiedSkills || 0) + ' 个技能复制校验通过</span></div>' +\n '<div class=\"result\"><span class=\"badge ok\">重链</span><span>' + esc(move.relinkDirs || 0) + ' 个宿主目录</span></div>' +\n (move.trashedTo ? '<div class=\"result\"><span class=\"badge ok\">旧 Hub</span><span class=\"mono\">' + esc(move.trashedTo) + '</span><span>已入回收站(7 天可恢复)</span></div>' : '') +\n (move.incomplete > 0 ? '<div class=\"result\"><span class=\"badge danger\">重链未全部完成</span><span>旧 Hub 保留在 <span class=\"mono\">' + esc(move.oldHubKept || move.from || '') + '</span>;重跑:yotta-skills hub link --all --force</span></div>' : '') +\n (move.trashError ? '<div class=\"result\"><span class=\"badge warn\">回收站</span><span>旧 Hub 移入回收站失败(已保留原位):' + esc(move.trashError) + '</span></div>' : '') +\n '<div class=\"note\">新位置从下次面板加载开始生效(已在运行的 view 需重启)。</div>';\n rewireModalButtons({\n okText: '关闭',\n onOk: closeModal,\n cancelText: '刷新面板',\n onCancel: function () { closeModal(); loadOverview(); },\n });\n }\n\n function showSwitchResult(result) {\n $('#modalBody').innerHTML =\n '<div class=\"result\"><span class=\"badge ok\">已切换</span><span>' + esc(result.previous || '') + ' → ' + esc(result.hub || '') + '</span></div>' +\n ((result.oldSkills || 0) > 0\n ? '<div class=\"result\"><span class=\"badge warn\">未迁移</span><span>旧 Hub 还有 ' + esc(result.oldSkills) + ' 个技能未迁移;概览会持续提示,可一键迁移。</span></div>'\n : '') +\n '<div class=\"note\">只切换了指针,没有移动任何文件;新位置从下次面板加载开始生效。</div>';\n rewireModalButtons({\n okText: '关闭',\n onOk: closeModal,\n cancelText: '刷新面板',\n onCancel: function () { closeModal(); loadOverview(); },\n });\n }\n\n async function executeHubMigration(body, opts) {\n opts = opts || {};\n var seq = showLoad(body.mode === 'switch' ? '正在切换 Hub 位置…' : '正在迁移 Hub(复制 → 校验 → 重链 → 回收站)…');\n var payload = Object.assign({ confirm: 'hub-config' }, body);\n if (opts.from) payload = Object.assign(payload, { from: opts.from });\n var r = await api('/api/hub/config', { body: payload });\n hideLoad(seq);\n if (r.error) {\n if (r.code === 'target-residue') return wizardResiduePrompt(body, r.error, opts);\n if (opts.inModal) {\n $('#hubWizardResult').innerHTML = '<div class=\"result\"><span class=\"badge danger\">失败</span><span>' + esc(r.error) + '</span></div>';\n } else {\n toast(r.error);\n }\n return null;\n }\n if (opts.inModal) {\n if (body.mode === 'switch') showSwitchResult(r);\n else showMigrationResult(r);\n }\n else {\n closeModal();\n toast('迁移完成:校验 ' + ((r.move && r.move.verifiedSkills) || 0) + ' 个技能 · 重链 ' + ((r.move && r.move.relinkDirs) || 0) + ' 个目录' + (r.move && r.move.incomplete ? '(重链未全部完成,见概览提示)' : ''));\n loadOverview();\n }\n return r;\n }\n\n function wizardResiduePrompt(body, error, opts) {\n var host = opts && opts.inModal ? $('#hubWizardResult') : $('#modalBody');\n if (!host) { toast(error); return; }\n host.innerHTML = '<div class=\"result\"><span class=\"badge warn\">残留</span><span>' + esc(error) + '</span></div>' +\n '<button class=\"btn small primary\" id=\"hubResidueClean\">清理残留并迁移</button>';\n $('#hubResidueClean').onclick = async function () {\n var seq = showLoad('正在清理残留并迁移…');\n var payload = Object.assign({ confirm: 'hub-config', cleanResidue: true }, body);\n if (opts && opts.from) payload.from = opts.from;\n var r = await api('/api/hub/config', { body: payload });\n hideLoad(seq);\n if (r.error) { toast(r.error); return; }\n if (opts && opts.inModal) showMigrationResult(r);\n else { closeModal(); toast('迁移完成'); loadOverview(); }\n };\n }\n\n function openHubConfigWizard() {\n var d = state.overview || {};\n var current = d.hubDir || '';\n var skillCount = (d.status && d.status.summary && d.status.summary.skills) || 0;\n var body = '<p class=\"label\">写入 config.json 持久覆盖(优先级低于 --hub 参数 / YOTTA_SKILLS_HUB)。</p>' +\n '<label class=\"label\">新的 Hub 目录</label>' +\n '<input id=\"hubNewPath\" placeholder=\"D:\\\\my-hub\" value=\"\">' +\n '<div class=\"note mono\">当前: ' + esc(current) + '(' + esc(skillCount) + ' 个技能)</div>' +\n '<div class=\"section\"><label class=\"label\"><input type=\"radio\" name=\"hubMode\" value=\"move\" checked style=\"width:auto\"> 迁移到新位置(推荐)</label>' +\n '<div class=\"faint\">复制 → 逐技能校验 → 切换配置 → 全宿主重链 → 旧 Hub 入回收站(7 天可回滚)。</div>' +\n '<label class=\"label section\"><input type=\"radio\" name=\"hubMode\" value=\"switch\" style=\"width:auto\"> 仅切换位置(保留旧内容)</label>' +\n '<div class=\"faint\">只改指针,不移动任何文件;旧 Hub 内容保留,概览会提示未迁移。</div></div>' +\n '<div id=\"hubWizardResult\" class=\"section\"></div>';\n openModal('设置 Hub 位置', body, {\n okText: '执行',\n onOk: async function () {\n var value = ($('#hubNewPath').value || '').trim();\n if (!value) { toast('先填写新的 Hub 目录'); return; }\n var mode = document.querySelector('input[name=\"hubMode\"]:checked').value;\n if (mode === 'switch' && skillCount > 0) {\n $('#hubWizardResult').innerHTML = '<div class=\"result\"><span class=\"badge warn\">确认</span><span>旧 Hub 还有 ' + esc(skillCount) + ' 个技能未迁移。仅切换位置不会移动它们。</span></div>' +\n '<button class=\"btn small danger\" id=\"hubSwitchConfirm\">确认仅切换</button>';\n $('#hubSwitchConfirm').onclick = function () { executeHubMigration({ hub: value, mode: 'switch' }, { inModal: true }); };\n return;\n }\n await executeHubMigration({ hub: value, mode: mode }, { inModal: true });\n },\n });\n }\n\n function rollbackFlow() {\n api('/api/hub/config').then(function (d) {\n if (d.error) { toast(d.error); return; }\n var rb = d.rollback || {};\n if (!rb.lastMigration) { toast('没有可回滚的迁移记录'); return; }\n if (!rb.ok) {\n confirmModal('回滚被阻断', '<p class=\"label\">' + esc((rb.blocked || []).join(';') || rb.error || '') + '</p><div class=\"note\">回滚 fail-closed(不合并);处理阻断项后重试。</div>', null, true, null);\n return;\n }\n confirmModal('回滚 Hub 位置?',\n '<p class=\"label\">将把当前 Hub 内容复制回原位置:<span class=\"mono\">' + esc(rb.current) + '</span> → <span class=\"mono\">' + esc(rb.target) + '</span>(复制校验 → 切换 → 重链 → 当前 Hub 入回收站 7 天)。</p>' +\n '<div class=\"note\">位置回退(用当前内容),不是恢复旧快照。</div>',\n '执行回滚', true, async function () {\n var seq = showLoad('正在回滚 Hub 位置(复制 → 校验 → 重链 → 回收站)…');\n var r = await api('/api/hub/config', { body: { rollback: true, confirm: 'hub-config' } });\n hideLoad(seq);\n if (r.error) {\n if (r.code === 'target-residue') { wizardResiduePrompt({ rollback: true }, r.error, {}); return; }\n toast(r.error); return;\n }\n closeModal();\n toast('回滚完成:' + ((r.move && r.move.verifiedSkills) || 0) + ' 个技能已回到 ' + (r.hub || ''));\n loadOverview();\n });\n });\n }\n\n $('#hubConfigBtn').onclick = openHubConfigWizard;\n $('#hubConfigClearBtn').onclick = function () {\n var d = state.overview || {};\n var hubInfo = d.hub || {};\n var skills = (d.status && d.status.summary && d.status.summary.skills) || 0;\n var body = '<p class=\"label\">将删除 config.json 中的 Hub 覆盖,恢复 --hub > 环境变量 > 默认位置的解析。不会移动任何文件。</p>' +\n '<div class=\"result\"><span class=\"badge warn\">影响面</span><b>' + esc(skills) + '</b><span>个技能与已建链接仍在 <span class=\"mono\">' + esc(hubInfo.configured || d.hubDir || '') + '</span>;清除后生效位置回落到默认 <span class=\"mono\">' + esc(hubInfo.defaultDir || '默认位置') + '</span>(可能是空目录),面板 / CLI 将按新位置工作。</span></div>' +\n '<p class=\"faint\">替代路径:迁移到默认位置或其它位置,用「设置新位置」向导(等价 CLI hub config set --hub <路径> --move)。</p>';\n if (skills > 0) {\n body += '<div class=\"section\"><label class=\"label\">输入 clear 确认(当前 Hub 有技能):</label><input id=\"hubClearConfirm\" placeholder=\"clear\"></div>';\n }\n confirmModal('清除 Hub 位置覆盖?', body, '清除覆盖', true, async function () {\n if (skills > 0) {\n var typed = ($('#hubClearConfirm') && $('#hubClearConfirm').value || '').trim();\n if (typed !== 'clear') { toast('确认串不匹配(输入 clear)'); return; }\n }\n var r = await api('/api/hub/config', { body: { clear: true, confirm: 'hub-config' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已清除覆盖;重启 view 后生效');\n loadOverview();\n });\n };\n\n function renderOverviewSkills() {\n var d = state.overview;\n if (!d) return;\n var skills = (d.status && d.status.skills) || [];\n var filter = state.ovSkillFilter.trim().toLowerCase();\n var list = filter\n ? skills.filter(function (s) { return String(s.slug || '').toLowerCase().indexOf(filter) >= 0; })\n : skills.slice(0, 8);\n $('#ovSkills').innerHTML = list.length ? list.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>' +\n '<span class=\"badge ' + (s.status === 'present' ? 'ok' : 'danger') + '\">' + esc(s.status === 'present' ? '已入库' : '缺失') + '</span>' +\n '<div class=\"row-actions\"><button class=\"btn small danger\" data-remove-skill=\"' + esc(s.slug) + '\">删除</button></div></div>';\n }).join('') : '<div class=\"empty\">' + (filter ? '没有匹配的技能。' : 'Hub 还是空的。先在「收编向导」把已装技能收编进来。') + '</div>';\n }\n $('#ovSkillFilter').addEventListener('input', function () {\n state.ovSkillFilter = $('#ovSkillFilter').value || '';\n renderOverviewSkills();\n });\n $('#ovSkills').addEventListener('click', async function (e) {\n var b = e.target.closest('[data-remove-skill]');\n if (!b) return;\n var slug = b.dataset.removeSkill;\n var plan = await api('/api/skills/remove-plan?slug=' + encodeURIComponent(slug));\n if (!plan || plan.error) { toast((plan && plan.error) || '影响预览失败'); return; }\n var targets = plan.targets || [];\n var willUnlink = targets.filter(function (t) { return t.action === 'unlink'; });\n var kept = targets.filter(function (t) { return t.action === 'keep'; });\n var rows = targets.map(function (t) {\n var tone = t.action === 'unlink' ? 'warn' : t.action === 'keep' ? 'info' : '';\n return '<div class=\"result\"><span class=\"badge ' + tone + '\">' + esc(t.action === 'unlink' ? '将清理' : t.action === 'keep' ? '保留' : '无动作') + '</span><span class=\"mono\">' + esc(t.dir) + '</span><span class=\"faint\">' + esc(t.note || '') + '</span></div>';\n }).join('');\n var body = '<p class=\"label\">将清理 ' + willUnlink.length + ' 条宿主链接(含死链)' +\n (plan.hubPresent ? ',并把 Hub 目录移入回收站(保留 7 天)' : ';Hub 目录已缺失,仅清理残留') +\n ';保留 ' + kept.length + ' 项宿主真副本 / 外部链接。</p>' +\n '<div class=\"result-list\">' + (rows || '<div class=\"faint\">没有需要处理的宿主目录。</div>') + '</div>' +\n '<div class=\"section\"><label class=\"label\">输入完整技能名确认:' + esc(slug) + '</label><input id=\"removeConfirmSlug\" placeholder=\"' + esc(slug) + '\"></div>';\n confirmModal('删除 Hub 技能 ' + slug + '?', body, '删除', true, async function () {\n var typed = ($('#removeConfirmSlug') && $('#removeConfirmSlug').value || '').trim();\n if (typed !== slug) { toast('技能名不匹配'); return; }\n var r = await api('/api/skills/remove', { body: { slug: slug, confirm: 'remove', confirmSlug: typed } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast(r.verdict === 'cleanup-only' ? '已清理残留(Hub 目录原本缺失)' : '已删除 ' + slug + '(目录已入回收站)');\n loadOverview();\n });\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 var s = d.summary.states || {};\n $('#hostSummary').textContent = '可用 ' + (s.available || 0) + ' · 残留 ' + (s.orphan || 0) + ' · 未创建 ' + (s.missing || 0) + ' · 仅标记 ' + (s.markerOnly || 0) + (s.ignored ? ' · 隐藏标记 ' + s.ignored : '') + (d.summary.excluded ? ' · 不接管 ' + d.summary.excluded : '') + ' · 链接 ' + d.summary.links + '(异常 ' + d.summary.brokenLinks + ')' + (d.summary.otherHubLinks ? ' · 指向其它 Hub ' + d.summary.otherHubLinks : '');\n renderHostStateChips();\n renderHosts();\n var marks = d.unpaired || [];\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 || '') + ' · 未发现技能目录</div></div><span class=\"badge info\">仅标记</span></div>';\n }).join('') : '<div class=\"empty\">没有未配对的宿主标记。</div>';\n renderHostExcluded(d.excluded || []);\n }\n function renderHostExcluded(list) {\n $('#hostExcludedWrap').hidden = list.length === 0;\n $('#hostExcluded').innerHTML = list.map(function (item) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(item.label || item.value) + '</div><div class=\"row-meta mono\">' + esc(item.value) + ' · ' + esc(item.kind === 'agent' ? '智能体' : '目录') + '</div></div><div class=\"row-actions\"><button class=\"btn small\" data-host-include=\"' + esc(item.value) + '\">恢复接管</button></div></div>';\n }).join('');\n }\n function renderHostStateChips() {\n var d = state.hosts; if (!d) return;\n var s = d.summary.states || {};\n var chips = [\n ['', '全部'],\n ['available', '可用 ' + (s.available || 0)],\n ['orphan', '残留 ' + (s.orphan || 0)],\n ['missing', '未创建 ' + (s.missing || 0)],\n ['marker-only', '仅标记 ' + (s.markerOnly || 0)],\n ['ignored', '隐藏标记 ' + (s.ignored || 0)],\n ];\n $('#hostStateChips').innerHTML = chips.map(function (c) {\n return '<button class=\"chip' + (state.hostStateFilter === c[0] ? ' active' : '') + '\" data-host-state=\"' + esc(c[0]) + '\">' + esc(c[1]) + '</button>';\n }).join('');\n }\n function hostStateBadge(h) {\n if (h.bridgeOnly) return '<span class=\"badge info\">桥接</span>';\n var map = { available: ['ok', '可用'], orphan: ['warn', '残留'], missing: ['', '未创建'], 'marker-only': ['info', '仅标记'], ignored: ['', '隐藏标记'] };\n var item = map[h.state] || ['', h.state || '未知'];\n return '<span class=\"badge ' + item[0] + '\">' + esc(item[1]) + (h.stateSource === 'manual' ? '·手动' : '') + '</span>' + (h.brokenLinks ? ' <span class=\"badge danger\">异常</span>' : '');\n }\n function renderHosts() {\n var d = state.hosts; if (!d) return;\n var q = state.hostFilter.toLowerCase();\n var sf = state.hostStateFilter;\n var rows = (d.hosts || []).filter(function (h) {\n if (sf && h.state !== sf) return false;\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 var overrideIds = {};\n ((d.overrides) || []).forEach(function (o) { overrideIds[String(o.agentId).toLowerCase()] = true; });\n $('#hostRows').innerHTML = rows.map(function (h) {\n var scopeBadge = h.bridgeOnly ? '<span class=\"badge info\">桥接·不链</span>'\n : (h.detection === 'user-registry' ? '<span class=\"badge info\">用户注册</span>'\n : (h.detection === 'override' ? '<span class=\"badge ok\">目录覆盖</span>'\n : (h.verified ? '<span class=\"badge ok\">已核实</span>'\n : ((h.detection === 'discovered' || h.detection === 'workspace') ? '<span class=\"badge warn\">自动发现</span>' : '<span class=\"badge warn\">映射·未核实</span>'))));\n var action = '';\n if (h.exists && h.state === 'orphan' && !h.bridgeOnly) action += '<button class=\"btn small danger\" data-host-purge=\"' + esc(h.dir) + '\" title=\"清理残留:解除指向 Hub 的链接 + 目录移入回收站(7 天可恢复)\">清理</button> ';\n if (h.state === 'ignored') action += '<button class=\"btn small\" data-host-mark=\"' + esc(h.dir) + '\" data-mark-state=\"available\" title=\"恢复为可用状态(仅状态标记)\">标记可用</button> ';\n else if (h.exists && !h.bridgeOnly) action += '<button class=\"btn small\" data-host-mark=\"' + esc(h.dir) + '\" data-mark-state=\"ignored\" title=\"只影响列表显示,不拦截行为(拦截用「不接管」)\">隐藏标记</button> ';\n if (h.agentId && !h.bridgeOnly) {\n if (overrideIds[String(h.agentId).toLowerCase()]) action += '<button class=\"btn small\" data-host-clear=\"' + esc(h.agentId) + '\" title=\"删除目录覆盖,恢复内置映射\">恢复默认</button> ';\n else action += '<button class=\"btn small\" data-host-edit=\"' + esc(h.agentId) + '\" data-host-dir=\"' + esc(h.dir) + '\" data-host-label=\"' + esc(h.label) + '\" title=\"写入目录覆盖(hosts.json),不改宿主配置\">编辑目录</button> ';\n }\n if (!h.bridgeOnly) action += '<button class=\"btn small\" data-host-exclude=\"' + esc(h.agentId || h.dir) + '\" data-host-links=\"' + esc(h.hubLinks || 0) + '\" title=\"加入不接管名单:发现 / 显示 / 链接三层跳过;已有链接不动\">不接管</button> ';\n if (h.detection === 'user-registry') action += '<button class=\"btn small\" data-host-rename=\"' + esc(h.dir) + '\" data-host-label=\"' + esc(h.label) + '\" title=\"只改显示名;目录与手动状态保持\">重命名</button> ';\n if (h.detection === 'user-registry') action += '<button class=\"btn small\" data-host-unregister=\"' + esc(h.dir) + '\" data-host-links=\"' + esc(h.hubLinks || 0) + '\" title=\"只移除注册,不删目录;已有链接不清理(用 hub unlink --dir)\">移除注册</button>';\n var linksCell = esc(h.hubLinks || 0);\n if (h.hubLinksOther) {\n var otherTitle = '指向其它位置的链接(配置错位预警)' + ((h.hubLinksOtherRoots || []).length ? ':' + h.hubLinksOtherRoots.join(' / ') : '');\n linksCell += ' <span class=\"badge warn\" title=\"' + esc(otherTitle) + '\">其它 Hub ' + esc(h.hubLinksOther) + '</span>';\n }\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>' + linksCell + '</td><td>' + esc(h.brokenLinks || 0) + '</td><td>' + hostStateBadge(h) + '</td><td>' + action + '</td></tr>';\n }).join('');\n }\n $('#hostSearch').addEventListener('input', function (e) { state.hostFilter = e.target.value; renderHosts(); });\n $('#hostStateChips').addEventListener('click', function (e) {\n var b = e.target.closest('[data-host-state]');\n if (!b) return;\n state.hostStateFilter = b.dataset.hostState;\n renderHostStateChips();\n renderHosts();\n });\n $('#hostAddBtn').addEventListener('click', function () {\n var body = '<p class=\"label\">注册自定义宿主目录:纳入发现 / 分发 / 收编。不创建目录、不改宿主配置。</p>' +\n '<div class=\"section\"><label class=\"label\">目录(必须已存在)</label><input id=\"hostAddDir\" placeholder=\"例如 D:\\\\my-agent\\\\skills\"></div>' +\n '<div class=\"section\"><label class=\"label\">名称(可选,默认目录名)</label><input id=\"hostAddLabel\" placeholder=\"我的宿主\"></div>';\n openModal('添加自定义目录', body, {\n okText: '注册',\n onOk: async function () {\n var dir = ($('#hostAddDir') && $('#hostAddDir').value || '').trim();\n var label = ($('#hostAddLabel') && $('#hostAddLabel').value || '').trim();\n if (!dir) { toast('请填写目录'); return; }\n var r = await api('/api/hosts/add', { body: { dir: dir, label: label } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已注册:' + ((r.entry && r.entry.label) || dir));\n loadHosts();\n },\n });\n });\n async function openHostPurgeModal(dir) {\n var plan = await api('/api/hosts/purge-plan?dir=' + encodeURIComponent(dir));\n if (plan.error) { toast(plan.error); return; }\n var body = '<p class=\"label\">只删除指向 Hub 的链接(含死链);非 Hub 链接 / 非技能内容随目录保留在回收站(7 天可恢复)。</p>' +\n '<div class=\"result\"><span class=\"badge warn\">将解除链接</span><b>' + esc(plan.unlinkCount) + '</b><span>条(保留 ' + esc(plan.keptLinks) + ' 非 Hub 链接 / ' + esc(plan.keptEntries) + ' 非链接内容)</span></div>' +\n '<div class=\"mono\">' + esc(dir) + '</div>' +\n '<div class=\"section\"><label class=\"label\">输入 purge 确认执行:</label><input id=\"hostPurgeConfirm\" placeholder=\"purge\"></div>';\n confirmModal('清理残留目录?', body, '清理(移入回收站)', true, async function () {\n var typed = ($('#hostPurgeConfirm') && $('#hostPurgeConfirm').value || '').trim();\n if (typed !== 'purge') { toast('确认串不匹配(输入 purge)'); return; }\n var r = await api('/api/hosts/purge', { body: { dir: dir, confirm: 'purge' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已清理:解除 ' + r.unlinkCount + ' 条链接;回收站保留 7 天');\n loadHosts();\n });\n }\n function openHostEditModal(agentId, dir, label) {\n var body = '<p class=\"label\">覆盖「' + esc(label) + '」的技能目录(写入 hosts.json,不改宿主配置)。被覆盖的旧目录不再进入默认发现 / 链接。</p>' +\n '<div class=\"section\"><label class=\"label\">新目录(必须已存在)</label><input id=\"hostEditDir\" value=\"' + esc(dir) + '\"></div>';\n openModal('编辑目录 · ' + esc(agentId), body, {\n okText: '保存覆盖',\n onOk: async function () {\n var value = ($('#hostEditDir') && $('#hostEditDir').value || '').trim();\n if (!value) { toast('请填写目录'); return; }\n var r = await api('/api/hosts/set', { body: { agentId: agentId, dir: value } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已覆盖目录');\n loadHosts();\n },\n });\n }\n $('#hostRows').addEventListener('click', async function (e) {\n var copyBtn = e.target.closest('[data-copy]');\n if (copyBtn) { copyText(copyBtn.dataset.copy); return; }\n var purgeBtn = e.target.closest('[data-host-purge]');\n if (purgeBtn) { openHostPurgeModal(purgeBtn.dataset.hostPurge); return; }\n var editBtn = e.target.closest('[data-host-edit]');\n if (editBtn) {\n openHostEditModal(editBtn.dataset.hostEdit, editBtn.dataset.hostDir, editBtn.dataset.hostLabel);\n return;\n }\n var clearBtn = e.target.closest('[data-host-clear]');\n if (clearBtn) {\n var clearAgent = clearBtn.dataset.hostClear;\n confirmModal('恢复默认目录?', '<p class=\"label\">删除目录覆盖,恢复内置映射。</p><div class=\"mono\">' + esc(clearAgent) + '</div>', '恢复默认', false, async function () {\n var r = await api('/api/hosts/set', { body: { agentId: clearAgent, clear: true } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已恢复内置映射');\n loadHosts();\n });\n return;\n }\n var renameBtn = e.target.closest('[data-host-rename]');\n if (renameBtn) {\n var renameDir = renameBtn.dataset.hostRename;\n var oldLabel = renameBtn.dataset.hostLabel;\n openModal('重命名宿主 · ' + esc(oldLabel),\n '<p class=\"label\">只改显示名;目录、手动状态与目录覆盖保持不动。</p>' +\n '<div class=\"section\"><label class=\"label\">新名称</label><input id=\"hostRenameLabel\" value=\"' + esc(oldLabel) + '\"></div>', {\n okText: '保存',\n onOk: async function () {\n var value = ($('#hostRenameLabel') && $('#hostRenameLabel').value || '').trim();\n if (!value) { toast('请填写新名称'); return; }\n var r = await api('/api/hosts/rename', { body: { dir: renameDir, label: value } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已重命名:' + (r.previous ? r.previous + ' → ' : '') + value);\n loadHosts();\n },\n });\n return;\n }\n var excludeBtn = e.target.closest('[data-host-exclude]');\n if (excludeBtn) {\n var excludeTarget = excludeBtn.dataset.hostExclude;\n var excludeLinks = Number(excludeBtn.dataset.hostLinks || 0);\n var excludeBody = '<p class=\"label\">该宿主 / 目录将从发现、显示、链接三层全部跳过(--include-discovered 也不纳入);已有链接不受影响。</p>' +\n (excludeLinks > 0\n ? '<div class=\"result\"><span class=\"badge warn\">影响面</span><b>' + esc(excludeLinks) + '</b><span>条链接仍在该目录(用 hub unlink --dir 或「链接与体检」逐条解除)</span></div>'\n : '<p class=\"faint\">该目录当前没有指向 Hub 的链接。</p>') +\n '<div class=\"mono\">' + esc(excludeTarget) + '</div>';\n confirmModal('加入「不接管」名单?', excludeBody, '不接管', false, async function () {\n var r = await api('/api/hosts/exclude', { body: { target: excludeTarget } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已加入不接管名单');\n loadHosts();\n });\n return;\n }\n var markBtn = e.target.closest('[data-host-mark]');\n if (markBtn) {\n var r = await api('/api/hosts/mark', { body: { dir: markBtn.dataset.hostMark, state: markBtn.dataset.markState } });\n if (r.error) { toast(r.error); return; }\n var markLabels = { available: '可用', orphan: '残留', ignored: '隐藏标记' };\n toast('已标记:' + (markLabels[markBtn.dataset.markState] || markBtn.dataset.markState));\n loadHosts();\n return;\n }\n var unregBtn = e.target.closest('[data-host-unregister]');\n if (unregBtn) {\n var dir = unregBtn.dataset.hostUnregister;\n var unregLinks = Number(unregBtn.dataset.hostLinks || 0);\n var unregBody = '<p class=\"label\">只移除自定义宿主注册,不删除目录、不改宿主配置。</p>' +\n (unregLinks > 0\n ? '<div class=\"result\"><span class=\"badge warn\">影响面</span><b>' + esc(unregLinks) + '</b><span>条链接仍在该目录(移除注册不清理链接;用 hub unlink --dir 清理)</span></div>'\n : '<p class=\"faint\">该目录当前没有指向 Hub 的链接。</p>') +\n '<div class=\"mono\">' + esc(dir) + '</div>';\n confirmModal('移除注册?', unregBody, '移除注册', false, async function () {\n var r = await api('/api/hosts/remove', { body: { dir: dir } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已移除注册(目录保留)');\n loadHosts();\n });\n }\n });\n $('#hostExcluded').addEventListener('click', async function (e) {\n var includeBtn = e.target.closest('[data-host-include]');\n if (!includeBtn) return;\n var r = await api('/api/hosts/include', { body: { target: includeBtn.dataset.hostInclude } });\n if (r.error) { toast(r.error); return; }\n toast('已恢复接管');\n loadHosts();\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.hubLinksOther ? ' · <span class=\"badge warn\">其它 Hub ' + esc(h.hubLinksOther) + '</span>' : '') + (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 seq = showLoad('正在生成路由与编排…');\n $('#routeBtn').disabled = true;\n var d = await api('/api/route?request=' + encodeURIComponent(request));\n hideLoad(seq);\n $('#routeBtn').disabled = false;\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 /* 高级 CLI:/api/help 单一真源(与 CLI --help 同源) */\n function findHelpEntry(groups, ref) {\n var seg = String(ref).split(' ');\n for (var i = 0; i < groups.length; i++) {\n var cmds = groups[i].commands || [];\n for (var j = 0; j < cmds.length; j++) {\n var c = cmds[j];\n if (c.name !== seg[0]) continue;\n if (seg.length === 1) return c;\n var subs = c.subcommands || [];\n for (var k = 0; k < subs.length; k++) if (subs[k].name === seg[1]) return subs[k];\n }\n }\n return null;\n }\n function renderCliHelp(d) {\n if (!d || d.error) { $('#cliMeta').textContent = '帮助加载失败'; return; }\n var total = 0, optTotal = (d.options || []).length;\n (d.groups || []).forEach(function (g) {\n (g.commands || []).forEach(function (c) {\n total += 1 + (c.subcommands || []).length;\n optTotal += (c.options || []).length;\n (c.subcommands || []).forEach(function (s) { optTotal += (s.options || []).length; });\n });\n });\n $('#cliMeta').textContent = 'v' + (d.version || '-') + ' · 命令 ' + total + ' · 选项 ' + optTotal;\n $('#cliQuick').innerHTML = (d.quick || []).map(function (g) {\n return '<div class=\"cmd-group\"><h3>' + esc(g.group) + '</h3>' + (g.items || []).map(function (item) {\n var meta = findHelpEntry(d.groups, item.cmd) || {};\n return '<div class=\"cmd\"><code>' + esc(item.example) + '</code><span class=\"why\">' + esc(meta.what || '') +\n (item.note ? ' · ' + esc(item.note) : '') + '</span>' +\n (item.danger ? '<span class=\"badge danger\">危险</span>' : '') +\n '<button class=\"btn small\" data-copy-cmd=\"' + esc(item.example) + '\">复制</button></div>';\n }).join('') + '</div>';\n }).join('');\n var full = (d.groups || []).map(function (g) {\n return '<div class=\"cmd-group\"><h3>' + esc(g.group) + '</h3>' + (g.commands || []).map(function (c) {\n var opts = (c.options || []).length\n ? '<div class=\"note\">常用选项: ' + c.options.map(function (f) { return '<code>' + esc(f) + '</code>'; }).join(' ') + '</div>'\n : '';\n var subs = (c.subcommands || []).map(function (s) {\n var sOpts = (s.options || []).length\n ? '<div class=\"note\">选项: ' + s.options.map(function (f) { return '<code>' + esc(f) + '</code>'; }).join(' ') + '</div>'\n : '';\n return '<div class=\"cmd\"><code>' + esc('yotta-skills ' + s.usage) + '</code><span class=\"why\">' + esc(s.what || '') + '</span>' + sOpts +\n (s.impact ? '<div class=\"note\">' + esc(s.impact) + '</div>' : '') +\n (s.alternative ? '<div class=\"note\">' + esc(s.alternative) + '</div>' : '') +\n '<button class=\"btn small\" data-copy-cmd=\"' + esc('yotta-skills ' + s.usage) + '\">复制</button></div>';\n }).join('');\n return '<details class=\"cmd-detail\"><summary><code>' + esc('yotta-skills ' + c.usage) + '</code> <span class=\"why\">' + esc(c.what || '') + '</span></summary>' +\n (c.when ? '<div class=\"note\">' + esc(c.when) + '</div>' : '') +\n (c.impact ? '<div class=\"note\">' + esc(c.impact) + '</div>' : '') +\n (c.alternative ? '<div class=\"note\">' + esc(c.alternative) + '</div>' : '') + opts + subs + '</details>';\n }).join('') + '</div>';\n }).join('');\n full += '<div class=\"cmd-group\"><h3>完整选项</h3>' + (d.options || []).map(function (o) {\n return '<div class=\"cmd\"><code>' + esc(o.flag + (o.arg ? ' ' + o.arg : '')) + '</code><span class=\"why\">' + esc(o.what || '') +\n (o.when ? ' · ' + esc(o.when) : '') + (o.caution ? ' · ⚠ ' + esc(o.caution) : '') + '</span></div>';\n }).join('') + '</div>';\n $('#cliFull').innerHTML = full;\n }\n api('/api/help').then(renderCliHelp);\n document.addEventListener('click', function (e) {\n var b = e.target.closest('#view-cli [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 runLoad('overview');\n })();\n})();\n</script>\n</body>\n</html>\n";
|
|
69
|
+
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 .loadbar{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:13px;padding:10px 13px;border:1px dashed var(--line2);border-radius:8px;margin:0 0 14px;background:var(--panel)}\n .spinner{width:14px;height:14px;border-radius:50%;border:2px solid var(--line2);border-top-color:var(--acc);animation:spin .8s linear infinite;flex:none}\n @keyframes spin{to{transform:rotate(360deg)}}\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 .chips{display:flex;gap:6px;flex-wrap:wrap}\n .chip{cursor:pointer;background:transparent}\n .chip.active{color:var(--acc);border-color:rgba(95,191,143,.45);background:rgba(95,191,143,.10)}\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 .cmd-detail{border:1px solid var(--line);border-radius:6px;background:var(--panel2);margin-top:8px;padding:0 11px;min-width:0}\n .cmd-detail summary{cursor:pointer;padding:9px 0;display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}\n .cmd-detail .note{padding:0 0 8px}\n .cmd-detail .cmd{margin:0 0 8px}\n\n /* 0.30.0 UX-05:术语速查 */\n .glossary{border:1px solid var(--line);border-radius:8px;background:var(--panel);padding:0 12px}\n .glossary summary{padding:10px 0}\n .glossary-body{display:grid;gap:6px;padding:0 0 12px;font-size:12px;color:var(--muted)}\n .glossary-body b{color:var(--text)}\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 <div class=\"loadbar\" id=\"loadbar\" hidden><span class=\"spinner\"></span><span id=\"loadbarText\">正在加载…</span></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>\n <label class=\"search section\" style=\"min-width:100%\"><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=\"ovSkillFilter\" placeholder=\"过滤技能(输入后显示全部匹配)\"></label>\n <div class=\"list\" id=\"ovSkills\"></div>\n </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=\"faint\" id=\"ovHubMeta\"></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 <button class=\"btn small\" id=\"hubConfigBtn\">设置新位置</button>\n <button class=\"btn small\" id=\"hubConfigClearBtn\" hidden title=\"清除持久 Hub 位置覆盖,回落默认解析;当前 Hub 有技能时会先展示影响面\">清除覆盖</button>\n </div>\n </div>\n <div class=\"section\" id=\"ovSwitchBanner\" hidden></div>\n <div class=\"section panel\" id=\"ovMigration\" hidden>\n <h3>最近一次迁移</h3>\n <div id=\"ovMigrationBody\"></div>\n <div class=\"toolbar section\">\n <button class=\"btn small danger\" id=\"migrationRollbackBtn\">回滚到原位置</button>\n <button class=\"btn small\" id=\"copyMigrationTrash\">复制回收站路径</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 <div class=\"chips\" id=\"hostStateChips\"></div>\n <button class=\"btn small\" id=\"hostAddBtn\">添加自定义目录</button>\n </div>\n <details class=\"glossary section\">\n <summary>术语速查</summary>\n <div class=\"glossary-body\">\n <div><b>可用</b>:宿主实体确认、技能目录存在(默认分发范围)。</div>\n <div><b>残留</b>:宿主实体未确认(疑似已卸载);可用「清理」把目录移入回收站。</div>\n <div><b>仅标记</b>:检测到宿主已安装,但技能目录尚未创建。</div>\n <div><b>隐藏标记</b>:只影响列表显示,不拦截任何行为(ignored)。</div>\n <div><b>不接管</b>:发现 / 显示 / 链接三层全部跳过;已有链接不动(真正拦截行为的开关)。</div>\n <div><b>清理</b>:解除指向 Hub 的链接 + 目录移入回收站(保留 7 天可恢复)。</div>\n <div><b>桥接</b>:锁 / 数据目录(如 XDG state),永不作为链接目标。</div>\n </div>\n </details>\n <div class=\"section panel\">\n <div class=\"faint\" id=\"hostSummary\"></div>\n <table><thead><tr><th>宿主</th><th>技能目录</th><th>范围</th><th>目录技能</th><th title=\"只统计指向当前生效 Hub 的链接(文件系统为真源);指向其它位置的链接单独标注(配置错位预警)\">指向当前 Hub</th><th>异常</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 <div class=\"section panel\" id=\"hostExcludedWrap\" hidden>\n <h3>不接管名单</h3>\n <div class=\"faint\">这些宿主 / 目录在发现、显示、链接三层全部跳过(--include-discovered 也不纳入)。</div>\n <div class=\"list\" id=\"hostExcluded\"></div>\n </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 class=\"section panel\">\n <div class=\"shead\"><h2>命令速查</h2><span class=\"faint\" id=\"cliMeta\">加载中…</span></div>\n <div id=\"cliQuick\"></div>\n </div>\n <div class=\"section panel\">\n <details id=\"cliFullWrap\">\n <summary>完整命令与参数(点击展开)</summary>\n <div id=\"cliFull\"></div>\n </details>\n </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: '', hostStateFilter: '', adoptFilter: '', ovSkillFilter: '' };\n\n function $(sel) { return document.querySelector(sel); }\n function esc(value) {\n return String(value === null || value === undefined ? '' : value)\n .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n .replace(/\"/g, '"').replace(/'/g, ''');\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: '全量命令速查与复制(与 CLI --help 同源)', load: null }\n };\n var loadSeq = 0;\n function showLoad(text) {\n loadSeq += 1;\n $('#loadbarText').textContent = text;\n $('#loadbar').hidden = false;\n $('#refreshBtn').disabled = true;\n return loadSeq;\n }\n function hideLoad(seq) {\n if (seq !== undefined && seq !== loadSeq) return;\n $('#loadbar').hidden = true;\n $('#refreshBtn').disabled = false;\n }\n async function runLoad(name) {\n var v = VIEWS[name];\n if (!v) return;\n if (!v.load) { loadSeq += 1; hideLoad(); return; }\n var seq = showLoad('正在加载' + v.title + '…');\n try { await v.load(); } finally { hideLoad(seq); }\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 runLoad(name);\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) runLoad(state.view); 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 renderOverviewSkills();\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 var hubInfo = d.hub || {};\n var sourceNames = { flag: '--hub 参数', env: 'YOTTA_SKILLS_HUB 环境变量', config: 'config.json 配置', default: '默认位置' };\n var metaParts = ['来源: ' + (sourceNames[hubInfo.source] || hubInfo.source || '默认位置')];\n if (hubInfo.configured) metaParts.push('配置覆盖: ' + hubInfo.configured);\n if (hubInfo.restartRequired) metaParts.push('配置已指向 ' + (hubInfo.effectiveNow || '') + ',重启 view 后生效');\n $('#ovHubMeta').textContent = metaParts.join(' · ');\n $('#hubConfigClearBtn').hidden = !hubInfo.configured;\n renderMigrationBlock(d);\n }\n\n /* 0.29.5 S1/S2:迁移向导(迁移 / 仅切换)+ 最近迁移 + 一键回滚(与 CLI 同内核) */\n function renderMigrationBlock(d) {\n var mig = (d.migration && d.migration.last) || null;\n var pending = (d.migration && d.migration.switchPending) || null;\n var banner = $('#ovSwitchBanner');\n if (pending) {\n banner.hidden = false;\n banner.innerHTML = '<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>' +\n '<div><div class=\"banner-title\">旧 Hub 还有 ' + esc(pending.skills) + ' 个技能未迁移</div>' +\n '<div class=\"banner-body\">旧位置 <span class=\"mono\">' + esc(pending.from) + '</span> · 当前位置 <span class=\"mono\">' + esc(d.hubDir) + '</span></div>' +\n '<div class=\"toolbar section\"><button class=\"btn small primary\" id=\"switchMigrateNow\">一键迁移到当前位置</button>' +\n '<button class=\"btn small\" id=\"switchMigrateOpen\">迁移到其它位置…</button></div></div></div>';\n $('#switchMigrateNow').onclick = function () {\n confirmModal('迁移旧 Hub 到当前位置?',\n '<p class=\"label\">将把 <span class=\"mono\">' + esc(pending.from) + '</span>(' + esc(pending.skills) + ' 个技能)复制到 <span class=\"mono\">' + esc(d.hubDir) + '</span>,逐技能校验后切换配置、重链全部宿主,旧 Hub 入回收站(7 天可回滚)。</p>',\n '开始迁移', false, async function () {\n await executeHubMigration({ hub: d.hubDir, mode: 'move' }, { from: pending.from });\n });\n };\n $('#switchMigrateOpen').onclick = function () { openHubConfigWizard(); };\n } else {\n banner.hidden = true;\n banner.innerHTML = '';\n }\n var box = $('#ovMigration');\n if (!mig) {\n box.hidden = true;\n return;\n }\n box.hidden = false;\n var kind = mig.kind === 'rollback' ? '回滚' : '迁移';\n var lines = [\n '最近一次' + kind + ':' + esc(mig.from) + ' → ' + esc(mig.to),\n '时间 ' + esc(fmtTime(mig.at)) + ' · 技能 ' + esc(mig.verifiedSkills) + ' · 重链 ' + esc(mig.relinkDirs) + ' 个宿主目录',\n ];\n if (mig.trashedTo) lines.push('回收站 ' + esc(mig.trashedTo) + '(剩余 ' + esc(mig.daysLeft) + ' 天)');\n if (mig.incomplete > 0) lines.push('⚠ 重链未全部完成(' + esc(mig.incomplete) + ' 项);旧 Hub 保留在 ' + esc(mig.oldHubKept || mig.from));\n if (mig.trashError) lines.push('⚠ 旧 Hub 移入回收站失败(已保留原位):' + esc(mig.trashError));\n $('#ovMigrationBody').innerHTML = '<div class=\"faint\">' + lines.join('<br>') + '</div>';\n $('#copyMigrationTrash').hidden = !mig.trashedTo;\n $('#copyMigrationTrash').onclick = function () { copyText(mig.trashedTo); };\n $('#migrationRollbackBtn').onclick = rollbackFlow;\n }\n\n function rewireModalButtons(opts) {\n var ok = $('#modal').querySelector('[data-modal=\"ok\"]');\n var cancel = $('#modal').querySelector('[data-modal=\"cancel\"]');\n if (ok && opts.okText) { ok.textContent = opts.okText; ok.disabled = false; ok.onclick = opts.onOk || closeModal; }\n if (ok && opts.hideOk) ok.hidden = true;\n if (cancel && opts.cancelText) { cancel.textContent = opts.cancelText; cancel.onclick = opts.onCancel || closeModal; }\n }\n\n function showMigrationResult(result) {\n var move = result.move || {};\n var done = !move.incomplete && !move.trashError;\n $('#modalBody').innerHTML =\n '<div class=\"result\"><span class=\"badge ' + (done ? 'ok' : 'danger') + '\">' + (done ? '迁移完成' : '部分完成') + '</span><span>' + esc(result.previous || '') + ' → ' + esc(result.hub || '') + '</span></div>' +\n '<div class=\"result\"><span class=\"badge ok\">校验</span><span>' + esc(move.verifiedSkills || 0) + ' 个技能复制校验通过</span></div>' +\n '<div class=\"result\"><span class=\"badge ok\">重链</span><span>' + esc(move.relinkDirs || 0) + ' 个宿主目录</span></div>' +\n (move.trashedTo ? '<div class=\"result\"><span class=\"badge ok\">旧 Hub</span><span class=\"mono\">' + esc(move.trashedTo) + '</span><span>已入回收站(7 天可恢复)</span></div>' : '') +\n (move.incomplete > 0 ? '<div class=\"result\"><span class=\"badge danger\">重链未全部完成</span><span>旧 Hub 保留在 <span class=\"mono\">' + esc(move.oldHubKept || move.from || '') + '</span>;重跑:yotta-skills hub link --all --force</span></div>' : '') +\n (move.trashError ? '<div class=\"result\"><span class=\"badge warn\">回收站</span><span>旧 Hub 移入回收站失败(已保留原位):' + esc(move.trashError) + '</span></div>' : '') +\n '<div class=\"note\">新位置从下次面板加载开始生效(已在运行的 view 需重启)。</div>';\n rewireModalButtons({\n okText: '关闭',\n onOk: closeModal,\n cancelText: '刷新面板',\n onCancel: function () { closeModal(); loadOverview(); },\n });\n }\n\n function showSwitchResult(result) {\n $('#modalBody').innerHTML =\n '<div class=\"result\"><span class=\"badge ok\">已切换</span><span>' + esc(result.previous || '') + ' → ' + esc(result.hub || '') + '</span></div>' +\n ((result.oldSkills || 0) > 0\n ? '<div class=\"result\"><span class=\"badge warn\">未迁移</span><span>旧 Hub 还有 ' + esc(result.oldSkills) + ' 个技能未迁移;概览会持续提示,可一键迁移。</span></div>'\n : '') +\n '<div class=\"note\">只切换了指针,没有移动任何文件;新位置从下次面板加载开始生效。</div>';\n rewireModalButtons({\n okText: '关闭',\n onOk: closeModal,\n cancelText: '刷新面板',\n onCancel: function () { closeModal(); loadOverview(); },\n });\n }\n\n async function executeHubMigration(body, opts) {\n opts = opts || {};\n var seq = showLoad(body.mode === 'switch' ? '正在切换 Hub 位置…' : '正在迁移 Hub(复制 → 校验 → 重链 → 回收站)…');\n var payload = Object.assign({ confirm: 'hub-config' }, body);\n if (opts.from) payload = Object.assign(payload, { from: opts.from });\n var r = await api('/api/hub/config', { body: payload });\n hideLoad(seq);\n if (r.error) {\n if (r.code === 'target-residue') return wizardResiduePrompt(body, r.error, opts);\n if (opts.inModal) {\n $('#hubWizardResult').innerHTML = '<div class=\"result\"><span class=\"badge danger\">失败</span><span>' + esc(r.error) + '</span></div>';\n } else {\n toast(r.error);\n }\n return null;\n }\n if (opts.inModal) {\n if (body.mode === 'switch') showSwitchResult(r);\n else showMigrationResult(r);\n }\n else {\n closeModal();\n toast('迁移完成:校验 ' + ((r.move && r.move.verifiedSkills) || 0) + ' 个技能 · 重链 ' + ((r.move && r.move.relinkDirs) || 0) + ' 个目录' + (r.move && r.move.incomplete ? '(重链未全部完成,见概览提示)' : ''));\n loadOverview();\n }\n return r;\n }\n\n function wizardResiduePrompt(body, error, opts) {\n var host = opts && opts.inModal ? $('#hubWizardResult') : $('#modalBody');\n if (!host) { toast(error); return; }\n host.innerHTML = '<div class=\"result\"><span class=\"badge warn\">残留</span><span>' + esc(error) + '</span></div>' +\n '<button class=\"btn small primary\" id=\"hubResidueClean\">清理残留并迁移</button>';\n $('#hubResidueClean').onclick = async function () {\n var seq = showLoad('正在清理残留并迁移…');\n var payload = Object.assign({ confirm: 'hub-config', cleanResidue: true }, body);\n if (opts && opts.from) payload.from = opts.from;\n var r = await api('/api/hub/config', { body: payload });\n hideLoad(seq);\n if (r.error) { toast(r.error); return; }\n if (opts && opts.inModal) showMigrationResult(r);\n else { closeModal(); toast('迁移完成'); loadOverview(); }\n };\n }\n\n function openHubConfigWizard() {\n var d = state.overview || {};\n var current = d.hubDir || '';\n var skillCount = (d.status && d.status.summary && d.status.summary.skills) || 0;\n var body = '<p class=\"label\">写入 config.json 持久覆盖(优先级低于 --hub 参数 / YOTTA_SKILLS_HUB)。</p>' +\n '<label class=\"label\">新的 Hub 目录</label>' +\n '<input id=\"hubNewPath\" placeholder=\"D:\\\\my-hub\" value=\"\">' +\n '<div class=\"note mono\">当前: ' + esc(current) + '(' + esc(skillCount) + ' 个技能)</div>' +\n '<div class=\"section\"><label class=\"label\"><input type=\"radio\" name=\"hubMode\" value=\"move\" checked style=\"width:auto\"> 迁移到新位置(推荐)</label>' +\n '<div class=\"faint\">复制 → 逐技能校验 → 切换配置 → 全宿主重链 → 旧 Hub 入回收站(7 天可回滚)。</div>' +\n '<label class=\"label section\"><input type=\"radio\" name=\"hubMode\" value=\"switch\" style=\"width:auto\"> 仅切换位置(保留旧内容)</label>' +\n '<div class=\"faint\">只改指针,不移动任何文件;旧 Hub 内容保留,概览会提示未迁移。</div></div>' +\n '<div id=\"hubWizardResult\" class=\"section\"></div>';\n openModal('设置 Hub 位置', body, {\n okText: '执行',\n onOk: async function () {\n var value = ($('#hubNewPath').value || '').trim();\n if (!value) { toast('先填写新的 Hub 目录'); return; }\n var mode = document.querySelector('input[name=\"hubMode\"]:checked').value;\n if (mode === 'switch' && skillCount > 0) {\n $('#hubWizardResult').innerHTML = '<div class=\"result\"><span class=\"badge warn\">确认</span><span>旧 Hub 还有 ' + esc(skillCount) + ' 个技能未迁移。仅切换位置不会移动它们。</span></div>' +\n '<button class=\"btn small danger\" id=\"hubSwitchConfirm\">确认仅切换</button>';\n $('#hubSwitchConfirm').onclick = function () { executeHubMigration({ hub: value, mode: 'switch' }, { inModal: true }); };\n return;\n }\n await executeHubMigration({ hub: value, mode: mode }, { inModal: true });\n },\n });\n }\n\n function rollbackFlow() {\n api('/api/hub/config').then(function (d) {\n if (d.error) { toast(d.error); return; }\n var rb = d.rollback || {};\n if (!rb.lastMigration) { toast('没有可回滚的迁移记录'); return; }\n if (!rb.ok) {\n confirmModal('回滚被阻断', '<p class=\"label\">' + esc((rb.blocked || []).join(';') || rb.error || '') + '</p><div class=\"note\">回滚 fail-closed(不合并);处理阻断项后重试。</div>', null, true, null);\n return;\n }\n confirmModal('回滚 Hub 位置?',\n '<p class=\"label\">将把当前 Hub 内容复制回原位置:<span class=\"mono\">' + esc(rb.current) + '</span> → <span class=\"mono\">' + esc(rb.target) + '</span>(复制校验 → 切换 → 重链 → 当前 Hub 入回收站 7 天)。</p>' +\n '<div class=\"note\">位置回退(用当前内容),不是恢复旧快照。</div>',\n '执行回滚', true, async function () {\n var seq = showLoad('正在回滚 Hub 位置(复制 → 校验 → 重链 → 回收站)…');\n var r = await api('/api/hub/config', { body: { rollback: true, confirm: 'hub-config' } });\n hideLoad(seq);\n if (r.error) {\n if (r.code === 'target-residue') { wizardResiduePrompt({ rollback: true }, r.error, {}); return; }\n toast(r.error); return;\n }\n closeModal();\n toast('回滚完成:' + ((r.move && r.move.verifiedSkills) || 0) + ' 个技能已回到 ' + (r.hub || ''));\n loadOverview();\n });\n });\n }\n\n $('#hubConfigBtn').onclick = openHubConfigWizard;\n $('#hubConfigClearBtn').onclick = function () {\n var d = state.overview || {};\n var hubInfo = d.hub || {};\n var skills = (d.status && d.status.summary && d.status.summary.skills) || 0;\n var body = '<p class=\"label\">将删除 config.json 中的 Hub 覆盖,恢复 --hub > 环境变量 > 默认位置的解析。不会移动任何文件。</p>' +\n '<div class=\"result\"><span class=\"badge warn\">影响面</span><b>' + esc(skills) + '</b><span>个技能与已建链接仍在 <span class=\"mono\">' + esc(hubInfo.configured || d.hubDir || '') + '</span>;清除后生效位置回落到默认 <span class=\"mono\">' + esc(hubInfo.defaultDir || '默认位置') + '</span>(可能是空目录),面板 / CLI 将按新位置工作。</span></div>' +\n '<p class=\"faint\">替代路径:迁移到默认位置或其它位置,用「设置新位置」向导(等价 CLI hub config set --hub <路径> --move)。</p>';\n if (skills > 0) {\n body += '<div class=\"section\"><label class=\"label\">输入 clear 确认(当前 Hub 有技能):</label><input id=\"hubClearConfirm\" placeholder=\"clear\"></div>';\n }\n confirmModal('清除 Hub 位置覆盖?', body, '清除覆盖', true, async function () {\n if (skills > 0) {\n var typed = ($('#hubClearConfirm') && $('#hubClearConfirm').value || '').trim();\n if (typed !== 'clear') { toast('确认串不匹配(输入 clear)'); return; }\n }\n var r = await api('/api/hub/config', { body: { clear: true, confirm: 'hub-config' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已清除覆盖;重启 view 后生效');\n loadOverview();\n });\n };\n\n function renderOverviewSkills() {\n var d = state.overview;\n if (!d) return;\n var skills = (d.status && d.status.skills) || [];\n var filter = state.ovSkillFilter.trim().toLowerCase();\n var list = filter\n ? skills.filter(function (s) { return String(s.slug || '').toLowerCase().indexOf(filter) >= 0; })\n : skills.slice(0, 8);\n $('#ovSkills').innerHTML = list.length ? list.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>' +\n '<span class=\"badge ' + (s.status === 'present' ? 'ok' : 'danger') + '\">' + esc(s.status === 'present' ? '已入库' : '缺失') + '</span>' +\n '<div class=\"row-actions\"><button class=\"btn small danger\" data-remove-skill=\"' + esc(s.slug) + '\">删除</button></div></div>';\n }).join('') : '<div class=\"empty\">' + (filter ? '没有匹配的技能。' : 'Hub 还是空的。先在「收编向导」把已装技能收编进来。') + '</div>';\n }\n $('#ovSkillFilter').addEventListener('input', function () {\n state.ovSkillFilter = $('#ovSkillFilter').value || '';\n renderOverviewSkills();\n });\n $('#ovSkills').addEventListener('click', async function (e) {\n var b = e.target.closest('[data-remove-skill]');\n if (!b) return;\n var slug = b.dataset.removeSkill;\n var plan = await api('/api/skills/remove-plan?slug=' + encodeURIComponent(slug));\n if (!plan || plan.error) { toast((plan && plan.error) || '影响预览失败'); return; }\n var targets = plan.targets || [];\n var willUnlink = targets.filter(function (t) { return t.action === 'unlink'; });\n var kept = targets.filter(function (t) { return t.action === 'keep'; });\n var rows = targets.map(function (t) {\n var tone = t.action === 'unlink' ? 'warn' : t.action === 'keep' ? 'info' : '';\n return '<div class=\"result\"><span class=\"badge ' + tone + '\">' + esc(t.action === 'unlink' ? '将清理' : t.action === 'keep' ? '保留' : '无动作') + '</span><span class=\"mono\">' + esc(t.dir) + '</span><span class=\"faint\">' + esc(t.note || '') + '</span></div>';\n }).join('');\n var body = '<p class=\"label\">将清理 ' + willUnlink.length + ' 条宿主链接(含死链)' +\n (plan.hubPresent ? ',并把 Hub 目录移入回收站(保留 7 天)' : ';Hub 目录已缺失,仅清理残留') +\n ';保留 ' + kept.length + ' 项宿主真副本 / 外部链接。</p>' +\n '<div class=\"result-list\">' + (rows || '<div class=\"faint\">没有需要处理的宿主目录。</div>') + '</div>' +\n '<div class=\"section\"><label class=\"label\">输入完整技能名确认:' + esc(slug) + '</label><input id=\"removeConfirmSlug\" placeholder=\"' + esc(slug) + '\"></div>';\n confirmModal('删除 Hub 技能 ' + slug + '?', body, '删除', true, async function () {\n var typed = ($('#removeConfirmSlug') && $('#removeConfirmSlug').value || '').trim();\n if (typed !== slug) { toast('技能名不匹配'); return; }\n var r = await api('/api/skills/remove', { body: { slug: slug, confirm: 'remove', confirmSlug: typed } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast(r.verdict === 'cleanup-only' ? '已清理残留(Hub 目录原本缺失)' : '已删除 ' + slug + '(目录已入回收站)');\n loadOverview();\n });\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 var s = d.summary.states || {};\n $('#hostSummary').textContent = '可用 ' + (s.available || 0) + ' · 残留 ' + (s.orphan || 0) + ' · 未创建 ' + (s.missing || 0) + ' · 仅标记 ' + (s.markerOnly || 0) + (s.ignored ? ' · 隐藏标记 ' + s.ignored : '') + (d.summary.excluded ? ' · 不接管 ' + d.summary.excluded : '') + ' · 链接 ' + d.summary.links + '(异常 ' + d.summary.brokenLinks + ')' + (d.summary.otherHubLinks ? ' · 指向其它 Hub ' + d.summary.otherHubLinks : '');\n renderHostStateChips();\n renderHosts();\n var marks = d.unpaired || [];\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 || '') + ' · 未发现技能目录</div></div><span class=\"badge info\">仅标记</span></div>';\n }).join('') : '<div class=\"empty\">没有未配对的宿主标记。</div>';\n renderHostExcluded(d.excluded || []);\n }\n function renderHostExcluded(list) {\n $('#hostExcludedWrap').hidden = list.length === 0;\n $('#hostExcluded').innerHTML = list.map(function (item) {\n return '<div class=\"row\"><div class=\"row-main\"><div class=\"row-title\">' + esc(item.label || item.value) + '</div><div class=\"row-meta mono\">' + esc(item.value) + ' · ' + esc(item.kind === 'agent' ? '智能体' : '目录') + '</div></div><div class=\"row-actions\"><button class=\"btn small\" data-host-include=\"' + esc(item.value) + '\">恢复接管</button></div></div>';\n }).join('');\n }\n function renderHostStateChips() {\n var d = state.hosts; if (!d) return;\n var s = d.summary.states || {};\n var chips = [\n ['', '全部'],\n ['available', '可用 ' + (s.available || 0)],\n ['orphan', '残留 ' + (s.orphan || 0)],\n ['missing', '未创建 ' + (s.missing || 0)],\n ['marker-only', '仅标记 ' + (s.markerOnly || 0)],\n ['ignored', '隐藏标记 ' + (s.ignored || 0)],\n ];\n $('#hostStateChips').innerHTML = chips.map(function (c) {\n return '<button class=\"chip' + (state.hostStateFilter === c[0] ? ' active' : '') + '\" data-host-state=\"' + esc(c[0]) + '\">' + esc(c[1]) + '</button>';\n }).join('');\n }\n function hostStateBadge(h) {\n if (h.bridgeOnly) return '<span class=\"badge info\">桥接</span>';\n var map = { available: ['ok', '可用'], orphan: ['warn', '残留'], missing: ['', '未创建'], 'marker-only': ['info', '仅标记'], ignored: ['', '隐藏标记'] };\n var item = map[h.state] || ['', h.state || '未知'];\n return '<span class=\"badge ' + item[0] + '\">' + esc(item[1]) + (h.stateSource === 'manual' ? '·手动' : '') + '</span>' + (h.brokenLinks ? ' <span class=\"badge danger\">异常</span>' : '');\n }\n function renderHosts() {\n var d = state.hosts; if (!d) return;\n var q = state.hostFilter.toLowerCase();\n var sf = state.hostStateFilter;\n var rows = (d.hosts || []).filter(function (h) {\n if (sf && h.state !== sf) return false;\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 var overrideIds = {};\n ((d.overrides) || []).forEach(function (o) { overrideIds[String(o.agentId).toLowerCase()] = true; });\n $('#hostRows').innerHTML = rows.map(function (h) {\n var scopeBadge = h.bridgeOnly ? '<span class=\"badge info\">桥接·不链</span>'\n : (h.detection === 'user-registry' ? '<span class=\"badge info\">用户注册</span>'\n : (h.detection === 'override' ? '<span class=\"badge ok\">目录覆盖</span>'\n : (h.verified ? '<span class=\"badge ok\">已核实</span>'\n : ((h.detection === 'discovered' || h.detection === 'workspace') ? '<span class=\"badge warn\">自动发现</span>' : '<span class=\"badge warn\">映射·未核实</span>'))));\n var action = '';\n if (h.exists && h.state === 'orphan' && !h.bridgeOnly) action += '<button class=\"btn small danger\" data-host-purge=\"' + esc(h.dir) + '\" title=\"清理残留:解除指向 Hub 的链接 + 目录移入回收站(7 天可恢复)\">清理</button> ';\n if (h.state === 'ignored') action += '<button class=\"btn small\" data-host-mark=\"' + esc(h.dir) + '\" data-mark-state=\"available\" title=\"恢复为可用状态(仅状态标记)\">标记可用</button> ';\n else if (h.exists && !h.bridgeOnly) action += '<button class=\"btn small\" data-host-mark=\"' + esc(h.dir) + '\" data-mark-state=\"ignored\" title=\"只影响列表显示,不拦截行为(拦截用「不接管」)\">隐藏标记</button> ';\n if (h.agentId && !h.bridgeOnly) {\n if (overrideIds[String(h.agentId).toLowerCase()]) action += '<button class=\"btn small\" data-host-clear=\"' + esc(h.agentId) + '\" title=\"删除目录覆盖,恢复内置映射\">恢复默认</button> ';\n else action += '<button class=\"btn small\" data-host-edit=\"' + esc(h.agentId) + '\" data-host-dir=\"' + esc(h.dir) + '\" data-host-label=\"' + esc(h.label) + '\" title=\"写入目录覆盖(hosts.json),不改宿主配置\">编辑目录</button> ';\n }\n if (!h.bridgeOnly) action += '<button class=\"btn small\" data-host-exclude=\"' + esc(h.agentId || h.dir) + '\" data-host-links=\"' + esc(h.hubLinks || 0) + '\" title=\"加入不接管名单:发现 / 显示 / 链接三层跳过;已有链接不动\">不接管</button> ';\n if (h.detection === 'user-registry') action += '<button class=\"btn small\" data-host-rename=\"' + esc(h.dir) + '\" data-host-label=\"' + esc(h.label) + '\" title=\"只改显示名;目录与手动状态保持\">重命名</button> ';\n if (h.detection === 'user-registry') action += '<button class=\"btn small\" data-host-unregister=\"' + esc(h.dir) + '\" data-host-links=\"' + esc(h.hubLinks || 0) + '\" title=\"只移除注册,不删目录;已有链接不清理(用 hub unlink --dir)\">移除注册</button>';\n var linksCell = esc(h.hubLinks || 0);\n if (h.hubLinksOther) {\n var otherTitle = '指向其它位置的链接(配置错位预警)' + ((h.hubLinksOtherRoots || []).length ? ':' + h.hubLinksOtherRoots.join(' / ') : '');\n linksCell += ' <span class=\"badge warn\" title=\"' + esc(otherTitle) + '\">其它 Hub ' + esc(h.hubLinksOther) + '</span>';\n }\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>' + linksCell + '</td><td>' + esc(h.brokenLinks || 0) + '</td><td>' + hostStateBadge(h) + '</td><td>' + action + '</td></tr>';\n }).join('');\n }\n $('#hostSearch').addEventListener('input', function (e) { state.hostFilter = e.target.value; renderHosts(); });\n $('#hostStateChips').addEventListener('click', function (e) {\n var b = e.target.closest('[data-host-state]');\n if (!b) return;\n state.hostStateFilter = b.dataset.hostState;\n renderHostStateChips();\n renderHosts();\n });\n $('#hostAddBtn').addEventListener('click', function () {\n var body = '<p class=\"label\">注册自定义宿主目录:纳入发现 / 分发 / 收编。不创建目录、不改宿主配置。</p>' +\n '<div class=\"section\"><label class=\"label\">目录(必须已存在)</label><input id=\"hostAddDir\" placeholder=\"例如 D:\\\\my-agent\\\\skills\"></div>' +\n '<div class=\"section\"><label class=\"label\">名称(可选,默认目录名)</label><input id=\"hostAddLabel\" placeholder=\"我的宿主\"></div>';\n openModal('添加自定义目录', body, {\n okText: '注册',\n onOk: async function () {\n var dir = ($('#hostAddDir') && $('#hostAddDir').value || '').trim();\n var label = ($('#hostAddLabel') && $('#hostAddLabel').value || '').trim();\n if (!dir) { toast('请填写目录'); return; }\n var r = await api('/api/hosts/add', { body: { dir: dir, label: label } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已注册:' + ((r.entry && r.entry.label) || dir));\n loadHosts();\n },\n });\n });\n async function openHostPurgeModal(dir) {\n var plan = await api('/api/hosts/purge-plan?dir=' + encodeURIComponent(dir));\n if (plan.error) { toast(plan.error); return; }\n var body = '<p class=\"label\">只删除指向 Hub 的链接(含死链);非 Hub 链接 / 非技能内容随目录保留在回收站(7 天可恢复)。</p>' +\n '<div class=\"result\"><span class=\"badge warn\">将解除链接</span><b>' + esc(plan.unlinkCount) + '</b><span>条(保留 ' + esc(plan.keptLinks) + ' 非 Hub 链接 / ' + esc(plan.keptEntries) + ' 非链接内容)</span></div>' +\n '<div class=\"mono\">' + esc(dir) + '</div>' +\n '<div class=\"section\"><label class=\"label\">输入 purge 确认执行:</label><input id=\"hostPurgeConfirm\" placeholder=\"purge\"></div>';\n confirmModal('清理残留目录?', body, '清理(移入回收站)', true, async function () {\n var typed = ($('#hostPurgeConfirm') && $('#hostPurgeConfirm').value || '').trim();\n if (typed !== 'purge') { toast('确认串不匹配(输入 purge)'); return; }\n var r = await api('/api/hosts/purge', { body: { dir: dir, confirm: 'purge' } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已清理:解除 ' + r.unlinkCount + ' 条链接;回收站保留 7 天');\n loadHosts();\n });\n }\n function openHostEditModal(agentId, dir, label) {\n var body = '<p class=\"label\">覆盖「' + esc(label) + '」的技能目录(写入 hosts.json,不改宿主配置)。被覆盖的旧目录不再进入默认发现 / 链接。</p>' +\n '<div class=\"section\"><label class=\"label\">新目录(必须已存在)</label><input id=\"hostEditDir\" value=\"' + esc(dir) + '\"></div>';\n openModal('编辑目录 · ' + esc(agentId), body, {\n okText: '保存覆盖',\n onOk: async function () {\n var value = ($('#hostEditDir') && $('#hostEditDir').value || '').trim();\n if (!value) { toast('请填写目录'); return; }\n var r = await api('/api/hosts/set', { body: { agentId: agentId, dir: value } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已覆盖目录');\n loadHosts();\n },\n });\n }\n $('#hostRows').addEventListener('click', async function (e) {\n var copyBtn = e.target.closest('[data-copy]');\n if (copyBtn) { copyText(copyBtn.dataset.copy); return; }\n var purgeBtn = e.target.closest('[data-host-purge]');\n if (purgeBtn) { openHostPurgeModal(purgeBtn.dataset.hostPurge); return; }\n var editBtn = e.target.closest('[data-host-edit]');\n if (editBtn) {\n openHostEditModal(editBtn.dataset.hostEdit, editBtn.dataset.hostDir, editBtn.dataset.hostLabel);\n return;\n }\n var clearBtn = e.target.closest('[data-host-clear]');\n if (clearBtn) {\n var clearAgent = clearBtn.dataset.hostClear;\n confirmModal('恢复默认目录?', '<p class=\"label\">删除目录覆盖,恢复内置映射。</p><div class=\"mono\">' + esc(clearAgent) + '</div>', '恢复默认', false, async function () {\n var r = await api('/api/hosts/set', { body: { agentId: clearAgent, clear: true } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已恢复内置映射');\n loadHosts();\n });\n return;\n }\n var renameBtn = e.target.closest('[data-host-rename]');\n if (renameBtn) {\n var renameDir = renameBtn.dataset.hostRename;\n var oldLabel = renameBtn.dataset.hostLabel;\n openModal('重命名宿主 · ' + esc(oldLabel),\n '<p class=\"label\">只改显示名;目录、手动状态与目录覆盖保持不动。</p>' +\n '<div class=\"section\"><label class=\"label\">新名称</label><input id=\"hostRenameLabel\" value=\"' + esc(oldLabel) + '\"></div>', {\n okText: '保存',\n onOk: async function () {\n var value = ($('#hostRenameLabel') && $('#hostRenameLabel').value || '').trim();\n if (!value) { toast('请填写新名称'); return; }\n var r = await api('/api/hosts/rename', { body: { dir: renameDir, label: value } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已重命名:' + (r.previous ? r.previous + ' → ' : '') + value);\n loadHosts();\n },\n });\n return;\n }\n var excludeBtn = e.target.closest('[data-host-exclude]');\n if (excludeBtn) {\n var excludeTarget = excludeBtn.dataset.hostExclude;\n var excludeLinks = Number(excludeBtn.dataset.hostLinks || 0);\n var excludeBody = '<p class=\"label\">该宿主 / 目录将从发现、显示、链接三层全部跳过(--include-discovered 也不纳入);已有链接不受影响。</p>' +\n (excludeLinks > 0\n ? '<div class=\"result\"><span class=\"badge warn\">影响面</span><b>' + esc(excludeLinks) + '</b><span>条链接仍在该目录(用 hub unlink --dir 或「链接与体检」逐条解除)</span></div>'\n : '<p class=\"faint\">该目录当前没有指向 Hub 的链接。</p>') +\n '<div class=\"mono\">' + esc(excludeTarget) + '</div>';\n confirmModal('加入「不接管」名单?', excludeBody, '不接管', false, async function () {\n var r = await api('/api/hosts/exclude', { body: { target: excludeTarget } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已加入不接管名单');\n loadHosts();\n });\n return;\n }\n var markBtn = e.target.closest('[data-host-mark]');\n if (markBtn) {\n var r = await api('/api/hosts/mark', { body: { dir: markBtn.dataset.hostMark, state: markBtn.dataset.markState } });\n if (r.error) { toast(r.error); return; }\n var markLabels = { available: '可用', orphan: '残留', ignored: '隐藏标记' };\n toast('已标记:' + (markLabels[markBtn.dataset.markState] || markBtn.dataset.markState));\n loadHosts();\n return;\n }\n var unregBtn = e.target.closest('[data-host-unregister]');\n if (unregBtn) {\n var dir = unregBtn.dataset.hostUnregister;\n var unregLinks = Number(unregBtn.dataset.hostLinks || 0);\n var unregBody = '<p class=\"label\">只移除自定义宿主注册,不删除目录、不改宿主配置。</p>' +\n (unregLinks > 0\n ? '<div class=\"result\"><span class=\"badge warn\">影响面</span><b>' + esc(unregLinks) + '</b><span>条链接仍在该目录(移除注册不清理链接;用 hub unlink --dir 清理)</span></div>'\n : '<p class=\"faint\">该目录当前没有指向 Hub 的链接。</p>') +\n '<div class=\"mono\">' + esc(dir) + '</div>';\n confirmModal('移除注册?', unregBody, '移除注册', false, async function () {\n var r = await api('/api/hosts/remove', { body: { dir: dir } });\n if (r.error) { toast(r.error); return; }\n closeModal();\n toast('已移除注册(目录保留)');\n loadHosts();\n });\n }\n });\n $('#hostExcluded').addEventListener('click', async function (e) {\n var includeBtn = e.target.closest('[data-host-include]');\n if (!includeBtn) return;\n var r = await api('/api/hosts/include', { body: { target: includeBtn.dataset.hostInclude } });\n if (r.error) { toast(r.error); return; }\n toast('已恢复接管');\n loadHosts();\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.hubLinksOther ? ' · <span class=\"badge warn\">其它 Hub ' + esc(h.hubLinksOther) + '</span>' : '') + (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 seq = showLoad('正在生成路由与编排…');\n $('#routeBtn').disabled = true;\n var d = await api('/api/route?request=' + encodeURIComponent(request));\n hideLoad(seq);\n $('#routeBtn').disabled = false;\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 /* 高级 CLI:/api/help 单一真源(与 CLI --help 同源;速查解析由服务端按最深前缀完成) */\n function renderCliHelp(d) {\n if (!d || d.error) { $('#cliMeta').textContent = '帮助加载失败'; return; }\n var total = 0, optTotal = (d.options || []).length;\n (d.groups || []).forEach(function (g) {\n (g.commands || []).forEach(function (c) {\n total += 1 + (c.subcommands || []).length;\n optTotal += (c.options || []).length;\n (c.subcommands || []).forEach(function (s) { optTotal += (s.options || []).length; });\n });\n });\n $('#cliMeta').textContent = 'v' + (d.version || '-') + ' · 命令 ' + total + ' · 选项 ' + optTotal;\n $('#cliQuick').innerHTML = (d.quick || []).map(function (g) {\n return '<div class=\"cmd-group\"><h3>' + esc(g.group) + '</h3>' + (g.items || []).map(function (item) {\n var meta = item.meta || {};\n return '<div class=\"cmd\"><code>' + esc(item.example) + '</code><span class=\"why\">' + esc(meta.what || '') +\n (item.note ? ' · ' + esc(item.note) : '') + '</span>' +\n (item.danger ? '<span class=\"badge danger\">危险</span>' : '') +\n '<button class=\"btn small\" data-copy-cmd=\"' + esc(item.example) + '\">复制</button></div>';\n }).join('') + '</div>';\n }).join('');\n var full = (d.groups || []).map(function (g) {\n return '<div class=\"cmd-group\"><h3>' + esc(g.group) + '</h3>' + (g.commands || []).map(function (c) {\n var opts = (c.options || []).length\n ? '<div class=\"note\">常用选项: ' + c.options.map(function (f) { return '<code>' + esc(f) + '</code>'; }).join(' ') + '</div>'\n : '';\n var subs = (c.subcommands || []).map(function (s) {\n var sOpts = (s.options || []).length\n ? '<div class=\"note\">选项: ' + s.options.map(function (f) { return '<code>' + esc(f) + '</code>'; }).join(' ') + '</div>'\n : '';\n return '<div class=\"cmd\"><code>' + esc('yotta-skills ' + s.usage) + '</code><span class=\"why\">' + esc(s.what || '') + '</span>' + sOpts +\n (s.impact ? '<div class=\"note\">' + esc(s.impact) + '</div>' : '') +\n (s.alternative ? '<div class=\"note\">' + esc(s.alternative) + '</div>' : '') +\n '<button class=\"btn small\" data-copy-cmd=\"' + esc('yotta-skills ' + s.usage) + '\">复制</button></div>';\n }).join('');\n return '<details class=\"cmd-detail\"><summary><code>' + esc('yotta-skills ' + c.usage) + '</code> <span class=\"why\">' + esc(c.what || '') + '</span></summary>' +\n (c.when ? '<div class=\"note\">' + esc(c.when) + '</div>' : '') +\n (c.impact ? '<div class=\"note\">' + esc(c.impact) + '</div>' : '') +\n (c.alternative ? '<div class=\"note\">' + esc(c.alternative) + '</div>' : '') + opts + subs + '</details>';\n }).join('') + '</div>';\n }).join('');\n full += '<div class=\"cmd-group\"><h3>完整选项</h3>' + (d.options || []).map(function (o) {\n return '<div class=\"cmd\"><code>' + esc(o.flag + (o.arg ? ' ' + o.arg : '')) + '</code><span class=\"why\">' + esc(o.what || '') +\n (o.when ? ' · ' + esc(o.when) : '') + (o.caution ? ' · ⚠ ' + esc(o.caution) : '') + '</span></div>';\n }).join('') + '</div>';\n $('#cliFull').innerHTML = full;\n }\n api('/api/help').then(renderCliHelp);\n document.addEventListener('click', function (e) {\n var b = e.target.closest('#view-cli [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 runLoad('overview');\n })();\n})();\n</script>\n</body>\n</html>\n";
|
|
70
70
|
// @generated view-html:end
|
|
71
71
|
|
|
72
72
|
function loadManifest() {
|
|
@@ -268,47 +268,47 @@ const CLI_HELP_QUICK = [
|
|
|
268
268
|
{
|
|
269
269
|
group: '安装与更新',
|
|
270
270
|
items: [
|
|
271
|
-
{ cmd: 'install', example: 'yotta-skills install --agent codex' },
|
|
272
|
-
{ cmd: 'install', example: 'yotta-skills install yotta-memory --dir <技能目录>' },
|
|
273
|
-
{ cmd: 'update', example: 'yotta-skills update --installed-only --dir <技能目录>' },
|
|
274
|
-
{ cmd: 'update', example: 'yotta-skills update --check' },
|
|
275
|
-
{ cmd: 'update', example: 'yotta-skills update --auto' },
|
|
276
|
-
{ cmd: '--dry-run', example: 'yotta-skills install --dry-run --agent codex' },
|
|
277
|
-
{ cmd: 'install-self', example: 'yotta-skills install-self' },
|
|
278
|
-
{ cmd: 'where', example: 'yotta-skills where' },
|
|
271
|
+
{ cmd: 'install', example: 'yotta-skills install --agent codex', note: '装全家到指定智能体' },
|
|
272
|
+
{ cmd: 'install', example: 'yotta-skills install yotta-memory --dir <技能目录>', note: '只装一个技能到指定目录' },
|
|
273
|
+
{ cmd: 'update', example: 'yotta-skills update --installed-only --dir <技能目录>', note: '接管现有技能(只升不降)' },
|
|
274
|
+
{ cmd: 'update', example: 'yotta-skills update --check', note: '只读检查 npm 更新(不改动)' },
|
|
275
|
+
{ cmd: 'update', example: 'yotta-skills update --auto', note: '检查到家族更新后自动更新' },
|
|
276
|
+
{ cmd: '--dry-run', example: 'yotta-skills install --dry-run --agent codex', note: '预演安装 / 更新清单(不联网不改动)' },
|
|
277
|
+
{ cmd: 'install-self', example: 'yotta-skills install-self', note: '把元阁装成独立 CLI' },
|
|
278
|
+
{ cmd: 'where', example: 'yotta-skills where', note: '查看运行中 / 独立安装 / Hub 位置' },
|
|
279
279
|
],
|
|
280
280
|
},
|
|
281
281
|
{
|
|
282
282
|
group: '体检与回滚',
|
|
283
283
|
items: [
|
|
284
|
-
{ cmd: 'doctor', example: 'yotta-skills doctor --dir <技能目录>' },
|
|
285
|
-
{ cmd: 'rollback', example: 'yotta-skills rollback --list' },
|
|
286
|
-
{ cmd: 'rollback', example: 'yotta-skills rollback --slug <技能> --dir <技能目录>' },
|
|
284
|
+
{ cmd: 'doctor', example: 'yotta-skills doctor --dir <技能目录>', note: '只读自检技能目录与注册表' },
|
|
285
|
+
{ cmd: 'rollback', example: 'yotta-skills rollback --list', note: '列出可回滚的备份点' },
|
|
286
|
+
{ cmd: 'rollback', example: 'yotta-skills rollback --slug <技能> --dir <技能目录>', note: '回滚指定技能到上一个备份' },
|
|
287
287
|
],
|
|
288
288
|
},
|
|
289
289
|
{
|
|
290
290
|
group: '盘点与路由',
|
|
291
291
|
items: [
|
|
292
|
-
{ cmd: '--list', example: 'yotta-skills --list' },
|
|
293
|
-
{ cmd: '--inventory', example: 'yotta-skills --inventory --json' },
|
|
294
|
-
{ cmd: '--reindex', example: 'yotta-skills --reindex' },
|
|
295
|
-
{ cmd: '--route', example: 'yotta-skills --route "帮我做发布前质量检查"' },
|
|
296
|
-
{ cmd: 'decide-memory', example: 'yotta-skills decide-memory --dry-run' },
|
|
292
|
+
{ cmd: '--list', example: 'yotta-skills --list', note: '列出全家技能与版本' },
|
|
293
|
+
{ cmd: '--inventory', example: 'yotta-skills --inventory --json', note: '盘点本机已装技能' },
|
|
294
|
+
{ cmd: '--reindex', example: 'yotta-skills --reindex', note: '重扫本地技能注册表' },
|
|
295
|
+
{ cmd: '--route', example: 'yotta-skills --route "帮我做发布前质量检查"', note: '按需求给技能组合建议(不自动安装)' },
|
|
296
|
+
{ cmd: 'decide-memory', example: 'yotta-skills decide-memory --dry-run', note: 'M1 记忆裁决只读建议' },
|
|
297
297
|
],
|
|
298
298
|
},
|
|
299
299
|
{
|
|
300
300
|
group: '技能 Hub',
|
|
301
301
|
items: [
|
|
302
|
-
{ cmd: 'hub', example: 'yotta-skills hub' },
|
|
303
|
-
{ cmd: 'hub hosts', example: 'yotta-skills hub hosts' },
|
|
304
|
-
{ cmd: 'hub hosts', example: 'yotta-skills hub hosts --state orphan' },
|
|
305
|
-
{ cmd: 'hub hosts add', example: 'yotta-skills hub hosts add <技能目录>' },
|
|
302
|
+
{ cmd: 'hub', example: 'yotta-skills hub', note: 'Hub 子命令总览' },
|
|
303
|
+
{ cmd: 'hub hosts', example: 'yotta-skills hub hosts', note: '查看全部宿主与技能目录状态' },
|
|
304
|
+
{ cmd: 'hub hosts', example: 'yotta-skills hub hosts --state orphan', note: '只筛选「残留」状态的宿主' },
|
|
305
|
+
{ cmd: 'hub hosts add', example: 'yotta-skills hub hosts add <技能目录>', note: '注册自定义宿主目录(不创建目录)' },
|
|
306
306
|
{ cmd: 'hub hosts rename', example: 'yotta-skills hub hosts rename <技能目录> --label <新名称>', note: '只改显示名;目录与手动状态保持' },
|
|
307
|
-
{ cmd: 'hub hosts list', example: 'yotta-skills hub hosts list' },
|
|
308
|
-
{ cmd: 'hub hosts mark', example: 'yotta-skills hub hosts mark <技能目录> --state orphan' },
|
|
309
|
-
{ cmd: 'hub hosts exclude', example: 'yotta-skills hub hosts exclude <agentId|目录>' },
|
|
310
|
-
{ cmd: 'hub hosts include', example: 'yotta-skills hub hosts include <agentId|目录>' },
|
|
311
|
-
{ cmd: 'hub hosts set', example: 'yotta-skills hub hosts set <agentId> --dir <目录>' },
|
|
307
|
+
{ cmd: 'hub hosts list', example: 'yotta-skills hub hosts list', note: '查看手动注册的自定义宿主' },
|
|
308
|
+
{ cmd: 'hub hosts mark', example: 'yotta-skills hub hosts mark <技能目录> --state orphan', note: '手动标记状态(可用 / 残留 / 隐藏标记)' },
|
|
309
|
+
{ cmd: 'hub hosts exclude', example: 'yotta-skills hub hosts exclude <agentId|目录>', note: '加入「不接管」名单' },
|
|
310
|
+
{ cmd: 'hub hosts include', example: 'yotta-skills hub hosts include <agentId|目录>', note: '恢复接管' },
|
|
311
|
+
{ cmd: 'hub hosts set', example: 'yotta-skills hub hosts set <agentId> --dir <目录>', note: '覆盖某智能体的技能目录映射' },
|
|
312
312
|
{ cmd: 'hub config get', example: 'yotta-skills hub config get', note: '查看生效位置与来源' },
|
|
313
313
|
{ cmd: 'hub config set', example: 'yotta-skills hub config set --hub <Hub目录>', note: '持久化 Hub 位置;迁移旧 Hub 加 --move' },
|
|
314
314
|
{ cmd: 'hub config rollback', example: 'yotta-skills hub config rollback --yes', note: '回滚到上次迁移前的位置(位置回退,用当前内容)', danger: true },
|
|
@@ -316,50 +316,104 @@ const CLI_HELP_QUICK = [
|
|
|
316
316
|
{ cmd: 'hub cleanup-backups', example: 'yotta-skills hub cleanup-backups --yes', note: '历史备份 / 暂存残留移入回收站(7 天可恢复)', danger: true },
|
|
317
317
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录>', note: '只移除注册,不删目录;该目录已有链接不清理(用 hub unlink --dir)' },
|
|
318
318
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录> --purge', danger: true, note: '清理残留目录:先预览,--yes 才执行;目录入回收站保留 7 天' },
|
|
319
|
-
{ cmd: 'hub install', example: 'yotta-skills hub install' },
|
|
320
|
-
{ cmd: 'hub update', example: 'yotta-skills hub update' },
|
|
321
|
-
{ cmd: 'hub adopt', example: 'yotta-skills hub adopt --scan' },
|
|
322
|
-
{ cmd: 'hub adopt', example: 'yotta-skills hub adopt --apply --include <技能>' },
|
|
323
|
-
{ cmd: 'hub refresh', example: 'yotta-skills hub refresh <技能> --from <技能目录>' },
|
|
324
|
-
{ cmd: 'hub link', example: 'yotta-skills hub link --all' },
|
|
319
|
+
{ cmd: 'hub install', example: 'yotta-skills hub install', note: '把元技能装进 Hub 真源' },
|
|
320
|
+
{ cmd: 'hub update', example: 'yotta-skills hub update', note: '更新 Hub 内元技能(只升不降)' },
|
|
321
|
+
{ cmd: 'hub adopt', example: 'yotta-skills hub adopt --scan', note: '只读预演收编候选与冲突' },
|
|
322
|
+
{ cmd: 'hub adopt', example: 'yotta-skills hub adopt --apply --include <技能>', note: '执行收编(逐项元信扫描,high / critical 阻断)' },
|
|
323
|
+
{ cmd: 'hub refresh', example: 'yotta-skills hub refresh <技能> --from <技能目录>', note: '手动同步非元阁技能(需 --from)' },
|
|
324
|
+
{ cmd: 'hub link', example: 'yotta-skills hub link --all', note: '全量分发到全部已核实宿主' },
|
|
325
325
|
{ cmd: 'hub link', example: 'yotta-skills hub link <技能> --agent codex', note: '针对指定智能体分发' },
|
|
326
326
|
{ cmd: 'hub unlink', example: 'yotta-skills hub unlink <技能> --agent codex', note: '只解除该智能体的链接,Hub 真源保留' },
|
|
327
327
|
{ cmd: 'hub unlink', example: 'yotta-skills hub unlink --dir <技能目录>', note: '清理该目录全部指向当前 Hub 的链接(文件系统为真源;其它位置链接不动)' },
|
|
328
|
-
{ cmd: 'hub unlink', example: 'yotta-skills hub unlink --all' },
|
|
328
|
+
{ cmd: 'hub unlink', example: 'yotta-skills hub unlink --all', note: '解除全部已核实宿主的链接' },
|
|
329
329
|
{ cmd: 'hub remove', example: 'yotta-skills hub remove <技能> --dry-run', danger: true, note: '删除 Hub 技能:全宿主清链接(含死链)+ 目录入回收站(7 天)+ 清台账;先 --dry-run 看影响' },
|
|
330
|
-
{ cmd: 'hub status', example: 'yotta-skills hub status' },
|
|
331
|
-
{ cmd: 'hub doctor', example: 'yotta-skills hub doctor' },
|
|
330
|
+
{ cmd: 'hub status', example: 'yotta-skills hub status', note: '查看 Hub 技能 / 版本 / 链接 / 宿主' },
|
|
331
|
+
{ cmd: 'hub doctor', example: 'yotta-skills hub doctor', note: '检查断链 / 缺失 / slug 不一致' },
|
|
332
332
|
],
|
|
333
333
|
},
|
|
334
334
|
{
|
|
335
335
|
group: '使用记录与 Hook',
|
|
336
336
|
items: [
|
|
337
|
-
{ cmd: 'usage', example: 'yotta-skills usage status' },
|
|
338
|
-
{ cmd: 'usage status', example: 'yotta-skills usage status --json' },
|
|
339
|
-
{ cmd: 'usage enable', example: 'yotta-skills usage enable' },
|
|
340
|
-
{ cmd: 'usage disable', example: 'yotta-skills usage disable' },
|
|
341
|
-
{ cmd: 'usage mark', example: 'yotta-skills usage mark --skill yotta-memory --signal used' },
|
|
337
|
+
{ cmd: 'usage', example: 'yotta-skills usage status', note: '使用记录开关与计数总览' },
|
|
338
|
+
{ cmd: 'usage status', example: 'yotta-skills usage status --json', note: '查看开关与计数(JSON)' },
|
|
339
|
+
{ cmd: 'usage enable', example: 'yotta-skills usage enable', note: '开启本地使用记录' },
|
|
340
|
+
{ cmd: 'usage disable', example: 'yotta-skills usage disable', note: '关闭本地使用记录' },
|
|
341
|
+
{ cmd: 'usage mark', example: 'yotta-skills usage mark --skill yotta-memory --signal used', note: '记录一次使用信号' },
|
|
342
342
|
{ cmd: 'usage reset', example: 'yotta-skills usage reset --yes', danger: true, note: '清空本地使用记录,先确认不再需要计数' },
|
|
343
|
-
{ cmd: 'hook', example: 'yotta-skills hook capabilities' },
|
|
344
|
-
{ cmd: 'hook capabilities', example: 'yotta-skills hook capabilities --host codex --json' },
|
|
345
|
-
{ cmd: 'hook evaluate', example: 'yotta-skills hook evaluate --event before_install --manifest <file>' },
|
|
346
|
-
{ cmd: 'hook bind', example: 'yotta-skills hook bind --manifest <file>' },
|
|
347
|
-
{ cmd: 'hook unbind', example: 'yotta-skills hook unbind <binding-id>' },
|
|
343
|
+
{ cmd: 'hook', example: 'yotta-skills hook capabilities', note: 'hook 子命令总览' },
|
|
344
|
+
{ cmd: 'hook capabilities', example: 'yotta-skills hook capabilities --host codex --json', note: '查看宿主六事件能力矩阵' },
|
|
345
|
+
{ cmd: 'hook evaluate', example: 'yotta-skills hook evaluate --event before_install --manifest <file>', note: '评估 hook 声明并留证' },
|
|
346
|
+
{ cmd: 'hook bind', example: 'yotta-skills hook bind --manifest <file>', note: '注册 hook 声明(幂等)' },
|
|
347
|
+
{ cmd: 'hook unbind', example: 'yotta-skills hook unbind <binding-id>', note: '反注册 hook 声明' },
|
|
348
348
|
],
|
|
349
349
|
},
|
|
350
350
|
{
|
|
351
351
|
group: '面板与帮助',
|
|
352
352
|
items: [
|
|
353
|
-
{ cmd: 'view', example: 'yotta-skills view' },
|
|
354
|
-
{ cmd: 'view', example: 'yotta-skills view --port 8790' },
|
|
355
|
-
{ cmd: '--version', example: 'yotta-skills --version' },
|
|
356
|
-
{ cmd: '--help', example: 'yotta-skills --help' },
|
|
353
|
+
{ cmd: 'view', example: 'yotta-skills view', note: '打开本地控制面板(仅 127.0.0.1)' },
|
|
354
|
+
{ cmd: 'view', example: 'yotta-skills view --port 8790', note: '指定端口打开(默认 8789)' },
|
|
355
|
+
{ cmd: '--version', example: 'yotta-skills --version', note: '查看当前版本' },
|
|
356
|
+
{ cmd: '--help', example: 'yotta-skills --help', note: '查看终端帮助(与面板同一真源)' },
|
|
357
357
|
],
|
|
358
358
|
},
|
|
359
359
|
];
|
|
360
360
|
|
|
361
|
+
/**
|
|
362
|
+
* UX-10:速查引用解析(单一真源,服务端随 /api/help 下发)。
|
|
363
|
+
*
|
|
364
|
+
* 按「最深前缀」把速查条目的 cmd 解析到模型条目:
|
|
365
|
+
* - 'hub hosts add' → 'hub hosts add'(不是父级 'hub hosts')
|
|
366
|
+
* - 'hub hosts --state orphan' → 'hub hosts'(旗标 / 参数尾段容忍)
|
|
367
|
+
* - '--list' 等伪命令按全名精确匹配
|
|
368
|
+
*/
|
|
369
|
+
function resolveHelpEntry(groups, ref) {
|
|
370
|
+
const raw = String(ref || '').trim();
|
|
371
|
+
if (!raw) return null;
|
|
372
|
+
const entries = [];
|
|
373
|
+
for (const group of groups) {
|
|
374
|
+
for (const cmd of group.commands) {
|
|
375
|
+
entries.push({ path: cmd.name, entry: cmd });
|
|
376
|
+
for (const sub of cmd.subcommands || []) {
|
|
377
|
+
entries.push({ path: cmd.name + ' ' + sub.name, entry: sub });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const item of entries) {
|
|
382
|
+
if (item.path === raw) return item.entry;
|
|
383
|
+
}
|
|
384
|
+
const tokens = [];
|
|
385
|
+
for (const token of raw.split(/\s+/)) {
|
|
386
|
+
if (token.startsWith('-') || token.startsWith('<') || token.startsWith('[')) break;
|
|
387
|
+
tokens.push(token);
|
|
388
|
+
}
|
|
389
|
+
if (!tokens.length) return null;
|
|
390
|
+
let best = null;
|
|
391
|
+
let bestLen = -1;
|
|
392
|
+
for (const item of entries) {
|
|
393
|
+
const pathTokens = item.path.split(' ');
|
|
394
|
+
if (pathTokens.length > tokens.length || pathTokens.length <= bestLen) continue;
|
|
395
|
+
if (pathTokens.every((token, index) => token === tokens[index])) {
|
|
396
|
+
best = item.entry;
|
|
397
|
+
bestLen = pathTokens.length;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return best;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** 速查数据 + 服务端解析结果(面板直接用 meta,不再自行解析)。 */
|
|
404
|
+
function quickWithMeta() {
|
|
405
|
+
return CLI_HELP_QUICK.map((group) => ({
|
|
406
|
+
group: group.group,
|
|
407
|
+
items: group.items.map((item) => Object.assign({}, item, {
|
|
408
|
+
meta: resolveHelpEntry(CLI_HELP_MODEL, item.cmd) || {},
|
|
409
|
+
})),
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
|
|
361
413
|
module.exports = {
|
|
362
414
|
CLI_HELP_MODEL,
|
|
363
415
|
CLI_HELP_QUICK,
|
|
364
416
|
CLI_GLOBAL_OPTIONS,
|
|
417
|
+
resolveHelpEntry,
|
|
418
|
+
quickWithMeta,
|
|
365
419
|
};
|
|
@@ -1092,7 +1092,8 @@ function handleGet(ctx, pathname, url, res) {
|
|
|
1092
1092
|
return json(res, 200, {
|
|
1093
1093
|
version: ctx.version || '',
|
|
1094
1094
|
groups: cliHelpLib.CLI_HELP_MODEL,
|
|
1095
|
-
|
|
1095
|
+
// UX-10:速查解析在服务端完成(单一真源),面板直接用 item.meta。
|
|
1096
|
+
quick: cliHelpLib.quickWithMeta(),
|
|
1096
1097
|
options: cliHelpLib.CLI_GLOBAL_OPTIONS,
|
|
1097
1098
|
});
|
|
1098
1099
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
| `yotta-anti-shallow` | 元谨 | `@yottameta/yotta-anti-shallow` | 1.4.2 | quality | 防 AI 敷衍规则引擎(深入分析/根因追溯时激活) |
|
|
9
9
|
| `yotta-code-quality` | 元质 | `@yottameta/yotta-code-quality` | 0.4.3 | quality | 结对式代码质量评审(十二类腐化风险 + 0-100 健康分) |
|
|
10
10
|
| `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.4.3 | workflow | 跨会话/跨项目通用工作流协议(状态就近存) |
|
|
11
|
-
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.22.
|
|
12
|
-
| `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.
|
|
11
|
+
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.22.9 | memory | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
|
|
12
|
+
| `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.6.0 | memory | 跨智能体知识库(CLI / MCP / 本地面板):可复用经验沉淀为分类条目(草稿→核验→查询);含归位决策树与首次激活引导 |
|
|
13
13
|
| `yotta-security-audit` | 元安 | `@yottameta/yotta-security-audit` | 0.3.1 | security | 安全扫描引擎:13 类检测器 + 系统安全基线 + 学生数据隐私(教育版) |
|
|
14
14
|
| `yotta-vetter` | 元审 | `@yottameta/yotta-vetter` | 0.2.7 | security | 安全审查协议:四阶段 review + SAFE TO INSTALL 判定 |
|
|
15
15
|
| `yotta-recon` | 元析 | `@yottameta/yotta-recon` | 0.1.8 | security | 跨智能体网络侦察:零依赖端口/服务/版本指纹探测 |
|
|
@@ -197,10 +197,10 @@
|
|
|
197
197
|
]
|
|
198
198
|
},
|
|
199
199
|
"yotta-memory": {
|
|
200
|
-
"version": "0.22.
|
|
201
|
-
"treeHash": "sha256:
|
|
200
|
+
"version": "0.22.9",
|
|
201
|
+
"treeHash": "sha256:8cc92c3ede15f72c471959e79c81b975ba12708eaeffd556ce4331207d29074b",
|
|
202
202
|
"files": 18,
|
|
203
|
-
"bytes":
|
|
203
|
+
"bytes": 1352294,
|
|
204
204
|
"mode": "strict",
|
|
205
205
|
"exceptions": [
|
|
206
206
|
{
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"slug": "yotta-memory",
|
|
32
32
|
"name": "元忆",
|
|
33
33
|
"pkg": "@yottameta/yotta-memory",
|
|
34
|
-
"version": "0.22.
|
|
34
|
+
"version": "0.22.9",
|
|
35
35
|
"domain": "memory",
|
|
36
36
|
"runtimePayload": ["bin"],
|
|
37
37
|
"desc": "有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT)"
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"slug": "yotta-learn",
|
|
41
41
|
"name": "元习",
|
|
42
42
|
"pkg": "@yottameta/yotta-learn",
|
|
43
|
-
"version": "0.
|
|
43
|
+
"version": "0.6.0",
|
|
44
44
|
"domain": "memory",
|
|
45
|
-
"desc": "
|
|
45
|
+
"desc": "跨智能体知识库(CLI / MCP / 本地面板):可复用经验沉淀为分类条目(草稿→核验→查询);含归位决策树与首次激活引导"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"slug": "yotta-security-audit",
|