mdk-skills 2.3.0 → 2.3.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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>mdk-skills 管理面板</title>
7
- <script type="module" crossorigin src="/assets/index--qLTFieg.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-DhB4kj3N.css">
7
+ <script type="module" crossorigin src="/assets/index-BnMBIily.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BItq1iGH.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
@@ -1,7 +1,8 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const http = require("http");
4
- const { execSync } = require("child_process");
4
+ const crypto = require("crypto");
5
+ const { execSync, exec } = require("child_process");
5
6
  const os = require("os");
6
7
  const core = require("../core");
7
8
 
@@ -109,6 +110,81 @@ function copyDirSync(src, dest) {
109
110
  }
110
111
  }
111
112
 
113
+ // npx skills 会在系统临时目录留下 skills-* 工作目录,统一清理
114
+ function cleanNpxTemp() {
115
+ const tmp = os.tmpdir();
116
+ for (const name of fs.readdirSync(tmp)) {
117
+ if (/^skills-[A-Za-z0-9]+$/.test(name)) {
118
+ const p = path.join(tmp, name);
119
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch {}
120
+ }
121
+ }
122
+ }
123
+
124
+ // 计算技能目录的文件指纹(基于所有文件内容 md5),用于检测是否有真实变更
125
+ function calcSkillFingerprint(dir) {
126
+ if (!fs.existsSync(dir)) return "";
127
+ const hash = crypto.createHash("md5");
128
+ const walk = (d) => {
129
+ for (const item of fs.readdirSync(d).sort()) {
130
+ const p = path.join(d, item);
131
+ if (item === ".meta.json") continue; // 忽略元信息
132
+ const stat = fs.statSync(p);
133
+ if (stat.isDirectory()) {
134
+ walk(p);
135
+ } else {
136
+ hash.update(item);
137
+ hash.update(fs.readFileSync(p));
138
+ }
139
+ }
140
+ };
141
+ walk(dir);
142
+ return hash.digest("hex");
143
+ }
144
+
145
+ // 统一写入技能元信息。wasUpdated=true 时自动递增 _updateCount(仅对无上游版本号的技能)
146
+ function writeSkillMeta(dest, wasUpdated) {
147
+ const fm = core.parseFrontmatter(dest);
148
+ if (!fm) return;
149
+ const metaPath = path.join(dest, ".meta.json");
150
+ let oldMeta = {};
151
+ if (fs.existsSync(metaPath)) {
152
+ try { oldMeta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); } catch {}
153
+ }
154
+ const hasUpstream = !!fm.version;
155
+ const meta = {
156
+ version: fm.version || "1.0.0",
157
+ description: fm.description || "",
158
+ tags: fm.tags || [],
159
+ };
160
+ if (hasUpstream) {
161
+ meta._updateCount = 0;
162
+ } else if (wasUpdated) {
163
+ meta._updateCount = (oldMeta._updateCount || 0) + 1;
164
+ } else {
165
+ meta._updateCount = oldMeta._updateCount || 0;
166
+ }
167
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
168
+ }
169
+
170
+ // ---------- 异步 npx 管理:支持取消正在执行的进程 ----------
171
+
172
+ let _npxProcess = null;
173
+
174
+ function runNpx(cmd, cwd) {
175
+ return new Promise((resolve, reject) => {
176
+ const proc = exec(cmd, { cwd, timeout: 120000 }, (err) => {
177
+ _npxProcess = null;
178
+ if (err) {
179
+ reject(err.killed ? new Error("已取消") : err);
180
+ } else {
181
+ resolve();
182
+ }
183
+ });
184
+ _npxProcess = proc;
185
+ });
186
+ }
187
+
112
188
  // 读取 profiles.json
