mdk-skills 2.3.1 → 2.3.3
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-Cj7FAJgB.css → index-DgZ4NBMO.css} +1 -1
- package/scripts/web-ui/dist/assets/{index-CuiL4aIa.js → index-MFtHKGfF.js} +1552 -1552
- package/scripts/web-ui/dist/index.html +2 -2
- package/scripts/web-ui/server.js +74 -18
- package/scripts/web-ui/src/api/skills.js +4 -0
- package/scripts/web-ui/src/views/Dashboard.vue +127 -14
|
@@ -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-MFtHKGfF.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DgZ4NBMO.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="app"></div>
|
package/scripts/web-ui/server.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const http = require("http");
|
|
4
4
|
const crypto = require("crypto");
|
|
5
|
-
const { execSync } = require("child_process");
|
|
5
|
+
const { execSync, exec } = require("child_process");
|
|
6
6
|
const os = require("os");
|
|
7
7
|
const core = require("../core");
|
|
8
8
|
|
|
@@ -167,6 +167,60 @@ function writeSkillMeta(dest, wasUpdated) {
|
|
|
167
167
|
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// ---------- 异步 npx 管理:支持取消正在执行的进程 ----------
|
|
171
|
+
|
|
172
|
+
const runningTasks = new Map();
|
|
173
|
+
|
|
174
|
+
function runAsync(cmd, options = {}) {
|
|
175
|
+
const { cwd, timeout = 120000, taskId = "default" } = options;
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
// 同名任务已存在则先取消
|
|
178
|
+
if (runningTasks.has(taskId)) {
|
|
179
|
+
const old = runningTasks.get(taskId);
|
|
180
|
+
old.kill();
|
|
181
|
+
runningTasks.delete(taskId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const proc = exec(cmd, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
runningTasks.delete(taskId);
|
|
187
|
+
if (err) {
|
|
188
|
+
const e = new Error(err.killed ? "已取消" : (stderr || err.message));
|
|
189
|
+
e.stdout = stdout || "";
|
|
190
|
+
e.stderr = stderr || "";
|
|
191
|
+
e.killed = err.killed;
|
|
192
|
+
reject(e);
|
|
193
|
+
} else {
|
|
194
|
+
resolve({ stdout: stdout || "", stderr: stderr || "" });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
proc.kill();
|
|
200
|
+
runningTasks.delete(taskId);
|
|
201
|
+
const e = new Error("操作超时");
|
|
202
|
+
e.stdout = ""; e.stderr = ""; e.killed = true;
|
|
203
|
+
reject(e);
|
|
204
|
+
}, timeout);
|
|
205
|
+
|
|
206
|
+
proc.on("error", () => { clearTimeout(timer); runningTasks.delete(taskId); });
|
|
207
|
+
runningTasks.set(taskId, proc);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function cancelTask(taskId) {
|
|
212
|
+
if (taskId) {
|
|
213
|
+
const proc = runningTasks.get(taskId);
|
|
214
|
+
if (proc) { proc.kill(); runningTasks.delete(taskId); }
|
|
215
|
+
} else {
|
|
216
|
+
// 不传 taskId 取消全部
|
|
217
|
+
for (const [id, proc] of runningTasks) {
|
|
218
|
+
proc.kill();
|
|
219
|
+
runningTasks.delete(id);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
170
224
|
// 读取 profiles.json
|
|
171
225
|
function loadProfiles() {
|
|
172
226
|
const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
|
|
@@ -392,15 +446,12 @@ async function handleApi(req, res) {
|
|
|
392
446
|
const tmpDir = path.join(os.tmpdir(), "mdk-pull-" + Date.now());
|
|
393
447
|
fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
394
448
|
try {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
stdio: "pipe",
|
|
398
|
-
timeout: 120000,
|
|
399
|
-
});
|
|
400
|
-
} catch {
|
|
449
|
+
await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-pull" });
|
|
450
|
+
} catch (e) {
|
|
401
451
|
cleanNpxTemp();
|
|
402
452
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
403
|
-
return sendJSON(res, {
|
|
453
|
+
if (e.killed) return sendJSON(res, { cancelled: true }, 499);
|
|
454
|
+
return sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
|
|
404
455
|
}
|
|
405
456
|
cleanNpxTemp();
|
|
406
457
|
const imported = [];
|
|
@@ -457,13 +508,12 @@ async function handleApi(req, res) {
|
|
|
457
508
|
const tmpDir = path.join(os.tmpdir(), "mdk-update-" + Date.now());
|
|
458
509
|
fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
459
510
|
try {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
});
|
|
463
|
-
} catch {
|
|
511
|
+
await runAsync("npx --yes skills add \"" + source.url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-update-" + name });
|
|
512
|
+
} catch (e) {
|
|
464
513
|
cleanNpxTemp();
|
|
465
514
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
466
|
-
return sendJSON(res, {
|
|
515
|
+
if (e.killed) return sendJSON(res, { cancelled: true }, 499);
|
|
516
|
+
return sendJSON(res, { error: "重新拉取失败:" + (e.stderr || e.message) }, 400);
|
|
467
517
|
}
|
|
468
518
|
|
|
469
519
|
cleanNpxTemp();
|
|
@@ -519,13 +569,12 @@ async function handleApi(req, res) {
|
|
|
519
569
|
const tmpDir = path.join(os.tmpdir(), "mdk-batch-" + Date.now());
|
|
520
570
|
fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
|
|
521
571
|
try {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
});
|
|
525
|
-
} catch {
|
|
572
|
+
await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-batch" });
|
|
573
|
+
} catch (e) {
|
|
526
574
|
cleanNpxTemp();
|
|
527
575
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
528
|
-
return sendJSON(res, {
|
|
576
|
+
if (e.killed) return sendJSON(res, { cancelled: true }, 499);
|
|
577
|
+
return sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
|
|
529
578
|
}
|
|
530
579
|
|
|
531
580
|
cleanNpxTemp();
|
|
@@ -1144,6 +1193,13 @@ ${skillsList}
|
|
|
1144
1193
|
return sendJSON(res, results);
|
|
1145
1194
|
}
|
|
1146
1195
|
|
|
1196
|
+
// POST /api/tasks/cancel — 取消正在执行的 npx 操作
|
|
1197
|
+
if (method === "POST" && pathname === "/api/tasks/cancel") {
|
|
1198
|
+
const body = await parseBody(req);
|
|
1199
|
+
cancelTask(body.taskId);
|
|
1200
|
+
return sendJSON(res, { ok: true });
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1147
1203
|
// 404
|
|
1148
1204
|
sendJSON(res, { error: "Not Found: " + pathname }, 404);
|
|
1149
1205
|
} catch (err) {
|
|
@@ -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
|
<!-- 编辑面板 -->
|
|
@@ -174,6 +175,7 @@
|
|
|
174
175
|
preset="card"
|
|
175
176
|
style="width: 600px"
|
|
176
177
|
:mask-closable="true"
|
|
178
|
+
@update:show="(v) => { if (!v) { pulling = false; cancelTask(); } }"
|
|
177
179
|
>
|
|
178
180
|
<div class="pull-modal-body">
|
|
179
181
|
<div class="pull-input-row">
|
|
@@ -241,6 +243,57 @@
|
|
|
241
243
|
</div>
|
|
242
244
|
</div>
|
|
243
245
|
</n-modal>
|
|
246
|
+
|
|
247
|
+
<!-- 全部更新弹窗 -->
|
|
248
|
+
<n-modal
|
|
249
|
+
v-model:show="showBatchModal"
|
|
250
|
+
:title="batchPhase === 'confirm' ? '批量更新确认' : batchPhase === 'executing' ? '正在更新...' : batchPhase === 'done' ? '更新完成' : '更新失败'"
|
|
251
|
+
preset="card"
|
|
252
|
+
style="width: 480px"
|
|
253
|
+
:mask-closable="batchPhase !== 'executing'"
|
|
254
|
+
@update:show="(v) => { if (!v && batchPhase === 'executing') cancelTask(); }"
|
|
255
|
+
>
|
|
256
|
+
<div class="batch-modal-body">
|
|
257
|
+
<template v-if="batchPhase === 'confirm'">
|
|
258
|
+
<p class="batch-desc">确定更新 "{{ batchGroup?.url }}" 下的 {{ batchGroup?.skills?.length }} 个技能吗?</p>
|
|
259
|
+
<div class="batch-actions">
|
|
260
|
+
<n-button size="small" @click="showBatchModal = false">取消</n-button>
|
|
261
|
+
<n-button size="small" type="primary" @click="doBatchUpdate">开始更新</n-button>
|
|
262
|
+
</div>
|
|
263
|
+
</template>
|
|
264
|
+
<template v-if="batchPhase === 'executing'">
|
|
265
|
+
<div class="batch-executing">
|
|
266
|
+
<n-spin size="small" />
|
|
267
|
+
<span>正在更新,请稍候...</span>
|
|
268
|
+
</div>
|
|
269
|
+
</template>
|
|
270
|
+
<template v-if="batchPhase === 'done'">
|
|
271
|
+
<div class="batch-result" v-if="batchResult">
|
|
272
|
+
<div v-if="batchResult.updated?.length > 0" class="batch-result-line">
|
|
273
|
+
<span class="batch-result-label">已更新:</span>
|
|
274
|
+
<n-tag v-for="name in batchResult.updated" :key="name" size="small" type="success">{{ name }}</n-tag>
|
|
275
|
+
</div>
|
|
276
|
+
<div v-if="batchResult.unchanged?.length > 0" class="batch-result-line">
|
|
277
|
+
<span class="batch-result-label">已是最新:</span>
|
|
278
|
+
<n-tag v-for="name in batchResult.unchanged" :key="name" size="small">{{ name }}</n-tag>
|
|
279
|
+
</div>
|
|
280
|
+
<div v-if="batchResult.notFound?.length > 0" class="batch-result-line">
|
|
281
|
+
<span class="batch-result-label">未找到:</span>
|
|
282
|
+
<n-tag v-for="name in batchResult.notFound" :key="name" size="small" type="warning">{{ name }}</n-tag>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="batch-actions">
|
|
286
|
+
<n-button size="small" type="primary" @click="showBatchModal = false">完成</n-button>
|
|
287
|
+
</div>
|
|
288
|
+
</template>
|
|
289
|
+
<template v-if="batchPhase === 'error'">
|
|
290
|
+
<n-alert type="error" :bordered="false">{{ batchError }}</n-alert>
|
|
291
|
+
<div class="batch-actions" style="margin-top: 12px;">
|
|
292
|
+
<n-button size="small" type="primary" @click="showBatchModal = false">关闭</n-button>
|
|
293
|
+
</div>
|
|
294
|
+
</template>
|
|
295
|
+
</div>
|
|
296
|
+
</n-modal>
|
|
244
297
|
</div>
|
|
245
298
|
</template>
|
|
246
299
|
|
|
@@ -251,7 +304,7 @@ import { RefreshOutline } from "@vicons/ionicons5";
|
|
|
251
304
|
import { marked } from "marked";
|
|
252
305
|
import hljs from "highlight.js";
|
|
253
306
|
import SkillCard from "../components/SkillCard.vue";
|
|
254
|
-
import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, pullSkills, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir } from "../api/skills";
|
|
307
|
+
import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, pullSkills, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir, cancelTask } from "../api/skills";
|
|
255
308
|
import { sortSkills, getUsageMap } from "../utils/usage";
|
|
256
309
|
|
|
257
310
|
// marked 配置:代码高亮 + 外链安全
|
|
@@ -316,6 +369,13 @@ const updatedSiblings = ref(null);
|
|
|
316
369
|
const siblingCheck = ref({});
|
|
317
370
|
const updatingSiblings = ref(false);
|
|
318
371
|
|
|
372
|
+
// 全部更新弹窗
|
|
373
|
+
const showBatchModal = ref(false);
|
|
374
|
+
const batchPhase = ref("confirm");
|
|
375
|
+
const batchGroup = ref(null);
|
|
376
|
+
const batchResult = ref(null);
|
|
377
|
+
const batchError = ref("");
|
|
378
|
+
|
|
319
379
|
async function handleSiblingUpdate() {
|
|
320
380
|
const selected = Object.keys(siblingCheck.value).filter((k) => siblingCheck.value[k]);
|
|
321
381
|
if (selected.length === 0) {
|
|
@@ -374,6 +434,7 @@ async function handlePull() {
|
|
|
374
434
|
pullError.value = "";
|
|
375
435
|
try {
|
|
376
436
|
const res = await pullSkills(pullUrl.value.trim());
|
|
437
|
+
if (!showPullModal.value) return;
|
|
377
438
|
if (res.ok) {
|
|
378
439
|
pullResult.value = { imported: res.imported, skipped: res.skipped };
|
|
379
440
|
// 默认全选
|
|
@@ -385,6 +446,7 @@ async function handlePull() {
|
|
|
385
446
|
pullError.value = res.error || "拉取失败";
|
|
386
447
|
}
|
|
387
448
|
} catch (e) {
|
|
449
|
+
if (!showPullModal.value) return;
|
|
388
450
|
pullError.value = "拉取失败,请检查地址或网络";
|
|
389
451
|
} finally {
|
|
390
452
|
pulling.value = false;
|
|
@@ -541,6 +603,7 @@ async function handleUpdate() {
|
|
|
541
603
|
updatingSkill.value = true;
|
|
542
604
|
try {
|
|
543
605
|
const res = await updateSkill(detailSkill.value.name);
|
|
606
|
+
if (!detailVisible.value) return;
|
|
544
607
|
if (res.ok) {
|
|
545
608
|
if (res.updated === false) {
|
|
546
609
|
message.info("已是最新版本,无需更新");
|
|
@@ -565,6 +628,7 @@ async function handleUpdate() {
|
|
|
565
628
|
message.error(res.error || "更新失败");
|
|
566
629
|
}
|
|
567
630
|
} catch {
|
|
631
|
+
if (!detailVisible.value) return;
|
|
568
632
|
message.error("更新失败");
|
|
569
633
|
} finally {
|
|
570
634
|
updatingSkill.value = false;
|
|
@@ -572,25 +636,29 @@ async function handleUpdate() {
|
|
|
572
636
|
}
|
|
573
637
|
|
|
574
638
|
async function batchUpdateGroup(group) {
|
|
575
|
-
|
|
576
|
-
|
|
639
|
+
batchGroup.value = group;
|
|
640
|
+
batchResult.value = null;
|
|
641
|
+
batchError.value = "";
|
|
642
|
+
batchPhase.value = "confirm";
|
|
643
|
+
showBatchModal.value = true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function doBatchUpdate() {
|
|
647
|
+
batchPhase.value = "executing";
|
|
577
648
|
try {
|
|
578
|
-
const names =
|
|
579
|
-
const res = await batchUpdateSkills(names,
|
|
649
|
+
const names = batchGroup.value.skills.map((s) => s.name);
|
|
650
|
+
const res = await batchUpdateSkills(names, batchGroup.value.url);
|
|
580
651
|
if (res.ok) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
message.info(`${res.unchanged.length} 个技能已是最新,跳过`);
|
|
584
|
-
}
|
|
585
|
-
if (res.notFound && res.notFound.length > 0) {
|
|
586
|
-
message.warning(`${res.notFound.length} 个技能未找到,已跳过`);
|
|
587
|
-
}
|
|
652
|
+
batchResult.value = { updated: res.updated, unchanged: res.unchanged, notFound: res.notFound };
|
|
653
|
+
batchPhase.value = "done";
|
|
588
654
|
await loadSkills();
|
|
589
655
|
} else {
|
|
590
|
-
|
|
656
|
+
batchError.value = res.error || "批量更新失败";
|
|
657
|
+
batchPhase.value = "error";
|
|
591
658
|
}
|
|
592
659
|
} catch {
|
|
593
|
-
|
|
660
|
+
batchError.value = "批量更新失败";
|
|
661
|
+
batchPhase.value = "error";
|
|
594
662
|
}
|
|
595
663
|
}
|
|
596
664
|
|
|
@@ -931,4 +999,49 @@ onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
|
|
|
931
999
|
justify-content: flex-end;
|
|
932
1000
|
gap: 8px;
|
|
933
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
/* 全部更新弹窗 */
|
|
1004
|
+
.batch-modal-body {
|
|
1005
|
+
min-height: 60px;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.batch-desc {
|
|
1009
|
+
font-size: 13px;
|
|
1010
|
+
line-height: 1.6;
|
|
1011
|
+
margin-bottom: 16px;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.batch-actions {
|
|
1015
|
+
display: flex;
|
|
1016
|
+
justify-content: flex-end;
|
|
1017
|
+
gap: 8px;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.batch-executing {
|
|
1021
|
+
display: flex;
|
|
1022
|
+
align-items: center;
|
|
1023
|
+
gap: 12px;
|
|
1024
|
+
padding: 24px 0;
|
|
1025
|
+
justify-content: center;
|
|
1026
|
+
color: #666;
|
|
1027
|
+
font-size: 13px;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
.batch-result {
|
|
1031
|
+
margin-bottom: 16px;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.batch-result-line {
|
|
1035
|
+
display: flex;
|
|
1036
|
+
align-items: center;
|
|
1037
|
+
flex-wrap: wrap;
|
|
1038
|
+
gap: 6px;
|
|
1039
|
+
margin-bottom: 10px;
|
|
1040
|
+
font-size: 13px;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.batch-result-label {
|
|
1044
|
+
color: #888;
|
|
1045
|
+
flex-shrink: 0;
|
|
1046
|
+
}
|
|
934
1047
|
</style>
|