mdk-skills 2.3.26 → 2.3.28

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.
@@ -7,8 +7,8 @@
7
7
  <script>
8
8
  (function(){var t=localStorage.getItem("mdk-theme");if(t==="dark")document.documentElement.setAttribute("data-theme","dark")})();
9
9
  </script>
10
- <script type="module" crossorigin src="/assets/index-D57SEgQI.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-qfXYFvR5.css">
10
+ <script type="module" crossorigin src="/assets/index-Dr0-RSil.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-B3_Vjkxx.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="app"></div>
@@ -96,6 +96,23 @@ function addPullSource(skillName, url) {
96
96
  writePullSource(data);
97
97
  }
98
98
 
99
+ // 安全删除目录/文件,处理 Windows 文件锁定(EBUSY)
100
+ // 返回 { removed: boolean, reason?: 'not_found' | 'locked' | 'error', message?: string }
101
+ function safeRmSync(dir, label) {
102
+ if (!fs.existsSync(dir)) return { removed: false, reason: "not_found" };
103
+ try {
104
+ fs.rmSync(dir, { recursive: true, force: true });
105
+ return { removed: true };
106
+ } catch (err) {
107
+ if (err.code === "EBUSY") {
108
+ console.error(`跳过删除 "${label || dir}"(文件被其他程序占用)`);
109
+ return { removed: false, reason: "locked" };
110
+ }
111
+ console.error(`删除 "${label || dir}" 失败:`, err.message);
112
+ return { removed: false, reason: "error", message: err.message };
113
+ }
114
+ }
115
+
99
116
  // 检查技能源是否已设置,未设置则返回 400