113
189
  function loadProfiles() {
114
190
  const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
@@ -334,15 +410,14 @@ async function handleApi(req, res) {
334
410
  const tmpDir = path.join(os.tmpdir(), "mdk-pull-" + Date.now());
335
411
  fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
336
412
  try {
337
- execSync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", {
338
- cwd: tmpDir,
339
- stdio: "pipe",
340
- timeout: 120000,
341
- });
342
- } catch {
413
+ await runNpx("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", tmpDir);
414
+ } catch (e) {
415
+ cleanNpxTemp();
343
416
  fs.rmSync(tmpDir, { recursive: true, force: true });
417
+ if (e.message === "已取消") return sendJSON(res, { cancelled: true }, 499);
344
418
  return sendJSON(res, { error: "拉取失败,请检查地址或网络连接" }, 400);
345
419
  }
420
+ cleanNpxTemp();
346
421
  const imported = [];
347
422
  const skipped = [];
348
423
  const skillsDir = path.join(tmpDir, ".claude", "skills");
@@ -356,14 +431,7 @@ async function handleApi(req, res) {
356
431
  fs.rmSync(dest, { recursive: true, force: true });
357
432
  }
358
433
  copyDirSync(skillPath, dest);
359
- const fm = core.parseFrontmatter(dest);
360
- if (fm) {
361
- fs.writeFileSync(path.join(dest, ".meta.json"), JSON.stringify({
362
- version: fm.version || "1.0.0",
363
- description: fm.description || "",
364
- tags: fm.tags || [],
365
- }, null, 2) + "\n", "utf-8");
366
- }
434
+ writeSkillMeta(dest, false);
367
435
  addPullSource(entry.name, url);
368
436
  imported.push(entry.name);
369
437
  } else {
@@ -404,22 +472,32 @@ async function handleApi(req, res) {
404
472
  const tmpDir = path.join(os.tmpdir(), "mdk-update-" + Date.now());
405
473
  fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
406
474
  try {
407
- execSync("npx --yes skills add \"" + source.url + "\" --copy -y -a claude-code", {
408
- cwd: tmpDir, stdio: "pipe", timeout: 120000,
409
- });
410
- } catch {
475
+ await runNpx("npx --yes skills add \"" + source.url + "\" --copy -y -a claude-code", tmpDir);
476
+ } catch (e) {
477
+ cleanNpxTemp();
411
478
  fs.rmSync(tmpDir, { recursive: true, force: true });
479
+ if (e.message === "已取消") return sendJSON(res, { cancelled: true }, 499);
412
480
  return sendJSON(res, { error: "重新拉取失败,请检查网络连接" }, 400);
413
481
  }
414
482
 
483
+ cleanNpxTemp();
415
484
  const skillPath = path.join(tmpDir, ".claude", "skills", name);
416
485
  if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
417
486
  fs.rmSync(tmpDir, { recursive: true, force: true });
418
487
  return sendJSON(res, { error: "远程仓库中未找到该技能" }, 400);
419
488
  }
420
489
 
421
- // 覆盖源目录
490
+ // 指纹比较:检测是否有真实变更
422
491
  const dest = path.join(pkgSkillsSource, name);
492
+ const oldFingerprint = calcSkillFingerprint(dest);
493
+ const newFingerprint = calcSkillFingerprint(skillPath);
494
+
495
+ if (oldFingerprint === newFingerprint) {
496
+ fs.rmSync(tmpDir, { recursive: true, force: true });
497
+ return sendJSON(res, { ok: true, updated: false });
498
+ }
499
+
500
+ // 有变更:覆盖源目录
423
501
  if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
424
502
  copyDirSync(skillPath, dest);
425
503
 
@@ -431,14 +509,7 @@ async function handleApi(req, res) {
431
509
  }
432
510
 
433
511
  // 更新 meta
434
- const fm = core.parseFrontmatter(dest);
435
- if (fm) {
436
- fs.writeFileSync(path.join(dest, ".meta.json"), JSON.stringify({
437
- version: fm.version || "1.0.0",
438
- description: fm.description || "",
439
- tags: fm.tags || [],
440
- }, null, 2) + "\n", "utf-8");
441
- }
512
+ writeSkillMeta(dest, true);
442
513
 
443
514
  // 更新时间戳
444
515
  source.pulledAt = new Date().toISOString();
@@ -462,15 +533,18 @@ async function handleApi(req, res) {
462
533
  const tmpDir = path.join(os.tmpdir(), "mdk-batch-" + Date.now());
463
534
  fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
464
535
  try {
465
- execSync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", {
466
- cwd: tmpDir, stdio: "pipe", timeout: 120000,
467
- });
468
- } catch {
536
+ await runNpx("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", tmpDir);
537
+ } catch (e) {
538
+ cleanNpxTemp();
469
539
  fs.rmSync(tmpDir, { recursive: true, force: true });
540
+ if (e.message === "已取消") return sendJSON(res, { cancelled: true }, 499);
470
541
  return sendJSON(res, { error: "拉取失败,请检查网络连接" }, 400);
471
542
  }
