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.
- package/package.json +1 -1
- package/scripts/web-ui/dist/assets/index-DdbAYqm7.css +1 -0
- package/scripts/web-ui/dist/assets/{index-DwjE_sXd.js → index-pHDOtonU.js} +3 -3
- package/scripts/web-ui/dist/index.html +2 -2
- package/scripts/web-ui/server.js +71 -27
- package/scripts/web-ui/src/api/skills.js +2 -2
- package/scripts/web-ui/src/views/Dashboard.vue +135 -42
- package/scripts/web-ui/dist/assets/index-BGLelyx5.css +0 -1
|
@@ -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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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>
|
package/scripts/web-ui/server.js
CHANGED
|
@@ -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 —
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
|
461
|
-
|
|
462
|
-
if (!
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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: "
|
|
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="
|
|
180
|
-
<n-button size="small" type="primary" @click="
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
495
|
+
previewing.value = false;
|
|
451
496
|
}
|
|
452
497
|
}
|
|
453
498
|
|
|
454
|
-
async function
|
|
455
|
-
const selected = Object.keys(
|
|
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
|
-
|
|
505
|
+
pulling.value = true;
|
|
506
|
+
pullError.value = "";
|
|
461
507
|
try {
|
|
462
|
-
const res = await
|
|
508
|
+
const res = await pullSkills(pullUrl.value.trim(), selected);
|
|
509
|
+
if (!showPullModal.value) return;
|
|
463
510
|
if (res.ok) {
|
|
464
|
-
|
|
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
|
-
|
|
521
|
+
} catch (e) {
|
|
522
|
+
if (!showPullModal.value) return;
|
|
523
|
+
pullError.value = "拉取失败,请检查地址或网络";
|
|
472
524
|
} finally {
|
|
473
|
-
|
|
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}
|