mdk-skills 2.3.2 → 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.
@@ -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-BnMBIily.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BItq1iGH.css">
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>
@@ -169,22 +169,58 @@ function writeSkillMeta(dest, wasUpdated) {
169
169
 
170
170
  // ---------- 异步 npx 管理:支持取消正在执行的进程 ----------
171
171
 
172
- let _npxProcess = null;
172
+ const runningTasks = new Map();
173
173
 
174
- function runNpx(cmd, cwd) {
174
+ function runAsync(cmd, options = {}) {
175
+ const { cwd, timeout = 120000, taskId = "default" } = options;
175
176
  return new Promise((resolve, reject) => {
176
- const proc = exec(cmd, { cwd, timeout: 120000 }, (err) => {
177
- _npxProcess = null;
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);
178
187
  if (err) {
179
- reject(err.killed ? new Error("已取消") : 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);
180
193
  } else {
181
- resolve();
194
+ resolve({ stdout: stdout || "", stderr: stderr || "" });
182
195
  }
183
196
  });
184
- _npxProcess = proc;
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);
185
208
  });
186
209
  }
187
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
+
188
224
  // 读取 profiles.json
189
225
  function loadProfiles() {
190
226
  const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
@@ -410,12 +446,12 @@ async function handleApi(req, res) {
410
446
  const tmpDir = path.join(os.tmpdir(), "mdk-pull-" + Date.now());
411
447
  fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
412
448
  try {
413
- await runNpx("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", tmpDir);
449
+ await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-pull" });
414
450
  } catch (e) {
415
451
  cleanNpxTemp();
416
452
  fs.rmSync(tmpDir, { recursive: true, force: true });
417
- if (e.message === "已取消") return sendJSON(res, { cancelled: true }, 499);
418
- return sendJSON(res, { error: "拉取失败,请检查地址或网络连接" }, 400);
453
+ if (e.killed) return sendJSON(res, { cancelled: true }, 499);
454
+ return sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
419
455
  }
420
456
  cleanNpxTemp();
421
457
  const imported = [];
@@ -472,12 +508,12 @@ async function handleApi(req, res) {
472
508
  const tmpDir = path.join(os.tmpdir(), "mdk-update-" + Date.now());
473
509
  fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
474
510
  try {
475
- await runNpx("npx --yes skills add \"" + source.url + "\" --copy -y -a claude-code", tmpDir);
511
+ await runAsync("npx --yes skills add \"" + source.url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-update-" + name });
476
512
  } catch (e) {
477
513
  cleanNpxTemp();
478
514
  fs.rmSync(tmpDir, { recursive: true, force: true });
479
- if (e.message === "已取消") return sendJSON(res, { cancelled: true }, 499);
480
- return sendJSON(res, { error: "重新拉取失败,请检查网络连接" }, 400);
515
+ if (e.killed) return sendJSON(res, { cancelled: true }, 499);
516
+ return sendJSON(res, { error: "重新拉取失败:" + (e.stderr || e.message) }, 400);
481
517
  }
482
518
 
483
519
  cleanNpxTemp();
@@ -533,12 +569,12 @@ async function handleApi(req, res) {
533
569
  const tmpDir = path.join(os.tmpdir(), "mdk-batch-" + Date.now());
534
570
  fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
535
571
  try {
536
- await runNpx("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", tmpDir);
572
+ await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-batch" });
537
573
  } catch (e) {
538
574
  cleanNpxTemp();
539
575
  fs.rmSync(tmpDir, { recursive: true, force: true });
540
- if (e.message === "已取消") return sendJSON(res, { cancelled: true }, 499);
541
- return sendJSON(res, { error: "拉取失败,请检查网络连接" }, 400);
576
+ if (e.killed) return sendJSON(res, { cancelled: true }, 499);
577
+ return sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
542
578
  }
543
579
 
544
580
  cleanNpxTemp();
@@ -1159,10 +1195,8 @@ ${skillsList}
1159
1195
 
1160
1196
  // POST /api/tasks/cancel — 取消正在执行的 npx 操作
1161
1197
  if (method === "POST" && pathname === "/api/tasks/cancel") {
1162
- if (_npxProcess) {
1163
- _npxProcess.kill();
1164
- _npxProcess = null;
1165
- }
1198
+ const body = await parseBody(req);
1199
+ cancelTask(body.taskId);
1166
1200
  return sendJSON(res, { ok: true });
1167
1201
  }
1168
1202
 
@@ -243,6 +243,57 @@
243
243
  </div>
244
244
  </div>
245
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>
246
297
  </div>
247
298
  </template>
248
299
 
@@ -318,6 +369,13 @@ const updatedSiblings = ref(null);
318
369
  const siblingCheck = ref({});
319
370
  const updatingSiblings = ref(false);
320
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
+
321
379
  async function handleSiblingUpdate() {
322
380
  const selected = Object.keys(siblingCheck.value).filter((k) => siblingCheck.value[k]);
323
381
  if (selected.length === 0) {
@@ -578,25 +636,29 @@ async function handleUpdate() {
578
636
  }
579
637
 
580
638
  async function batchUpdateGroup(group) {
581
- const ok = window.confirm(`确定更新 "${group.url}" 下的 ${group.skills.length} 个技能吗?`);
582
- if (!ok) return;
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";
583
648
  try {
584
- const names = group.skills.map((s) => s.name);
585
- const res = await batchUpdateSkills(names, group.url);
649
+ const names = batchGroup.value.skills.map((s) => s.name);
650
+ const res = await batchUpdateSkills(names, batchGroup.value.url);
586
651
  if (res.ok) {
587
- message.success(`${res.updated.length} 个技能已更新`);
588
- if (res.unchanged && res.unchanged.length > 0) {
589
- message.info(`${res.unchanged.length} 个技能已是最新,跳过`);
590
- }
591
- if (res.notFound && res.notFound.length > 0) {
592
- message.warning(`${res.notFound.length} 个技能未找到,已跳过`);
593
- }
652
+ batchResult.value = { updated: res.updated, unchanged: res.unchanged, notFound: res.notFound };
653
+ batchPhase.value = "done";
594
654
  await loadSkills();
595
655
  } else {
596
- message.error(res.error || "批量更新失败");
656
+ batchError.value = res.error || "批量更新失败";
657
+ batchPhase.value = "error";
597
658
  }
598
659
  } catch {
599
- message.error("批量更新失败");
660
+ batchError.value = "批量更新失败";
661
+ batchPhase.value = "error";
600
662
  }
601
663
  }
602
664
 
@@ -937,4 +999,49 @@ onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
937
999
  justify-content: flex-end;
938
1000
  gap: 8px;
939
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
+ }
940
1047
  </style>