472
543
 
544
+ cleanNpxTemp();
545
+
473
546
  const updated = [];
547
+ const unchanged = [];
474
548
  const notFound = [];
475
549
  for (const name of names) {
476
550
  const skillPath = path.join(tmpDir, ".claude", "skills", name);
@@ -479,6 +553,12 @@ async function handleApi(req, res) {
479
553
  continue;
480
554
  }
481
555
  const dest = path.join(pkgSkillsSource, name);
556
+ const newFingerprint = calcSkillFingerprint(skillPath);
557
+ const oldFingerprint = calcSkillFingerprint(dest);
558
+ if (oldFingerprint === newFingerprint) {
559
+ unchanged.push(name);
560
+ continue;
561
+ }
482
562
  if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
483
563
  copyDirSync(skillPath, dest);
484
564
 
@@ -488,14 +568,7 @@ async function handleApi(req, res) {
488
568
  copyDirSync(skillPath, projectSkill);
489
569
  }
490
570
 
491
- const fm = core.parseFrontmatter(dest);
492
- if (fm) {
493
- fs.writeFileSync(path.join(dest, ".meta.json"), JSON.stringify({
494
- version: fm.version || "1.0.0",
495
- description: fm.description || "",
496
- tags: fm.tags || [],
497
- }, null, 2) + "\n", "utf-8");
498
- }
571
+ writeSkillMeta(dest, true);
499
572
  updated.push(name);
500
573
  }
501
574
 
@@ -509,7 +582,7 @@ async function handleApi(req, res) {
509
582
  writePullSource(pullSource);
510
583
 
511
584
  fs.rmSync(tmpDir, { recursive: true, force: true });
512
- return sendJSON(res, { ok: true, updated, notFound });
585
+ return sendJSON(res, { ok: true, updated, unchanged, notFound });
513
586
  }
514
587
 
515
588
  // POST /api/skills/:name/open — 在文件管理器中打开技能目录
@@ -947,17 +1020,7 @@ async function handleApi(req, res) {
947
1020
  const skillDir = path.join(skillsDir, name);
948
1021
  const metaPath = path.join(skillDir, ".meta.json");
949
1022
  if (!fs.existsSync(metaPath)) {
950
- const fm = core.parseFrontmatter(skillDir);
951
- const meta = {
952
- version: fm?.version || "1.0.0",
953
- description: fm?.description || name,
954
- tags: fm?.tags || [],
955
- };
956
- fs.writeFileSync(
957
- metaPath,
958
- JSON.stringify(meta, null, 2) + "\n",
959
- "utf-8",
960
- );
1023
+ writeSkillMeta(skillDir, false);
961
1024
  created.metaFiles++;
962
1025
  }
963
1026
  }
@@ -1094,6 +1157,15 @@ ${skillsList}
1094
1157
  return sendJSON(res, results);
1095
1158
  }
1096
1159
 
1160
+ // POST /api/tasks/cancel — 取消正在执行的 npx 操作
1161
+ if (method === "POST" && pathname === "/api/tasks/cancel") {
1162
+ if (_npxProcess) {
1163
+ _npxProcess.kill();
1164
+ _npxProcess = null;
1165
+ }
1166
+ return sendJSON(res, { ok: true });
1167
+ }
1168
+
1097
1169
  // 404
1098
1170
  sendJSON(res, { error: "Not Found: " + pathname }, 404);
