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.
- package/package.json +1 -1
- package/scripts/core.js +1 -0
- package/scripts/web-ui/dist/assets/index-BItq1iGH.css +1 -0
- package/scripts/web-ui/dist/assets/{index--qLTFieg.js → index-BnMBIily.js} +26 -26
- package/scripts/web-ui/dist/index.html +2 -2
- package/scripts/web-ui/server.js +124 -52
- package/scripts/web-ui/src/api/skills.js +4 -0
- package/scripts/web-ui/src/components/SkillCard.vue +1 -1
- package/scripts/web-ui/src/views/Dashboard.vue +34 -12
- package/scripts/web-ui/dist/assets/index-DhB4kj3N.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-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>
|
package/scripts/web-ui/server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const http = require("http");
|
|
4
|
-
const
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</span>
|
|
11
11
|
</template>
|
|
12
12
|
<div class="skill-meta">
|
|
13
|
-
<span class="skill-version">
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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}
|