100
117
  function requireSource(res) {
101
118
  if (!skillsSource || !pkgSkillsSource) {
@@ -258,6 +275,8 @@ function loadProfiles() {
258
275
  function applyProfile(profile) {
259
276
  const pkgSkills = core.listSkillDirs(pkgSkillsSource);
260
277
  const settings = readSettings();
278
+ const failedDelete = [];
279
+ const failedInstall = [];
261
280
 
262
281
  let selected;
263
282
  if (profile.skills === null) {
@@ -281,24 +300,18 @@ function applyProfile(profile) {
281
300
  copyDirSync(src, dest);
282
301
  } catch (err) {
283
302
  console.error(`安装技能 "${name}" 失败:`, err.message);
303
+ failedInstall.push(name);
284
304
  }
285
305
  }
286
306
  } else {
287
307
  if (fs.existsSync(dest)) {
288
- try {
289
- fs.rmSync(dest, { recursive: true, force: true });
290
- } catch (err) {
291
- if (err.code === 'EBUSY') {
292
- console.error(`切换场景:跳过删除 "${name}"(文件被其他程序占用)`);
293
- } else {
294
- console.error(`切换场景:删除 "${name}" 失败:`, err.message);
295
- }
296
- }
308
+ const r = safeRmSync(dest, name);
309
+ if (!r.removed) failedDelete.push(name);
297
310
  }
298
311
  }
299
312
  }
300
313
 
301
- // 更新 settings
314
+ // 更新 settings(文件操作已执行完毕,现在写配置)
302
315
  if (!settings.skills) settings.skills = {};
303
316
  for (const name of pkgSkills) {
304
317
  const enabled = selected.includes(name);
@@ -331,12 +344,14 @@ function applyProfile(profile) {
331
344
  selected,
332
345
  enabled: selected.length,
333
346
  disabled: pkgSkills.length - selected.length,
347
+ failedDelete,
348
+ failedInstall,
334
349
  };
335
350
  }
336
351
 
337
352
  // 安装勾选的技能
338
353
  function installSelectedSkills(selectedNames) {
339
- if (!fs.existsSync(pkgSkillsSource)) return { installed: 0 };
354
+ if (!fs.existsSync(pkgSkillsSource)) return { installed: 0, locked: [] };
340
355
 
341
356
  if (!fs.existsSync(skillsDest)) {
342
357
  fs.mkdirSync(skillsDest, { recursive: true });
@@ -344,6 +359,7 @@ function installSelectedSkills(selectedNames) {
344
359
 
345
360
  const allPkgSkills = core.listSkillDirs(pkgSkillsSource);
346
361
  let installedCount = 0;
362
+ const locked = [];
347
363
 
348
364
  for (const name of allPkgSkills) {
349
365
  const src = path.join(pkgSkillsSource, name);
@@ -360,15 +376,8 @@ function installSelectedSkills(selectedNames) {
360
376
  }
361
377
  } else {
362
378
  if (fs.existsSync(dest)) {
363
- try {
364
- fs.rmSync(dest, { recursive: true, force: true });
365
- } catch (err) {
366
- if (err.code === 'EBUSY') {
367
- console.error(`安装技能:跳过删除 "${name}"(文件被其他程序占用)`);
368
- } else {
369
- console.error(`安装技能:删除 "${name}" 失败:`, err.message);
370
- }
371
- }
379
+ const r = safeRmSync(dest, name);
380
+ if (!r.removed && r.reason === 'locked') locked.push(name);
372
381
  }
373
382
  }
374
383
  }
@@ -389,7 +398,7 @@ function installSelectedSkills(selectedNames) {
389
398
  fs.copyFileSync(mdSource, mdDest);
390
399
  }
391
400
 
392
- return { installed: installedCount };
401
+ return { installed: installedCount, locked };
393
402
  }
394
403
 
395
404
  // ---------- API 路由 ----------
@@ -424,8 +433,8 @@ async function handleApi(req, res) {
424
433
  if (!pkgNames.has(us.name)) {
425
434
  const dest = path.join(skillsDest, us.name);
426
435
  if (fs.existsSync(dest)) {
427
- fs.rmSync(dest, { recursive: true, force: true });
428
- cleaned = true;
436
+ const r = safeRmSync(dest, us.name);
437
+ if (r.removed) cleaned = true;
429
438
  }
430
439
  }
431
440
  }
@@ -462,7 +471,10 @@ async function handleApi(req, res) {
462
471
  }
463
472
  } else {
464
473
  if (fs.existsSync(dest)) {
465
- fs.rmSync(dest, { recursive: true, force: true });
474
+ const r = safeRmSync(dest, name);
475
+ if (!r.removed && r.reason === "locked") {
476
+ return sendJSON(res, { error: `技能 "${name}" 正被其他程序使用,无法停用` }, 409);
477
+ }
466
478
  }
467
479
  }
468
480
  const settings = readSettings();
@@ -509,7 +521,7 @@ async function handleApi(req, res) {
509
521
  await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-pull-preview" });
510
522
  } catch (e) {
511
523
  cleanNpxTemp();
512
- fs.rmSync(tmpDir, { recursive: true, force: true });
524
+ safeRmSync(tmpDir, "临时目录");
513
525
  if (e.killed) return sendJSON(res, { cancelled: true }, 499);
514
526
  return sendJSON(res, { error: "预览失败:" + (e.stderr || e.message) }, 400);
515
527
  }
@@ -517,7 +529,7 @@ async function handleApi(req, res) {
517
529
  const skillsDir = path.join(tmpDir, ".claude", "skills");
518
530
  const skills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
519
531
  if (skills.length === 0) {
520
- fs.rmSync(tmpDir, { recursive: true, force: true });
532
+ safeRmSync(tmpDir, "临时目录");
521
533
  return sendJSON(res, { error: "未找到有效技能" }, 400);
522
534
  }
523
535
  // 缓存,不删 tmpDir
@@ -542,7 +554,11 @@ async function handleApi(req, res) {
542
554
  }
543
555
  const dest = path.join(pkgSkillsSource, name);
544
556
  if (fs.existsSync(dest)) {
545
- fs.rmSync(dest, { recursive: true, force: true });
557
+ const r = safeRmSync(dest, name + "(源目录)");
558
+ if (!r.removed && r.reason === "locked") {
559
+ skipped.push(name);
560
+ continue;
561
+ }
546
562
  }
547
563
  copyDirSync(skillPath, dest);
548
564
  writeSkillMeta(dest, false);
@@ -585,19 +601,26 @@ async function handleApi(req, res) {
585
601
  if (!Array.isArray(names)) return sendJSON(res, { error: "参数错误" }, 400);
586
602
  const settings = readSettings();
587
603
  if (!settings.skills) settings.skills = {};
604
+ const locked = [];
588
605
  for (const name of names) {
589
606
  const src = path.join(pkgSkillsSource, name);
590
607
  const dest = path.join(skillsDest, name);
591
608
  if (enabled) {
592
609
  if (fs.existsSync(src) && !fs.existsSync(dest)) copyDirSync(src, dest);
593
610
  } else {
594
- if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
611
+ if (fs.existsSync(dest)) {
612
+ const r = safeRmSync(dest, name);
613
+ if (!r.removed && r.reason === "locked") {
614
+ locked.push(name);
615
+ continue;
616
+ }
617
+ }
595
618
  }
596
619
  if (!settings.skills[name]) settings.skills[name] = {};
597
620
  settings.skills[name].enabled = !!enabled;
598
621
  }
599
622
  writeSettings(settings);
600
- return sendJSON(res, { ok: true });
623
+ return sendJSON(res, { ok: true, locked });
601
624
  }
602
625
 
603
626
  // POST /api/skills/bulk-delete — 批量删除技能(源目录 + 项目目录)
@@ -607,15 +630,22 @@ async function handleApi(req, res) {
607
630
  const { names } = body;
608
631
  if (!Array.isArray(names) || names.length === 0) return sendJSON(res, { error: "参数错误" }, 400);
609
632
  const settings = readSettings();
633
+ const locked = [];
634
+ const deleted = [];
610
635
  for (const name of names) {
611
636
  const sourceDir = path.join(pkgSkillsSource, name);
612
637
  const destDir = path.join(skillsDest, name);
613
- if (fs.existsSync(sourceDir)) fs.rmSync(sourceDir, { recursive: true, force: true });
614
- if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
638
+ const r1 = safeRmSync(sourceDir, name + "(源目录)");
639
+ const r2 = safeRmSync(destDir, name + "(项目目录)");
640
+ if (r1.reason === "locked" || r2.reason === "locked") {
641
+ locked.push(name);
642
+ continue;
643
+ }
615
644
  if (settings.skills && settings.skills[name] !== undefined) delete settings.skills[name];
645
+ deleted.push(name);
616
646
  }
617
647
  writeSettings(settings);
618
- return sendJSON(res, { ok: true, deleted: names });
648
+ return sendJSON(res, { ok: true, deleted, locked });
619
649
  }
620
650
 
621
651
  // POST /api/skills/pull-cancel — 取消预览,清理缓存和临时目录
@@ -662,7 +692,7 @@ async function handleApi(req, res) {
662
692
  await runAsync("npx --yes skills add \"" + source.url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-update-" + name });
663
693
  } catch (e) {
664
694
  cleanNpxTemp();
665
- fs.rmSync(tmpDir, { recursive: true, force: true });
695
+ safeRmSync(tmpDir, "临时目录");
666
696
  if (e.killed) return sendJSON(res, { cancelled: true }, 499);
667
697
  return sendJSON(res, { error: "重新拉取失败:" + (e.stderr || e.message) }, 400);
668
698
  }
@@ -670,7 +700,7 @@ async function handleApi(req, res) {
670
700
  cleanNpxTemp();
671
701
  const skillPath = path.join(tmpDir, ".claude", "skills", name);
672
702
  if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
673
- fs.rmSync(tmpDir, { recursive: true, force: true });
703
+ safeRmSync(tmpDir, "临时目录");
674
704
  return sendJSON(res, { error: "远程仓库中未找到该技能" }, 400);
675
705
  }
676
706
 
@@ -680,18 +710,26 @@ async function handleApi(req, res) {
680
710
  const newFingerprint = calcSkillFingerprint(skillPath);
681
711
 
682
712
  if (oldFingerprint === newFingerprint) {
683
- fs.rmSync(tmpDir, { recursive: true, force: true });
713
+ safeRmSync(tmpDir, "临时目录");
684
714
  return sendJSON(res, { ok: true, updated: false });
685
715
  }
686
716
 
687
717
  // 有变更:覆盖源目录
688
- if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
718
+ const rSrc = safeRmSync(dest, name + "(源目录)");
719
+ if (!rSrc.removed && rSrc.reason === "locked") {
720
+ safeRmSync(tmpDir, "临时目录");
721
+ return sendJSON(res, { error: `技能"${name}"被其他程序占用,无法更新`, locked: true }, 409);
722
+ }
689
723
  copyDirSync(skillPath, dest);
690
724
 
691
725
  // 如果项目目录已启用,同步覆盖
692
726
  const projectSkill = path.join(skillsDest, name);
693
727
  if (fs.existsSync(projectSkill)) {
694
- fs.rmSync(projectSkill, { recursive: true, force: true });
728
+ const rProj = safeRmSync(projectSkill, name + "(项目目录)");
729
+ if (!rProj.removed && rProj.reason === "locked") {
730
+ safeRmSync(tmpDir, "临时目录");
731
+ return sendJSON(res, { error: `技能"${name}"项目目录被其他程序占用,无法更新`, locked: true }, 409);
732
+ }
695
733
  copyDirSync(skillPath, projectSkill);
696
734
  }
697
735
 
@@ -705,7 +743,7 @@ async function handleApi(req, res) {
705
743
  // 找同源兄弟
706
744
  const siblings = Object.keys(pullSource).filter(k => k !== name && pullSource[k].url === source.url);
707
745
 
708
- fs.rmSync(tmpDir, { recursive: true, force: true });
746
+ safeRmSync(tmpDir, "临时目录");
709
747
  return sendJSON(res, { ok: true, name, updatedAt: source.pulledAt, siblings });
710
748
  }
711
749
 
@@ -724,7 +762,7 @@ async function handleApi(req, res) {
724
762
  await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-batch" });
725
763
  } catch (e) {
726
764
  cleanNpxTemp();
727
- fs.rmSync(tmpDir, { recursive: true, force: true });
765
+ safeRmSync(tmpDir, "临时目录");
728
766
  if (e.killed) return sendJSON(res, { cancelled: true }, 499);
729
767
  return sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
730
768
  }
@@ -734,6 +772,7 @@ async function handleApi(req, res) {
734
772
  const updated = [];
735
773
  const unchanged = [];
736
774
  const notFound = [];
775
+ const locked = [];
737
776
  for (const name of names) {
738
777
  const skillPath = path.join(tmpDir, ".claude", "skills", name);
739
778
  if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
@@ -747,12 +786,20 @@ async function handleApi(req, res) {
747
786
  unchanged.push(name);
748
787
  continue;
749
788
  }
750
- if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
789
+ const rSrc = safeRmSync(dest, name + "(源目录)");
790
+ if (!rSrc.removed && rSrc.reason === "locked") {
791
+ locked.push(name);
792
+ continue;
793
+ }
751
794
  copyDirSync(skillPath, dest);
752
795
 
753
796
  const projectSkill = path.join(skillsDest, name);
754
797
  if (fs.existsSync(projectSkill)) {
755
- fs.rmSync(projectSkill, { recursive: true, force: true });
798
+ const rProj = safeRmSync(projectSkill, name + "(项目目录)");
799
+ if (!rProj.removed && rProj.reason === "locked") {
800
+ locked.push(name);
801
+ continue;
802
+ }
756
803
  copyDirSync(skillPath, projectSkill);
757
804
  }
758
805
 
@@ -769,8 +816,8 @@ async function handleApi(req, res) {
769
816
  }
770
817
  writePullSource(pullSource);
771
818
 
772
- fs.rmSync(tmpDir, { recursive: true, force: true });
773
- return sendJSON(res, { ok: true, updated, unchanged, notFound });
819
+ safeRmSync(tmpDir, "临时目录");
820
+ return sendJSON(res, { ok: true, updated, unchanged, notFound, locked });
774
821
  }
775
822
 
776
823
  // POST /api/skills/:name/open — 在文件管理器中打开技能目录
@@ -790,6 +837,50 @@ async function handleApi(req, res) {
790
837
  }
791
838
  }
792
839
 
840
+ // POST /api/skills/:name/retry-delete — 重试删除单个技能(项目目录)
841
+ const retryDeleteMatch = pathname.match(/^\/api\/skills\/([^/]+)\/retry-delete$/);
842
+ if (method === "POST" && retryDeleteMatch) {
843
+ if (!requireSource(res)) return;
844
+ const name = retryDeleteMatch[1];
845
+ const dest = path.join(skillsDest, name);
846
+ if (!fs.existsSync(dest)) return sendJSON(res, { ok: true, name, skipped: true });
847
+ const r = safeRmSync(dest, name);
848
+ if (r.removed) return sendJSON(res, { ok: true, name });
849
+ if (r.reason === "locked") return sendJSON(res, { error: `技能"${name}"被其他程序占用,无法删除`, locked: true }, 409);
850
+ return sendJSON(res, { error: `删除"${name}"失败: ${r.message || "未知错误"}` }, 500);
851
+ }
852
+
853
+ // POST /api/skills/:name/retry-install — 重试安装单个技能
854
+ const retryInstallMatch = pathname.match(/^\/api\/skills\/([^/]+)\/retry-install$/);
855
+ if (method === "POST" && retryInstallMatch) {
856
+ if (!requireSource(res)) return;
857
+ const name = retryInstallMatch[1];
858
+ const src = path.join(pkgSkillsSource, name);
859
+ const dest = path.join(skillsDest, name);
860
+ if (fs.existsSync(dest)) {
861
+ const r = safeRmSync(dest, name);
862
+ if (!r.removed) return sendJSON(res, { error: `无法覆盖现有目录: ${r.reason}` }, r.reason === "locked" ? 409 : 500);
863
+ }
864
+ if (!fs.existsSync(src)) return sendJSON(res, { error: `源目录不存在: ${name}` }, 404);
865
+ try {
866
+ copyDirSync(src, dest);
867
+ return sendJSON(res, { ok: true, name });
868
+ } catch (err) {
869
+ return sendJSON(res, { error: `安装"${name}"失败: ${err.message}` }, 500);
870
+ }
871
+ }
872
+
873
+ // POST /api/skills-dest/open — 打开项目技能目录
874
+ if (method === "POST" && pathname === "/api/skills-dest/open") {
875
+ if (!requireSource(res)) return;
876
+ try {
877
+ execSync("start \"\" \"" + skillsDest + "\"", { stdio: "ignore" });
878
+ return sendJSON(res, { ok: true });
879
+ } catch {
880
+ return sendJSON(res, { error: "打开目录失败" }, 500);
881
+ }
882
+ }
883
+
793
884
  // GET /api/readme — 获取根目录 README.md
794
885
  if (method === "GET" && pathname === "/api/readme") {
795
886
  if (!skillsSource) return sendJSON(res, { content: null, found: false });
@@ -868,14 +959,18 @@ async function handleApi(req, res) {
868
959
  const sourceDir = path.join(pkgSkillsSource, name);
869
960
  const destDir = path.join(skillsDest, name);
870
961
  let deleted = [];
962
+ let locked = false;
871
963
 
872
- if (fs.existsSync(sourceDir)) {
873
- fs.rmSync(sourceDir, { recursive: true, force: true });
874
- deleted.push("源目录");
875
- }
876
- if (fs.existsSync(destDir)) {
877
- fs.rmSync(destDir, { recursive: true, force: true });
878
- deleted.push("项目目录");
964
+ const r1 = safeRmSync(sourceDir, name + "(源目录)");
965
+ if (r1.removed) deleted.push("源目录");
966
+ else if (r1.reason === "locked") locked = true;
967
+
968
+ const r2 = safeRmSync(destDir, name + "(项目目录)");
969
+ if (r2.removed) deleted.push("项目目录");
970
+ else if (r2.reason === "locked") locked = true;
971
+
972
+ if (locked) {
973
+ return sendJSON(res, { error: `技能"${name}"被其他程序占用,无法删除`, locked: true }, 409);
879
974
  }
880
975
 
881
976
  // 清理 settings 中的启用状态
@@ -1125,7 +1220,7 @@ async function handleApi(req, res) {
1125
1220
  const src = path.join(skillsDest, name);
1126
1221
  const dest = path.join(repoSkills, name);
1127
1222
  if (fs.existsSync(dest))
1128
- fs.rmSync(dest, { recursive: true, force: true });
1223
+ safeRmSync(dest, name);
1129
1224
  copyDirSync(src, dest);
1130
1225
  }
1131
1226
  }
@@ -152,6 +152,24 @@ export function openSkillDir(name) {
152
152
  });
153
153
  }
154
154
 
155
+ export function retryDelete(name) {
156
+ return request("/skills/" + encodeURIComponent(name) + "/retry-delete", {
157
+ method: "POST",
158
+ });
159
+ }
160
+
161
+ export function retryInstall(name) {
162
+ return request("/skills/" + encodeURIComponent(name) + "/retry-install", {
163
+ method: "POST",
164
+ });
165
+ }
166
+
167
+ export function openSkillsDest() {
168
+ return request("/skills-dest/open", {
169
+ method: "POST",
170
+ });
171
+ }
172
+
155
173
  export function createReadme(data) {
156
174
  return request("/readme/create", {
157
175
  method: "POST",
@@ -45,6 +45,8 @@ async function onToggle(value) {
45
45
  if (value) recordUsage(props.skill.name);
46
46
  message.success(`技能 "${props.skill.name}" 已${value ? "启用" : "停用"}`);
47
47
  emit("refresh");
48
+ } else if (res.locked) {
49
+ message.warning(`技能 "${props.skill.name}" 被其他程序占用,无法${value ? "启用" : "停用"}`);
48
50
  }
49
51
  } catch {
50
52
  message.error("操作失败");
@@ -922,6 +922,8 @@ async function handleDelete() {
922
922
  message.success(`技能 "${detailSkill.value.name}" 已删除`);
923
923
  detailVisible.value = false;
924
924
  await loadSkills();
925
+ } else if (res.locked) {
926
+ message.warning(res.error || `技能 "${detailSkill.value.name}" 被其他程序占用,无法删除`);
925
927
  }
926
928
  } catch {
927
929
  message.error("删除失败");
@@ -1008,8 +1010,12 @@ async function onBatchMenuSelect(key, group) {
1008
1010
  switch (key) {
1009
1011
  case "enable":
1010
1012
  try {
1011
- await bulkToggleSkills(names, true);
1012
- message.success(`${names.length} 个技能已启用`);
1013
+ const r1 = await bulkToggleSkills(names, true);
1014
+ if (r1.locked?.length) {
1015
+ message.warning(`已启用 ${names.length - r1.locked.length} 个技能,${r1.locked.length} 个被占用`);
1016
+ } else {
1017
+ message.success(`${names.length} 个技能已启用`);
1018
+ }
1013
1019
  await loadSkills();
1014
1020
  } catch {
1015
1021
  message.error("操作失败");
@@ -1017,8 +1023,12 @@ async function onBatchMenuSelect(key, group) {
1017
1023
  break;
1018
1024
  case "disable":
1019
1025
  try {
1020
- await bulkToggleSkills(names, false);
1021
- message.success(`${names.length} 个技能已停用`);
1026
+ const r2 = await bulkToggleSkills(names, false);
1027
+ if (r2.locked?.length) {
1028
+ message.warning(`已停用 ${names.length - r2.locked.length} 个技能,${r2.locked.length} 个被占用`);
1029
+ } else {
1030
+ message.success(`${names.length} 个技能已停用`);
1031
+ }
1022
1032
  await loadSkills();
1023
1033
  } catch {
1024
1034
  message.error("操作失败");
@@ -1035,8 +1045,12 @@ async function onBatchMenuSelect(key, group) {
1035
1045
  negativeText: "取消",
1036
1046
  onPositiveClick: async () => {
1037
1047
  try {
1038
- await bulkDeleteSkills(names);
1039
- message.success(`${names.length} 个技能已删除`);
1048
+ const r3 = await bulkDeleteSkills(names);
1049
+ if (r3.locked?.length) {
1050
+ message.warning(`已删除 ${r3.deleted?.length || 0} 个技能,${r3.locked.length} 个被占用`);
1051
+ } else {
1052
+ message.success(`${names.length} 个技能已删除`);
1053
+ }
1040
1054
  await loadSkills();
1041
1055
  } catch {
1042
1056
  message.error("删除失败");