1099
1171
  } catch (err) {
@@ -151,3 +151,7 @@ export function createReadme(data) {
151
151
  body: JSON.stringify(data),
152
152
  });
153
153
  }
154
+
155
+ export function cancelTask() {
156
+ return fetch("/api/tasks/cancel", { method: "POST" }).catch(() => {});
157
+ }
@@ -10,7 +10,7 @@
10
10
  </span>
11
11
  </template>
12
12
  <div class="skill-meta">
13
- <span class="skill-version">v{{ skill.version }}</span>
13
+ <span class="skill-version">{{ skill._updateCount > 0 ? 'r' + skill._updateCount : 'v' + skill.version }}</span>
14
14
  <n-tag v-for="tag in skill.tags" :key="tag" size="tiny" :bordered="false">
15
15
  {{ tag }}
16
16
  </n-tag>
@@ -99,6 +99,7 @@
99
99
  style="width: 720px"
100
100
  content-style="padding: 0;"
101
101
  :mask-closable="true"
102
+ @update:show="(v) => { if (!v) { updatingSkill = false; detailLoading = false; cancelTask(); } }"
102
103
  >
103
104
  <div class="detail-body">
104
105
  <!-- 编辑面板 -->
@@ -106,6 +107,7 @@
106
107
  <div class="edit-row">
107
108
  <span class="edit-label">版本</span>
108
109
  <n-input v-model:value="editVersion" size="small" style="width: 120px" />
110
+ <span v-if="detailSkill?._updateCount > 0" class="update-count-hint">r{{ detailSkill._updateCount }}</span>
109
111
  </div>
110
112
  <div class="edit-row">
111
113
  <span class="edit-label">描述</span>
@@ -173,6 +175,7 @@
173
175
  preset="card"
174
176
  style="width: 600px"
175
177
  :mask-closable="true"
178
+ @update:show="(v) => { if (!v) { pulling = false; cancelTask(); } }"
176
179
  >
177
180
  <div class="pull-modal-body">
178
181
  <div class="pull-input-row">
@@ -250,7 +253,7 @@ import { RefreshOutline } from "@vicons/ionicons5";
250
253
  import { marked } from "marked";
251
254
  import hljs from "highlight.js";
252
255
  import SkillCard from "../components/SkillCard.vue";
253
- import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, pullSkills, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir } from "../api/skills";
256
+ import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, pullSkills, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir, cancelTask } from "../api/skills";
254
257
  import { sortSkills, getUsageMap } from "../utils/usage";
255
258
 
256
259
  // marked 配置:代码高亮 + 外链安全
