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.
@@ -70,6 +70,25 @@
70
70
  <n-tag v-if="profile.id === activeId" type="success" size="small">
71
71
  当前场景
72
72
  </n-tag>
73
+ <n-popover
74
+ v-if="failedProfiles[profile.id]"
75
+ trigger="hover"
76
+ placement="top"
77
+ >
78
+ <template #trigger>
79
+ <n-tag type="warning" size="small" style="cursor:pointer">
80
+ ⚠ 未就绪
81
+ </n-tag>
82
+ </template>
83
+ <div style="font-size:13px">
84
+ <div v-if="failedProfiles[profile.id]?.failedDelete?.length">
85
+ 删除失败:{{ failedProfiles[profile.id].failedDelete.length }} 个
86
+ </div>
87
+ <div v-if="failedProfiles[profile.id]?.failedInstall?.length">
88
+ 安装失败:{{ failedProfiles[profile.id].failedInstall.length }} 个
89
+ </div>
90
+ </div>
91
+ </n-popover>
73
92
  <n-popover
74
93
  v-if="profile.skills !== null"
75
94
  trigger="hover"
@@ -121,12 +140,13 @@
121
140
  <!-- 切换确认弹窗 -->
122
141
  <ModalComp
123
142
  :show="showApplyConfirm"
124
- title="确认切换场景"
143
+ :title="applyState === 'executing' ? '正在切换场景' : applyState === 'failed' ? '切换未完成' : '确认切换场景'"
125
144
  width="540px"
126
145
  :mask-closable="false"
127
- @update:show="(v) => { if (!v) showApplyConfirm = false }"
146
+ @update:show="(v) => { if (!v && applyState !== 'executing') showApplyConfirm = false }"
128
147
  >
129
- <div v-if="targetProfile" class="diff-preview">
148
+ <!-- 状态:diff 预览 -->
149
+ <div v-if="applyState === 'preview' && targetProfile" class="diff-preview">
130
150
  <p class="diff-hint">
131
151
  从 <strong>{{ currentProfileName }}</strong> 切换到
132
152
  <strong>{{ targetProfile.name }}</strong>
@@ -182,11 +202,71 @@
182
202
  </div>
183
203
  </div>
184
204
  </div>
205
+
206
+ <!-- 状态:执行中 -->
207
+ <div v-if="applyState === 'executing'" class="executing-state">
208
+ <div class="executing-spinner">
209
+ <n-spin size="large" />
210
+ </div>
211
+ <p class="executing-text">正在切换到「{{ targetProfile?.name }}」...</p>
212
+ <p class="executing-hint">处理中,请稍候</p>
213
+ </div>
214
+
215
+ <!-- 状态:失败详情 -->
216
+ <div v-if="applyState === 'failed'" class="diff-preview">
217
+ <p class="diff-hint">
218
+ 切换到 <strong>{{ targetProfile?.name }}</strong> 时部分技能未能处理
219
+ </p>
220
+ <div v-if="failedDelete.length > 0" class="failure-section">
221
+ <div class="failure-title removal-failure">
222
+ <n-icon size="16" color="#d03050"><TrashOutline /></n-icon>
223
+ <span>删除失败(被程序占用)</span>
224
+ </div>
225
+ <div class="failure-list">
226
+ <div v-for="name in failedDelete" :key="'del-'+name" class="failure-item">
227
+ <span class="failure-name">{{ name }}</span>
228
+ <n-button size="tiny" secondary type="warning" @click="retrySingle('delete', name)">
229
+ 重试
230
+ </n-button>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ <div v-if="failedInstall.length > 0" class="failure-section">
235
+ <div class="failure-title install-failure">
236
+ <n-icon size="16" color="#d03050"><AddOutline /></n-icon>
237
+ <span>安装失败</span>
238
+ </div>
239
+ <div class="failure-list">
240
+ <div v-for="name in failedInstall" :key="'inst-'+name" class="failure-item">
241
+ <span class="failure-name">{{ name }}</span>
242
+ <n-button size="tiny" secondary type="warning" @click="retrySingle('install', name)">
243
+ 重试
244
+ </n-button>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ <p class="failure-tip">请关闭相关程序后重试,或打开技能目录手动处理</p>
249
+ </div>
250
+
185
251
  <template #footer>
186
- <n-button @click="showApplyConfirm = false">取消</n-button>
187
- <n-button type="primary" :loading="confirmLoading" @click="onConfirmApply">
188
- 确认切换
189
- </n-button>
252
+ <template v-if="applyState === 'preview'">
253
+ <n-button @click="showApplyConfirm = false">取消</n-button>
254
+ <n-button type="primary" :loading="confirmLoading" @click="onConfirmApply">
255
+ 确认切换
256
+ </n-button>
257
+ </template>
258
+ <template v-if="applyState === 'failed'">
259
+ <n-button size="small" @click="openSkillsDestFolder">📂 打开技能目录</n-button>
260
+ <n-button size="small" @click="onAbandon">放弃</n-button>
261
+ <n-button
262
+ size="small"
263
+ type="primary"
264
+ :loading="confirmLoading"
265
+ @click="retryAll"
266
+ >
267
+ 重试所有失败项
268
+ </n-button>
269
+ </template>
190
270
  </template>
