mdk-skills 2.3.11 → 2.3.13

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-DwjE_sXd.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BGLelyx5.css">
7
+ <script type="module" crossorigin src="/assets/index-pHDOtonU.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DdbAYqm7.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
@@ -121,6 +121,19 @@ function cleanNpxTemp() {
121
121
  }
122
122
  }
123
123
 
124
+ // pull 缓存:预览克隆到临时目录后暂存,拉取时直接复用
125
+ const pullCache = new Map(); // url → { tmpDir, createdAt }
126
+
127
+ function cleanPullCache() {
128
+ const now = Date.now();
129
+ for (const [url, entry] of pullCache) {
130
+ if (now - entry.createdAt > 10 * 60 * 1000) {
131
+ try { fs.rmSync(entry.tmpDir, { recursive: true, force: true }); } catch {}
132
+ pullCache.delete(url);
133
+ }
134
+ }
135
+ }
136
+
124
137
  // 计算技能目录的文件指纹(基于所有文件内容 md5),用于检测是否有真实变更
125
138
  function calcSkillFingerprint(dir) {
126
139
  if (!fs.existsSync(dir)) return "";
@@ -438,45 +451,76 @@ async function handleApi(req, res) {
438
451
  const result = installSelectedSkills(selected);
439
452
  return sendJSON(res, { ok: true, ...result });
440
453
  }
441
- // POST /api/skills/pull — 从远程仓库拉取技能(使用 npx skills 自动发现)
454
+ // POST /api/skills/pull — 智能路由:预览或拉取远程技能
455
+ // - names 为空 → 预览模式:克隆到临时目录并缓存,返回技能列表
456
+ // - names 非空 → 拉取模式:从缓存读取 tmpDir,复制选中技能
442
457
  if (method === "POST" && pathname === "/api/skills/pull") {
443
458
  const body = await parseBody(req);
444
459
  const url = (body.url || "").trim();
445
460
  if (!url) return sendJSON(res, { error: "请输入仓库地址" }, 400);
446
- const tmpDir = path.join(os.tmpdir(), "mdk-pull-" + Date.now());
447
- fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
448
- try {
449
- await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-pull" });
450
- } catch (e) {
461
+ const names = body.names;
462
+
463
+ // ---------- 预览模式 ----------
464
+ if (!Array.isArray(names) || names.length === 0) {
465
+ cleanPullCache();
466
+ // 已有缓存则直接返回
467
+ const cached = pullCache.get(url);
468
+ if (cached) {
469
+ const skillsDir = path.join(cached.tmpDir, ".claude", "skills");
470
+ const skills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
471
+ return sendJSON(res, { ok: true, skills, total: skills.length });
472
+ }
473
+ const tmpDir = path.join(os.tmpdir(), "mdk-preview-" + Date.now());
474
+ fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
475
+ try {
476
+ await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-pull-preview" });
477
+ } catch (e) {
478
+ cleanNpxTemp();
479
+ fs.rmSync(tmpDir, { recursive: true, force: true });
480
+ if (e.killed) return sendJSON(res, { cancelled: true }, 499);
481
+ return sendJSON(res, { error: "预览失败:" + (e.stderr || e.message) }, 400);
482
+ }
451
483
  cleanNpxTemp();
452
- fs.rmSync(tmpDir, { recursive: true, force: true });
453
- if (e.killed) return sendJSON(res, { cancelled: true }, 499);
454
- return sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
484
+ const skillsDir = path.join(tmpDir, ".claude", "skills");
485
+ const skills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
486
+ if (skills.length === 0) {
487
+ fs.rmSync(tmpDir, { recursive: true, force: true });
488
+ return sendJSON(res, { error: "未找到有效技能" }, 400);
489
+ }
490
+ // 缓存,不删 tmpDir
491
+ pullCache.set(url, { tmpDir, createdAt: Date.now() });
492
+ return sendJSON(res, { ok: true, skills, total: skills.length });
455
493
  }
456
- cleanNpxTemp();
494
+
495
+ // ---------- 拉取模式 ----------
496
+ const entry = pullCache.get(url);
497
+ if (!entry) {
498
+ return sendJSON(res, { error: "请先预览该仓库" }, 400);
499
+ }
500
+ const tmpDir = entry.tmpDir;
457
501
  const imported = [];
458
502
  const skipped = [];
459
503
  const skillsDir = path.join(tmpDir, ".claude", "skills");
460
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
461
- for (const entry of entries) {
462
- if (!entry.isDirectory()) continue;
463
- const skillPath = path.join(skillsDir, entry.name);
464
- if (fs.existsSync(path.join(skillPath, "SKILL.md"))) {
465
- const dest = path.join(pkgSkillsSource, entry.name);
466
- if (fs.existsSync(dest)) {
467
- fs.rmSync(dest, { recursive: true, force: true });
468
- }
469
- copyDirSync(skillPath, dest);
470
- writeSkillMeta(dest, false);
471
- addPullSource(entry.name, url);
472
- imported.push(entry.name);
473
- } else {
474
- skipped.push(entry.name);
504
+ for (const name of names) {
505
+ const skillPath = path.join(skillsDir, name);
506
+ if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
507
+ skipped.push(name);
508
+ continue;
475
509
  }
510
+ const dest = path.join(pkgSkillsSource, name);
511
+ if (fs.existsSync(dest)) {
512
+ fs.rmSync(dest, { recursive: true, force: true });
513
+ }
514
+ copyDirSync(skillPath, dest);
515
+ writeSkillMeta(dest, false);
516
+ addPullSource(name, url);
517
+ imported.push(name);
476
518
  }
477
- fs.rmSync(tmpDir, { recursive: true, force: true });
519
+ // 清理缓存 + 临时目录
520
+ pullCache.delete(url);
521
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
478
522
  if (imported.length === 0) {
479
- return sendJSON(res, { error: "未找到有效的技能(目录需包含 SKILL.md)", skipped }, 400);
523
+ return sendJSON(res, { error: "所选技能均无效(需包含 SKILL.md)", skipped }, 400);
480
524
  }
481
525
  return sendJSON(res, { ok: true, imported, skipped });
482
526
  }
@@ -109,10 +109,10 @@ export function updateSkillMeta(name, data) {
109
109
  });
110
110
  }
111
111
 
112
- export function pullSkills(url) {
112
+ export function pullSkills(url, names = []) {
113
113
  return request("/skills/pull", {
114
114
  method: "POST",
115
- body: JSON.stringify({ url }),
115
+ body: JSON.stringify({ url, names }),
116
116
  });
117
117
  }
118
118
 
@@ -166,45 +166,73 @@
166
166
  </div>
167
167
  </ModalComp>
168
168
 
169
- <!-- 拉取弹窗 -->
169
+ <!-- 拉取弹窗(两阶段:预览 → 选择 → 拉取) -->
170
170
  <ModalComp
171
171
  :show="showPullModal"
172
172
  title="从远程仓库拉取"
173
173
  width="600px"
174
174
  :mask-closable="true"
175
- @update:show="(v) => { if (!v) { showPullModal = false; pulling = false; cancelTask(); } }"
175
+ @update:show="(v) => { if (!v) { showPullModal = false; pulling = false; previewing = false; cancelTask(); } }"
176
176
  >
177
177
  <div class="pull-modal-body">
178
+ <!-- URL 输入 -->
178
179
  <div class="pull-input-row">
179
- <n-input v-model:value="pullUrl" placeholder="输入 GitHub 仓库地址..." size="small" @keyup.enter="handlePull" />
180
- <n-button size="small" type="primary" @click="handlePull" :loading="pulling">拉取</n-button>
180
+ <n-input v-model:value="pullUrl" placeholder="输入 GitHub 仓库地址..." size="small" @keyup.enter="handlePreview" />
181
+ <n-button size="small" type="primary" @click="handlePreview" :loading="previewing">预览</n-button>
181
182
  </div>
182
183
 
184
+ <!-- 错误提示 -->
183
185
  <div v-if="pullError" class="pull-error">
184
186
  <n-alert type="error" :bordered="false" closable @close="pullError = ''">{{ pullError }}</n-alert>
185
187
  </div>
186
188
 
189
+ <!-- 预览中 -->
190
+ <div v-if="previewing" class="pull-status">
191
+ <n-spin size="small" />
192
+ <span>正在获取远程技能列表...</span>
193
+ </div>
194
+
195
+ <!-- 预览结果:技能列表 + 勾选 -->
196
+ <div v-if="previewDone && !pulling && !pullResult" class="preview-result">
197
+ <div class="preview-info">
198
+ 发现 <strong>{{ previewSkillList.length }}</strong> 个技能:
199
+ <n-button size="tiny" text @click="togglePullAll">全选/取消</n-button>
200
+ </div>
201
+ <div class="preview-list">
202
+ <n-checkbox
203
+ v-for="name in previewSkillList"
204
+ :key="name"
205
+ v-model:checked="pullCheck[name]"
206
+ :label="name"
207
+ />
208
+ </div>
209
+ <div class="preview-actions">
210
+ <n-button size="small" @click="showPullModal = false">取消</n-button>
211
+ <n-button size="small" type="primary" @click="handlePull" :loading="pulling" :disabled="selectedPullCount === 0">
212
+ 拉取选中 ({{ selectedPullCount }})
213
+ </n-button>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- 拉取中 -->
218
+ <div v-if="pulling" class="pull-status">
219
+ <n-spin size="small" />
220
+ <span>正在拉取选中技能...</span>
221
+ </div>
222
+
223
+ <!-- 拉取结果 -->
187
224
  <div v-if="pullResult" class="pull-result">
188
- <div v-if="pullResult.imported && pullResult.imported.length > 0" class="pull-result-line">
225
+ <div class="pull-result-line">
189
226
  <span class="pull-result-label">已导入:</span>
190
227
  <n-tag v-for="name in pullResult.imported" :key="name" size="small" type="success" style="margin: 2px">{{ name }}</n-tag>
191
228
  </div>
192
- <div v-if="pullResult.skipped && pullResult.skipped.length > 0" class="pull-result-line">
229
+ <div v-if="pullResult.skipped?.length" class="pull-result-line">
193
230
  <span class="pull-result-label">跳过:</span>
194
231
  <n-tag v-for="name in pullResult.skipped" :key="name" size="small" style="margin: 2px">{{ name }}</n-tag>
195
232
  <span class="pull-result-hint">(无 SKILL.md)</span>
196
233
  </div>
197
-
198
- <div v-if="pullResult.imported && pullResult.imported.length > 0" class="install-panel">
199
- <div class="install-panel-label">选择要安装到项目目录的技能:</div>
200
- <div class="install-check-list">
201
- <n-checkbox v-for="name in pullResult.imported" :key="name" v-model:checked="installSelected[name]" style="margin-right: 16px">
202
- {{ name }}
203
- </n-checkbox>
204
- </div>
205
- <n-button size="small" type="primary" @click="handleInstallPull" :loading="installing" class="install-btn">
206
- 安装选中
207
- </n-button>
234
+ <div class="preview-actions" style="margin-top: 12px;">
235
+ <n-button size="small" type="primary" @click="showPullModal = false">完成</n-button>
208
236
  </div>
209
237
  </div>
210
238
  </div>
@@ -301,7 +329,7 @@ import hljs from "highlight.js";
301
329
  import SkillCard from "../components/SkillCard.vue";
302
330
  import ModalComp from "../components/ModalComp.vue";
303
331
  import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, pullSkills, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir, cancelTask } from "../api/skills";
304
- import { sortSkills, getUsageMap } from "../utils/usage";
332
+ import { sortSkills, getUsageMap, recordUsage } from "../utils/usage";
305
333
 
306
334
  // marked 配置:代码高亮 + 外链安全
307
335
  marked.use({
@@ -404,73 +432,97 @@ const sortOptions = [
404
432
  ];
405
433
 
406
434
 
407
- // 拉取 & 安装
435
+ // 拉取(两阶段:预览 选择 → 拉取)
408
436
  const pullUrl = ref("");
437
+ const previewing = ref(false);
438
+ const previewDone = ref(false);
439
+ const previewSkillList = ref([]);
440
+ const pullCheck = ref({});
409
441
  const pulling = ref(false);
410
442
  const pullResult = ref(null);
411
443
  const pullError = ref("");
412
444
  const showPullModal = ref(false);
413
- const installSelected = ref({});
414
- const installing = ref(false);
445
+
446
+ const selectedPullCount = computed(() => {
447
+ return Object.keys(pullCheck.value).filter(k => pullCheck.value[k]).length;
448
+ });
415
449
 
416
450
  function openPullModal() {
417
451
  const savedY = window.scrollY;
418
452
  showPullModal.value = true;
419
453
  pullUrl.value = "";
454
+ previewDone.value = false;
455
+ previewing.value = false;
456
+ previewSkillList.value = [];
457
+ pullCheck.value = {};
420
458
  pullResult.value = null;
421
459
  pullError.value = "";
422
460
  nextTick(() => window.scrollTo(0, savedY));
423
461
  }
424
462
 
425
- async function handlePull() {
463
+ function togglePullAll() {
464
+ const checked = Object.values(pullCheck.value).some(v => !v);
465
+ for (const key of Object.keys(pullCheck.value)) {
466
+ pullCheck.value[key] = checked;
467
+ }
468
+ }
469
+
470
+ async function handlePreview() {
426
471
  if (!pullUrl.value.trim()) {
427
472
  pullError.value = "请输入仓库地址";
428
473
  return;
429
474
  }
430
- pulling.value = true;
475
+ previewing.value = true;
476
+ previewDone.value = false;
431
477
  pullResult.value = null;
432
478
  pullError.value = "";
433
479
  try {
434
480
  const res = await pullSkills(pullUrl.value.trim());
435
481
  if (!showPullModal.value) return;
436
482
  if (res.ok) {
437
- pullResult.value = { imported: res.imported, skipped: res.skipped };
438
- // 默认全选
439
- const sel = {};
440
- (res.imported || []).forEach((n) => { sel[n] = true; });
441
- installSelected.value = sel;
442
- await loadSkills();
483
+ previewSkillList.value = res.skills || [];
484
+ const check = {};
485
+ (res.skills || []).forEach(n => { check[n] = true; });
486
+ pullCheck.value = check;
487
+ previewDone.value = true;
443
488
  } else {
444
- pullError.value = res.error || "拉取失败";
489
+ pullError.value = res.error || "预览失败";
445
490
  }
446
491
  } catch (e) {
447
492
  if (!showPullModal.value) return;
448
- pullError.value = "拉取失败,请检查地址或网络";
493
+ pullError.value = "预览失败,请检查地址或网络";
449
494
  } finally {
450
- pulling.value = false;
495
+ previewing.value = false;
451
496
  }
452
497
  }
453
498
 
454
- async function handleInstallPull() {
455
- const selected = Object.keys(installSelected.value).filter((k) => installSelected.value[k]);
499
+ async function handlePull() {
500
+ const selected = Object.keys(pullCheck.value).filter(k => pullCheck.value[k]);
456
501
  if (selected.length === 0) {
457
502
  pullError.value = "请至少选择一个技能";
458
503
  return;
459
504
  }
460
- installing.value = true;
505
+ pulling.value = true;
506
+ pullError.value = "";
461
507
  try {
462
- const res = await installSkills(selected);
508
+ const res = await pullSkills(pullUrl.value.trim(), selected);
509
+ if (!showPullModal.value) return;
463
510
  if (res.ok) {
464
- message.success("安装完成");
511
+ pullResult.value = { imported: res.imported, skipped: res.skipped || [] };
512
+ // 自动安装到项目目录
513
+ await installSkills(selected);
514
+ message.success(`已拉取并安装 ${selected.length} 个技能`);
515
+ // 记录使用
516
+ selected.forEach(recordUsage);
465
517
  await loadSkills();
466
- pullResult.value = null;
467
518
  } else {
468
- pullError.value = res.error || "安装失败";
519
+ pullError.value = res.error || "拉取失败";
469
520
  }
470
- } catch {
471
- pullError.value = "安装失败";
521
+ } catch (e) {
522
+ if (!showPullModal.value) return;
523
+ pullError.value = "拉取失败,请检查地址或网络";
472
524
  } finally {
473
- installing.value = false;
525
+ pulling.value = false;
474
526
  }
475
527
  }
476
528
  const allTags = computed(() => {
@@ -804,6 +856,47 @@ onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
804
856
  min-height: 100px;
805
857
  }
806
858
 
859
+ .pull-status {
860
+ display: flex;
861
+ align-items: center;
862
+ gap: 10px;
863
+ padding: 24px 0;
864
+ justify-content: center;
865
+ color: #666;
866
+ font-size: 13px;
867
+ }
868
+
869
+ .preview-result {
870
+ margin-top: 8px;
871
+ }
872
+
873
+ .preview-info {
874
+ font-size: 13px;
875
+ margin-bottom: 10px;
876
+ color: #555;
877
+ display: flex;
878
+ align-items: center;
879
+ gap: 8px;
880
+ }
881
+
882
+ .preview-list {
883
+ display: grid;
884
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
885
+ gap: 6px;
886
+ margin-bottom: 14px;
887
+ padding: 10px;
888
+ background: rgba(0, 0, 0, 0.02);
889
+ border-radius: 6px;
890
+ max-height: 300px;
891
+ overflow-y: auto;
892
+ }
893
+
894
+ .preview-actions {
895
+ display: flex;
896
+ justify-content: flex-end;
897
+ gap: 8px;
898
+ }
899
+
807
900
  .pull-section {
808
901
  margin-top: 12px;
809
902
  }
@@ -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-f2c0de50]{cursor:pointer;margin-bottom:12px;transition:box-shadow .2s}.skill-card[data-v-f2c0de50]:hover{box-shadow:0 2px 8px #00000014}.skill-meta[data-v-f2c0de50]{align-items:center;gap:8px;margin-bottom:8px;display:flex}.skill-version[data-v-f2c0de50]{color:#888;font-family:monospace;font-size:12px}.skill-desc[data-v-f2c0de50]{color:#666;text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:13px;overflow:hidden}.modal-overlay[data-v-aa861984]{z-index:2000;background:#00000073;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal-card[data-v-aa861984]{background:#fff;border-radius:8px;flex-direction:column;width:90%;max-height:85vh;display:flex;box-shadow:0 8px 32px #0000001f}.modal-header[data-v-aa861984]{justify-content:space-between;align-items:center;padding:14px 20px 0;display:flex}.modal-title[data-v-aa861984]{color:#333;font-size:16px;font-weight:600}.modal-close[data-v-aa861984]{color:#999;cursor:pointer;background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;width:28px;height:28px;font-size:20px;line-height:1;transition:background .15s,color .15s;display:flex}.modal-close[data-v-aa861984]:hover{color:#333;background:#f0f0f0}.modal-body[data-v-aa861984]{padding:16px 20px;overflow-y:auto}.modal-footer[data-v-aa861984]{border-top:1px solid #f0f0f0;justify-content:flex-end;gap:8px;padding:12px 20px 16px;display:flex}.modal-fade-enter-active[data-v-aa861984]{transition:opacity .2s}.modal-fade-leave-active[data-v-aa861984]{transition:opacity .15s}.modal-fade-enter-from[data-v-aa861984],.modal-fade-leave-to[data-v-aa861984]{opacity:0}.dashboard[data-v-0ff2d0b3]{overflow-anchor:auto}.page-header[data-v-0ff2d0b3]{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-bottom:4px;display:flex}.page-header h2[data-v-0ff2d0b3]{font-size:20px;font-weight:600}.header-actions[data-v-0ff2d0b3]{align-items:center;gap:8px;display:flex}.tag-filter[data-v-0ff2d0b3]{flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:16px;display:flex}.detail-status[data-v-0ff2d0b3]{justify-content:center;padding:40px 0;display:flex}.detail-body[data-v-0ff2d0b3]{max-height:65vh;overflow-y:auto}.edit-panel[data-v-0ff2d0b3]{margin-bottom:4px}.edit-row[data-v-0ff2d0b3]{align-items:flex-start;gap:10px;margin-bottom:10px;display:flex}.edit-label[data-v-0ff2d0b3]{color:#888;flex-shrink:0;width:50px;font-size:13px;line-height:30px}.update-count-hint[data-v-0ff2d0b3]{color:#e68a00;margin-left:8px;font-family:monospace;font-size:12px}.pull-modal-body[data-v-0ff2d0b3]{min-height:100px}.pull-section[data-v-0ff2d0b3]{margin-top:12px}.pull-input-row[data-v-0ff2d0b3]{gap:8px;margin-bottom:8px;display:flex}.pull-input-row .n-input[data-v-0ff2d0b3]{flex:1}.pull-error[data-v-0ff2d0b3]{margin-bottom:8px}.pull-result[data-v-0ff2d0b3]{margin-top:8px}.pull-result-line[data-v-0ff2d0b3]{flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:8px;font-size:13px;display:flex}.pull-result-label[data-v-0ff2d0b3]{color:#888;flex-shrink:0}.pull-result-hint[data-v-0ff2d0b3]{color:#999;font-size:12px}.install-panel[data-v-0ff2d0b3]{background:#00000005;border-radius:6px;margin-top:12px;padding:12px}.install-panel-label[data-v-0ff2d0b3]{color:#666;margin-bottom:8px;font-size:13px}.install-check-list[data-v-0ff2d0b3]{margin-bottom:10px}.install-btn[data-v-0ff2d0b3]{float:right}.edit-actions[data-v-0ff2d0b3]{justify-content:flex-end;gap:8px;display:flex}.skill-group[data-v-0ff2d0b3]{margin-bottom:4px}.group-header[data-v-0ff2d0b3]{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-0ff2d0b3]:hover{background:#00000008}.fold-arrow[data-v-0ff2d0b3]{color:#999;flex-shrink:0;font-size:10px;transition:transform .2s}.fold-arrow.open[data-v-0ff2d0b3]{transform:rotate(90deg)}.group-label[data-v-0ff2d0b3]{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-0ff2d0b3]{color:#999;flex-shrink:0;font-size:12px}.group-update-btn[data-v-0ff2d0b3]{flex-shrink:0}.group-body[data-v-0ff2d0b3]{padding-left:4px}.source-info[data-v-0ff2d0b3]{margin-bottom:4px}.source-row[data-v-0ff2d0b3]{align-items:center;gap:10px;margin-bottom:6px;font-size:13px;display:flex}.source-label[data-v-0ff2d0b3]{color:#888;flex-shrink:0;width:70px}.source-value[data-v-0ff2d0b3]{word-break:break-all}.local-tag[data-v-0ff2d0b3]{color:#2e7d32;background:#e8f5e9;border-radius:3px;padding:1px 8px;font-size:12px;display:inline-block}.path-text[data-v-0ff2d0b3]{color:#666;font-family:monospace;font-size:12px}.source-actions[data-v-0ff2d0b3]{gap:8px;margin-top:8px;display:flex}.local-hint[data-v-0ff2d0b3]{color:#999;background:#00000005;border-radius:4px;margin-top:8px;padding:8px;font-size:12px;line-height:1.5}.siblings-body[data-v-0ff2d0b3]{min-height:60px}.siblings-desc[data-v-0ff2d0b3]{margin-bottom:12px;font-size:13px;line-height:1.6}.siblings-list[data-v-0ff2d0b3]{flex-direction:column;gap:8px;margin-bottom:16px;display:flex}.siblings-actions[data-v-0ff2d0b3]{justify-content:flex-end;gap:8px;display:flex}.batch-modal-body[data-v-0ff2d0b3]{min-height:60px}.batch-desc[data-v-0ff2d0b3]{margin-bottom:16px;font-size:13px;line-height:1.6}.batch-actions[data-v-0ff2d0b3]{justify-content:flex-end;gap:8px;display:flex}.batch-executing[data-v-0ff2d0b3]{color:#666;justify-content:center;align-items:center;gap:12px;padding:24px 0;font-size:13px;display:flex}.batch-result[data-v-0ff2d0b3]{margin-bottom:16px}.batch-result-line[data-v-0ff2d0b3]{flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:10px;font-size:13px;display:flex}.batch-result-label[data-v-0ff2d0b3]{color:#888;flex-shrink:0}.page-header[data-v-46e4fa8d]{justify-content:space-between;align-items:center;margin-bottom:20px;display:flex}.page-header h2[data-v-46e4fa8d]{font-size:20px;font-weight:600}.scene-grid[data-v-46e4fa8d]{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin:2px;display:grid}.scene-card[data-v-46e4fa8d]{transition:box-shadow .2s}.scene-card.active[data-v-46e4fa8d]{box-shadow:0 0 0 2px #2080f0}.scene-desc[data-v-46e4fa8d]{color:#666;margin:0;font-size:13px}.scene-footer[data-v-46e4fa8d]{justify-content:space-between;align-items:center;display:flex}.hint-text[data-v-46e4fa8d]{color:#999;font-size:12px}.modal-skill-groups[data-v-46e4fa8d]{max-height:50vh;overflow-y:auto}.skill-group[data-v-46e4fa8d]{margin-bottom:2px}.skill-group-header[data-v-46e4fa8d]{cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:4px;align-items:center;gap:6px;padding:6px 4px;font-size:13px;display:flex}.skill-group-header[data-v-46e4fa8d]:hover{background:#00000008}.fold-arrow[data-v-46e4fa8d]{color:#999;flex-shrink:0;font-size:10px;transition:transform .2s}.fold-arrow.open[data-v-46e4fa8d]{transform:rotate(90deg)}.skill-group-label[data-v-46e4fa8d]{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-weight:600;overflow:hidden}.skill-group-count[data-v-46e4fa8d]{color:#999;flex-shrink:0;font-size:12px}.skill-group-body[data-v-46e4fa8d]{padding:4px 0 4px 16px}.page-header[data-v-c7da8635]{margin-bottom:20px}.page-header h2[data-v-c7da8635]{font-size:20px;font-weight:600}.section[data-v-c7da8635]{margin-bottom:20px}.init-alert[data-v-c7da8635]{margin-bottom:12px}.missing-list[data-v-c7da8635]{flex-direction:column;gap:4px;display:flex}.missing-item[data-v-c7da8635]{font-size:13px;line-height:1.6}.missing-item code[data-v-c7da8635]{background:#0000000f;border-radius:3px;padding:1px 6px;font-size:12px}.skill-tag[data-v-c7da8635]{margin:1px 2px;display:inline-block}.missing-hint[data-v-c7da8635]{opacity:.7;margin-top:4px;font-size:12px}.source-status[data-v-c7da8635]{margin-bottom:16px}.source-actions[data-v-c7da8635]{flex-direction:column;gap:12px;display:flex}.action-buttons[data-v-c7da8635]{gap:8px;display:flex}.healthy-text[data-v-c7da8635]{color:#18a058;font-size:13px}.issue-text[data-v-c7da8635]{color:#d03050;font-size:13px}.readme-dialog-desc[data-v-c7da8635]{color:#555;margin-bottom:16px;font-size:14px}.readme-checkbox[data-v-c7da8635]{align-items:flex-start;margin-bottom:12px;display:flex}.readme-checkbox-content[data-v-c7da8635]{flex-direction:column;gap:2px;display:flex}.readme-checkbox-title[data-v-c7da8635]{font-size:14px;font-weight:500}.readme-checkbox-desc[data-v-c7da8635]{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}