@@ -373,6 +376,7 @@ async function handlePull() {
373
376
  pullError.value = "";
374
377
  try {
375
378
  const res = await pullSkills(pullUrl.value.trim());
379
+ if (!showPullModal.value) return;
376
380
  if (res.ok) {
377
381
  pullResult.value = { imported: res.imported, skipped: res.skipped };
378
382
  // 默认全选
@@ -384,6 +388,7 @@ async function handlePull() {
384
388
  pullError.value = res.error || "拉取失败";
385
389
  }
386
390
  } catch (e) {
391
+ if (!showPullModal.value) return;
387
392
  pullError.value = "拉取失败,请检查地址或网络";
388
393
  } finally {
389
394
  pulling.value = false;
@@ -540,24 +545,32 @@ async function handleUpdate() {
540
545
  updatingSkill.value = true;
541
546
  try {
542
547
  const res = await updateSkill(detailSkill.value.name);
548
+ if (!detailVisible.value) return;
543
549
  if (res.ok) {
544
- message.success("技能已更新到最新版本");
545
- if (res.siblings && res.siblings.length > 0) {
546
- updatedSiblings.value = {
547
- name: detailSkill.value.name,
548
- url: skillSource.value?.url || "",
549
- siblings: res.siblings,
550
- };
551
- const sel = {};
552
- res.siblings.forEach((n) => { sel[n] = true; });
553
- siblingCheck.value = sel;
550
+ if (res.updated === false) {
551
+ message.info("已是最新版本,无需更新");
552
+ } else {
553
+ message.success("技能已更新到最新版本");
554
+ if (res.siblings && res.siblings.length > 0) {
555
+ updatedSiblings.value = {
556
+ name: detailSkill.value.name,
557
+ url: skillSource.value?.url || "",
558
+ siblings: res.siblings,
559
+ };
560
+ const sel = {};
561
+ res.siblings.forEach((n) => { sel[n] = true; });
562
+ siblingCheck.value = sel;
563
+ }
564
+ if (res.updatedAt) {
565
+ skillSource.value = { type: "remote", url: res.url, pulledAt: res.updatedAt };
566
+ }
554
567
  }
555
- skillSource.value = { type: "remote", url: res.url, pulledAt: res.updatedAt };
556
568
  await loadSkills();
557
569
  } else {
558
570
  message.error(res.error || "更新失败");
559
571
  }
560
572
  } catch {
573
+ if (!detailVisible.value) return;
561
574
  message.error("更新失败");
562
575
  } finally {
563
576
  updatingSkill.value = false;
@@ -572,6 +585,9 @@ async function batchUpdateGroup(group) {
572
585
  const res = await batchUpdateSkills(names, group.url);
573
586
  if (res.ok) {
574
587
  message.success(`${res.updated.length} 个技能已更新`);
588
+ if (res.unchanged && res.unchanged.length > 0) {
589
+ message.info(`${res.unchanged.length} 个技能已是最新,跳过`);
590
+ }
575
591
  if (res.notFound && res.notFound.length > 0) {
576
592
  message.warning(`${res.notFound.length} 个技能未找到,已跳过`);
577
593
  }
@@ -710,6 +726,12 @@ onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
710
726
  flex-shrink: 0;
711
727
  }
712
728
 
729
+ .update-count-hint {
730
+ font-size: 12px;
731
+ color: #e68a00;
732
+ font-family: monospace;
733
+ margin-left: 8px;
734
+ }
713
735
 
714
736
  .pull-modal-body {
715
737
  min-height: 100px;
@@ -1 +0,0 @@
1
- .status-bar[data-v-bc9519f9]{background:#fff;border-bottom:1px solid #e5e7eb;flex-wrap:wrap;align-items:center;gap:12px;padding:10px 24px;display:flex}.status-item[data-v-bc9519f9]{color:#555;align-items:center;gap:6px;font-size:13px;display:flex}.status-item.clickable[data-v-bc9519f9]{cursor:pointer}.status-item.clickable[data-v-bc9519f9]:hover{color:#2080f0}.status-divider[data-v-bc9519f9]{background:#e0e0e0;width:1px;height:20px}[data-theme=dark] .status-bar{background:#1a1a2e;border-bottom-color:#333}[data-theme=dark] .status-item{color:#aaa}[data-theme=dark] .status-divider{background:#333}*{box-sizing:border-box;margin:0;padding:0}html,body,#app{background:#f5f7fa;height:100%}body{overflow-y:auto}*{scrollbar-width:none;-ms-overflow-style:none}::-webkit-scrollbar{display:none}.app-layout{flex-direction:column;min-height:100vh;padding-top:56px;display:flex}.app-header{z-index:100;background:#fff;border-bottom:1px solid #e5e7eb;align-items:center;height:56px;padding:0 24px;display:flex;position:fixed;top:0;left:0;right:0}.header-inner{justify-content:space-between;align-items:center;width:100%;display:flex}.header-left{align-items:center;gap:32px;display:flex}.logo{color:#1a1a1a;white-space:nowrap;font-size:18px;font-weight:700}.app-content{flex:1;width:100%;max-width:1000px;margin:0 auto;padding:24px}.markdown-content{color:#333;word-wrap:break-word;font-size:14px;line-height:1.7}.markdown-content h1{border-bottom:1px solid #eee;margin:20px 0 12px;padding-bottom:8px;font-size:22px;font-weight:700}.markdown-content h2{border-bottom:1px solid #eee;margin:18px 0 10px;padding-bottom:6px;font-size:18px;font-weight:700}.markdown-content h3{margin:16px 0 8px;font-size:16px;font-weight:600}.markdown-content h4{margin:14px 0 6px;font-size:14px;font-weight:600}.markdown-content h5,.markdown-content h6{margin:12px 0 6px;font-size:13px;font-weight:600}.markdown-content p{margin:8px 0}.markdown-content ul,.markdown-content ol{margin:8px 0;padding-left:24px}.markdown-content li{margin:4px 0}.markdown-content blockquote{color:#555;background:#f0f7ff;border-left:4px solid #2080f0;margin:10px 0;padding:8px 16px}.markdown-content blockquote p{margin:4px 0}.markdown-content a{color:#2080f0;text-decoration:none}.markdown-content a:hover{text-decoration:underline}.markdown-content hr{border:none;border-top:1px solid #e0e0e0;margin:16px 0}.markdown-content table{border-collapse:collapse;width:100%;margin:12px 0;font-size:13px}.markdown-content th,.markdown-content td{text-align:left;border:1px solid #e0e0e0;padding:8px 12px}.markdown-content th{background:#f5f7fa;font-weight:600}.markdown-content tr:nth-child(2n) td{background:#fafafa}.markdown-content img{border-radius:4px;max-width:100%;margin:8px 0}.markdown-content code{color:#d63384;background:#f0f0f0;border-radius:3px;padding:2px 6px;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px}.markdown-content pre{border:1px solid #e8e8e8;border-radius:6px;margin:12px 0;padding:0;position:relative;overflow:hidden}.markdown-content pre code{color:#333;tab-size:2;background:#f8f9fa;padding:14px 16px;font-size:13px;line-height:1.5;display:block;overflow-x:auto}.markdown-content pre .copy-btn{color:#999;cursor:pointer;opacity:0;background:#fff;border:1px solid #e0e0e0;border-radius:4px;padding:3px 8px;font-size:11px;transition:opacity .2s;position:absolute;top:6px;right:6px}.readme-fold{border:1px solid #e5e7eb;border-radius:6px;margin-bottom:20px;overflow:hidden}.fold-header{cursor:pointer;-webkit-user-select:none;user-select:none;background:#fafafa;align-items:center;gap:6px;padding:10px 16px;font-size:14px;font-weight:600;display:flex}.fold-header:hover{background:#f0f0f0}.fold-arrow{color:#999;font-size:11px;transition:transform .2s}.fold-arrow.open{transform:rotate(90deg)}.fold-body{padding:0 16px 16px}.markdown-content pre:hover .copy-btn{opacity:1}.markdown-content pre .copy-btn:hover{color:#2080f0;border-color:#2080f0}.header-right{align-items:center;gap:8px;display:flex}[data-theme=dark] html,[data-theme=dark] body,[data-theme=dark] #app{background:#1a1a2e}[data-theme=dark] .app-header{background:#1a1a2e;border-bottom-color:#333}[data-theme=dark] .app-content{background:0 0}[data-theme=dark] .readme-fold{border-color:#333}[data-theme=dark] .fold-header{color:#e0e0e0;background:#252538}[data-theme=dark] .fold-header:hover{background:#2a2a40}[data-theme=dark] .fold-body{background:#1e1e32}[data-theme=dark] .fold-arrow{color:#888}[data-theme=dark] .markdown-content{color:#ccc}[data-theme=dark] .markdown-content h1,[data-theme=dark] .markdown-content h2{color:#eee;border-bottom-color:#333}[data-theme=dark] .markdown-content h3,[data-theme=dark] .markdown-content h4{color:#eee}[data-theme=dark] .markdown-content code{color:#e06c9f;background:#2a2a40}[data-theme=dark] .markdown-content pre{border-color:#333}[data-theme=dark] .markdown-content pre code{color:#d4d4d4;background:#1e1e2e}[data-theme=dark] .markdown-content blockquote{color:#bbb;background:#252538;border-left-color:#4a6cf7}[data-theme=dark] .markdown-content a{color:#6a8cff}[data-theme=dark] .markdown-content th{background:#252538}[data-theme=dark] .markdown-content td{border-color:#333}[data-theme=dark] .markdown-content tr:nth-child(2n) td{background:#222238}[data-theme=dark] .markdown-content hr{border-top-color:#333}[data-theme=dark] .markdown-content img{filter:brightness(.85)}[data-theme=dark] .page-header h2{color:#e0e0e0}[data-theme=dark] .scene-desc{color:#aaa}.fade-enter-active,.fade-leave-active{transition:opacity .9s,transform .9s}.fade-enter-from{opacity:0;transform:translateY(6px)}.fade-leave-to{opacity:0}.skill-card[data-v-a11339d4]{cursor:pointer;margin-bottom:12px;transition:box-shadow .2s}.skill-card[data-v-a11339d4]:hover{box-shadow:0 2px 8px #00000014}.skill-meta[data-v-a11339d4]{align-items:center;gap:8px;margin-bottom:8px;display:flex}.skill-version[data-v-a11339d4]{color:#888;font-family:monospace;font-size:12px}.skill-desc[data-v-a11339d4]{color:#666;text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:13px;overflow:hidden}.page-header[data-v-2d1c5779]{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-bottom:4px;display:flex}.page-header h2[data-v-2d1c5779]{font-size:20px;font-weight:600}.header-actions[data-v-2d1c5779]{align-items:center;gap:8px;display:flex}.tag-filter[data-v-2d1c5779]{flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:16px;display:flex}.detail-status[data-v-2d1c5779]{justify-content:center;padding:40px 0;display:flex}.detail-body[data-v-2d1c5779]{max-height:65vh;padding:16px 24px;overflow-y:auto}.edit-panel[data-v-2d1c5779]{margin-bottom:4px}.edit-row[data-v-2d1c5779]{align-items:flex-start;gap:10px;margin-bottom:10px;display:flex}.edit-label[data-v-2d1c5779]{color:#888;flex-shrink:0;width:50px;font-size:13px;line-height:30px}.pull-modal-body[data-v-2d1c5779]{min-height:100px}.pull-section[data-v-2d1c5779]{margin-top:12px}.pull-input-row[data-v-2d1c5779]{gap:8px;margin-bottom:8px;display:flex}.pull-input-row .n-input[data-v-2d1c5779]{flex:1}.pull-error[data-v-2d1c5779]{margin-bottom:8px}.pull-result[data-v-2d1c5779]{margin-top:8px}.pull-result-line[data-v-2d1c5779]{flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:8px;font-size:13px;display:flex}.pull-result-label[data-v-2d1c5779]{color:#888;flex-shrink:0}.pull-result-hint[data-v-2d1c5779]{color:#999;font-size:12px}.install-panel[data-v-2d1c5779]{background:#00000005;border-radius:6px;margin-top:12px;padding:12px}.install-panel-label[data-v-2d1c5779]{color:#666;margin-bottom:8px;font-size:13px}.install-check-list[data-v-2d1c5779]{margin-bottom:10px}.install-btn[data-v-2d1c5779]{float:right}.edit-actions[data-v-2d1c5779]{justify-content:flex-end;gap:8px;display:flex}.skill-group[data-v-2d1c5779]{margin-bottom:4px}.group-header[data-v-2d1c5779]{cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:4px;align-items:center;gap:8px;padding:8px 4px;display:flex}.group-header[data-v-2d1c5779]:hover{background:#00000008}.fold-arrow[data-v-2d1c5779]{color:#999;flex-shrink:0;font-size:10px;transition:transform .2s}.fold-arrow.open[data-v-2d1c5779]{transform:rotate(90deg)}.group-label[data-v-2d1c5779]{color:inherit;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:13px;font-weight:600;overflow:hidden}.group-count[data-v-2d1c5779]{color:#999;flex-shrink:0;font-size:12px}.group-update-btn[data-v-2d1c5779]{flex-shrink:0}.group-body[data-v-2d1c5779]{padding-left:4px}.source-info[data-v-2d1c5779]{margin-bottom:4px}.source-row[data-v-2d1c5779]{align-items:center;gap:10px;margin-bottom:6px;font-size:13px;display:flex}.source-label[data-v-2d1c5779]{color:#888;flex-shrink:0;width:70px}.source-value[data-v-2d1c5779]{word-break:break-all}.local-tag[data-v-2d1c5779]{color:#2e7d32;background:#e8f5e9;border-radius:3px;padding:1px 8px;font-size:12px;display:inline-block}.path-text[data-v-2d1c5779]{color:#666;font-family:monospace;font-size:12px}.source-actions[data-v-2d1c5779]{gap:8px;margin-top:8px;display:flex}.local-hint[data-v-2d1c5779]{color:#999;background:#00000005;border-radius:4px;margin-top:8px;padding:8px;font-size:12px;line-height:1.5}.siblings-body[data-v-2d1c5779]{min-height:60px}.siblings-desc[data-v-2d1c5779]{margin-bottom:12px;font-size:13px;line-height:1.6}.siblings-list[data-v-2d1c5779]{flex-direction:column;gap:8px;margin-bottom:16px;display:flex}.siblings-actions[data-v-2d1c5779]{justify-content:flex-end;gap:8px;display:flex}.page-header[data-v-1736a530]{justify-content:space-between;align-items:center;margin-bottom:20px;display:flex}.page-header h2[data-v-1736a530]{font-size:20px;font-weight:600}.scene-grid[data-v-1736a530]{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin:2px;display:grid}.scene-card[data-v-1736a530]{transition:box-shadow .2s}.scene-card.active[data-v-1736a530]{box-shadow:0 0 0 2px #2080f0}.scene-desc[data-v-1736a530]{color:#666;margin:0;font-size:13px}.scene-footer[data-v-1736a530]{justify-content:space-between;align-items:center;display:flex}.hint-text[data-v-1736a530]{color:#999;font-size:12px}.page-header[data-v-5f659620]{margin-bottom:20px}.page-header h2[data-v-5f659620]{font-size:20px;font-weight:600}.section[data-v-5f659620]{margin-bottom:20px}.init-alert[data-v-5f659620]{margin-bottom:12px}.missing-list[data-v-5f659620]{flex-direction:column;gap:4px;display:flex}.missing-item[data-v-5f659620]{font-size:13px;line-height:1.6}.missing-item code[data-v-5f659620]{background:#0000000f;border-radius:3px;padding:1px 6px;font-size:12px}.skill-tag[data-v-5f659620]{margin:1px 2px;display:inline-block}.missing-hint[data-v-5f659620]{opacity:.7;margin-top:4px;font-size:12px}.source-status[data-v-5f659620]{margin-bottom:16px}.source-actions[data-v-5f659620]{flex-direction:column;gap:12px;display:flex}.action-buttons[data-v-5f659620]{gap:8px;display:flex}.healthy-text[data-v-5f659620]{color:#18a058;font-size:13px}.issue-text[data-v-5f659620]{color:#d03050;font-size:13px}.readme-dialog-desc[data-v-5f659620]{color:#555;margin-bottom:16px;font-size:14px}.readme-checkbox[data-v-5f659620]{align-items:flex-start;margin-bottom:12px;display:flex}.readme-checkbox-content[data-v-5f659620]{flex-direction:column;gap:2px;display:flex}.readme-checkbox-title[data-v-5f659620]{font-size:14px;font-weight:500}.readme-checkbox-desc[data-v-5f659620]{color:#999;font-size:12px}.page-header[data-v-97d38fe1]{justify-content:space-between;align-items:center;margin-bottom:20px;display:flex}.page-header h2[data-v-97d38fe1]{font-size:20px;font-weight:600}.fade-enter-active[data-v-97d38fe1],.fade-leave-active[data-v-97d38fe1]{transition:opacity .2s}.fade-enter-from[data-v-97d38fe1],.fade-leave-to[data-v-97d38fe1]{opacity:0}pre code.hljs{padding:1em;display:block;overflow-x:auto}code.hljs{padding:3px 5px}.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#005cc5}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-comment,.hljs-code,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}