191
271
  </ModalComp>
192
272
 
@@ -382,6 +462,9 @@ import {
382
462
  deleteProfile,
383
463
  installSkills,
384
464
  getSkillsReadme,
465
+ retryDelete,
466
+ retryInstall,
467
+ openSkillsDest,
385
468
  } from "../api/skills";
386
469
  import { recordUsage } from "../utils/usage";
387
470
 
@@ -428,6 +511,12 @@ const showApplyConfirm = ref(false);
428
511
  const confirmLoading = ref(false);
429
512
  const targetProfile = ref(null);
430
513
  const diffResult = ref({ added: [], removed: [], kept: [] });
514
+ // 弹窗状态:preview → executing → failed
515
+ const applyState = ref("preview");
516
+ const failedDelete = ref([]);
517
+ const failedInstall = ref([]);
518
+ // 卡片角标:记录每个场景的失败历史
519
+ const failedProfiles = ref({});
431
520
  const currentProfileName = computed(() => {
432
521
  if (!activeId.value) return "无";
433
522
  const p = profiles.value.find((p) => p.id === activeId.value);
@@ -603,7 +692,11 @@ async function onApplyCustom() {
603
692
  try {
604
693
  const res = await installSkills(customSelected.value);
605
694
  if (res.ok) {
606
- message.success(`已安装 ${customSelected.value.length} 个技能`);
695
+ if (res.locked?.length) {
696
+ message.warning(`已安装 ${customSelected.value.length - res.locked.length} 个技能,${res.locked.length} 个被占用`);
697
+ } else {
698
+ message.success(`已安装 ${customSelected.value.length} 个技能`);
699
+ }
607
700
  activeId.value = "custom";
608
701
  showCustom.value = false;
609
702
  customSelected.value.forEach(recordUsage);
@@ -677,32 +770,130 @@ function calcDiff(profile) {
677
770
  function onApply(profile) {
678
771
  targetProfile.value = profile;
679
772
  diffResult.value = calcDiff(profile);
773
+ failedDelete.value = [];
774
+ failedInstall.value = [];
775
+ applyState.value = "preview";
680
776
  showApplyConfirm.value = true;
681
777
  }
682
778
 
683
779
  async function onConfirmApply() {
684
780
  if (!targetProfile.value) return;
781
+ applyState.value = "executing";
685
782
  confirmLoading.value = true;
686
783
  applying.value = targetProfile.value.id;
687
784
  try {
688
785
  const res = await applyProfile(targetProfile.value.id);
689
786
  if (res.ok) {
690
787
  activeId.value = targetProfile.value.id;
691
- message.success(`已切换到「${targetProfile.value.name}」`);
788
+ failedDelete.value = res.failedDelete || [];
789
+ failedInstall.value = res.failedInstall || [];
790
+ const hasFailed = failedDelete.value.length > 0 || failedInstall.value.length > 0;
791
+
792
+ if (hasFailed) {
793
+ applyState.value = "failed";
794
+ // 记录失败信息到卡片角标
795
+ failedProfiles.value = {
796
+ ...failedProfiles.value,
797
+ [targetProfile.value.id]: {
798
+ failedDelete: [...failedDelete.value],
799
+ failedInstall: [...failedInstall.value],
800
+ },
801
+ };
802
+ } else {
803
+ // 全部成功,清理该场景的失败记录
804
+ const newFailed = { ...failedProfiles.value };
805
+ delete newFailed[targetProfile.value.id];
806
+ failedProfiles.value = newFailed;
807
+ message.success(`已切换到「${targetProfile.value.name}」`);
808
+ showApplyConfirm.value = false;
809
+ }
692
810
  (targetProfile.value.skills || []).forEach(recordUsage);
693
- showApplyConfirm.value = false;
694
811
  emit("refresh");
695
- // 刷新技能状态,确保下次差异计算基于最新数据
696
812
  allSkills.value = await getSkills();
697
813
  }
698
814
  } catch {
699
815
  message.error("切换失败");
816
+ applyState.value = "preview";
700
817
  } finally {
701
818
  confirmLoading.value = false;
702
819
  applying.value = null;
703
820
  }
704
821
  }
705
822
 
823
+ // 单技能重试
824
+ async function retrySingle(type, name) {
825
+ const fn = type === "delete" ? retryDelete : retryInstall;
826
+ const label = type === "delete" ? "删除" : "安装";
827
+ try {
828
+ const res = await fn(name);
829
+ if (res.ok) {
830
+ message.success(`技能 "${name}" ${label}成功`);
831
+ if (type === "delete") {
832
+ failedDelete.value = failedDelete.value.filter((n) => n !== name);
833
+ } else {
834
+ failedInstall.value = failedInstall.value.filter((n) => n !== name);
835
+ }
836
+ // 更新角标
837
+ const pid = targetProfile.value?.id;
838
+ if (pid) {
839
+ const updated = { ...failedProfiles.value };
840
+ if (updated[pid]) {
841
+ updated[pid] = {
842
+ failedDelete: [...failedDelete.value],
843
+ failedInstall: [...failedInstall.value],
844
+ };
845
+ if (updated[pid].failedDelete.length === 0 && updated[pid].failedInstall.length === 0) {
846
+ delete updated[pid];
847
+ }
848
+ failedProfiles.value = updated;
849
+ }
850
+ }
851
+ // 如果所有失败都解决了,自动关闭弹窗
852
+ if (failedDelete.value.length === 0 && failedInstall.value.length === 0) {
853
+ message.success(`已切换到「${targetProfile.value?.name}」`);
854
+ showApplyConfirm.value = false;
855
+ }
856
+ } else if (res.locked) {
857
+ message.warning(`技能 "${name}" 仍被占用,请关闭相关程序后重试`);
858
+ } else {
859
+ message.error(res.error || `${label}失败`);
860
+ }
861
+ } catch {
862
+ message.error(`${label}失败`);
863
+ }
864
+ }
865
+
866
+ // 重试所有失败项
867
+ async function retryAll() {
868
+ // 先重试所有安装失败,再重试所有删除失败
869
+ for (const name of [...failedInstall.value]) {
870
+ await retrySingle("install", name);
871
+ if (showApplyConfirm.value === false) return; // 弹窗已关闭
872
+ }
873
+ for (const name of [...failedDelete.value]) {
874
+ await retrySingle("delete", name);
875
+ if (showApplyConfirm.value === false) return;
876
+ }
877
+ }
878
+
879
+ // 放弃切换
880
+ function onAbandon() {
881
+ showApplyConfirm.value = false;
882
+ const pid = targetProfile.value?.id;
883
+ if (pid && (failedDelete.value.length > 0 || failedInstall.value.length > 0)) {
884
+ message.warning(
885
+ `部分技能未就绪:` +
886
+ (failedDelete.value.length ? `删除失败 ${failedDelete.value.length} 个、` : "") +
887
+ (failedInstall.value.length ? `安装失败 ${failedInstall.value.length} 个` : "").replace(/、$/, "")
888
+ );
889
+ }
890
+ }
891
+
892
+ // 打开项目技能目录
893
+ function openSkillsDestFolder() {
894
+ openSkillsDest().catch(() => message.error("打开目录失败"));
895
+ }
896
+
706
897
  onMounted(() => {
707
898
  loadData();
708
899
  loadSkillsReadme();
@@ -877,6 +1068,62 @@ onMounted(() => {
877
1068
  .search-input {
878
1069
  margin-bottom: 8px;
879
1070
  }
1071
+
1072
+ /* 执行中状态 */
1073
+ .executing-state {
1074
+ display: flex;
1075
+ flex-direction: column;
1076
+ align-items: center;
1077
+ padding: 32px 0;
1078
+ gap: 12px;
1079
+ }
1080
+ .executing-text {
1081
+ margin: 0;
1082
+ font-size: 15px;
1083
+ font-weight: 600;
1084
+ }
1085
+ .executing-hint {
1086
+ margin: 0;
1087
+ font-size: 13px;
1088
+ color: #999;
1089
+ }
1090
+
1091
+ /* 失败区域 */
1092
+ .failure-section {
1093
+ display: flex;
1094
+ flex-direction: column;
1095
+ gap: 6px;
1096
+ margin-top: 4px;
1097
+ }
1098
+ .failure-title {
1099
+ display: flex;
1100
+ align-items: center;
1101
+ gap: 4px;
1102
+ font-size: 13px;
1103
+ font-weight: 600;
1104
+ }
1105
+ .failure-list {
1106
+ display: flex;
1107
+ flex-direction: column;
1108
+ gap: 4px;
1109
+ }
1110
+ .failure-item {
1111
+ display: flex;
1112
+ align-items: center;
1113
+ justify-content: space-between;
1114
+ padding: 4px 8px;
1115
+ background: #fff1f0;
1116
+ border-radius: 4px;
1117
+ font-size: 13px;
1118
+ }
1119
+ .failure-name {
1120
+ font-weight: 500;
1121
+ }
1122
+ .failure-tip {
1123
+ margin: 4px 0 0;
1124
+ font-size: 12px;
1125
+ color: #999;
1126
+ }
880
1127
  </style>
881
1128
 
882
1129
  <style>
@@ -910,4 +1157,13 @@ onMounted(() => {
910
1157
  background: #8a6d00;
911
1158
  color: #fff;
912
1159
  }
1160
+ [data-theme="dark"] .failure-item {
1161
+ background: rgba(208, 48, 80, 0.15);
1162
+ }
1163
+ [data-theme="dark"] .executing-hint {
1164
+ color: #6c7086;
1165
+ }
1166
+ [data-theme="dark"] .failure-tip {
1167
+ color: #6c7086;
1168
+ }
913
1169
  </style>