mdk-skills 2.4.0 → 2.4.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.
@@ -1,1361 +0,0 @@
1
- <template>
2
- <div class="scene-switch">
3
- <!-- 技能说明文档 -->
4
- <div class="readme-fold" v-if="skillsReadmeContent">
5
- <div class="fold-header" @click="skillsReadmeOpen = !skillsReadmeOpen">
6
- <span class="fold-arrow" :class="{ open: skillsReadmeOpen }">▶</span>
7
- 技能说明
8
- </div>
9
- <div class="fold-body" v-show="skillsReadmeOpen">
10
- <div class="markdown-content" v-html="renderedSkillsReadme" />
11
- </div>
12
- </div>
13
-
14
- <div class="page-header">
15
- <h2>场景切换</h2>
16
- <n-button
17
- size="small"
18
- type="primary"
19
- @click="openCreate"
20
- >
21
- <template #icon
22
- ><n-icon><AddOutline /></n-icon
23
- ></template>
24
- 新建场景
25
- </n-button>
26
- </div>
27
-
28
- <n-spin :show="loading">
29
- <div class="scene-grid">
30
- <n-card
31
- v-for="profile in filteredProfiles"
32
- :key="profile.id"
33
- class="scene-card"
34
- :class="{ active: profile.id === activeId }"
35
- :title="profile.name"
36
- size="small"
37
- >
38
- <template #header-extra>
39
- <n-space
40
- v-if="profile.id !== 'custom' && profile.skills !== null"
41
- :size="4"
42
- >
43
- <n-button size="tiny" quaternary @click="openEdit(profile)">
44
- <template #icon
45
- ><n-icon size="16"><PencilOutline /></n-icon
46
- ></template>
47
- </n-button>
48
- <n-popconfirm
49
- :negative-text="'取消'"
50
- :positive-text="'确认删除'"
51
- @positive-click="onDelete(profile)"
52
- >
53
- <template #trigger>
54
- <n-button size="tiny" quaternary>
55
- <template #icon
56
- ><n-icon size="16"><TrashOutline /></n-icon
57
- ></template>
58
- </n-button>
59
- </template>
60
- 确定删除场景「{{ profile.name }}」?
61
- </n-popconfirm>
62
- </n-space>
63
- </template>
64
-
65
- <p class="scene-desc">{{ profile.description }}</p>
66
-
67
- <template #footer>
68
- <div class="scene-footer">
69
- <n-space :size="6">
70
- <n-tag v-if="profile.id === activeId" type="success" size="small">
71
- 当前场景
72
- </n-tag>
73
- <n-popover
74
- v-if="profile.id === activeId && sceneFailed"
75
- trigger="hover"
76
- placement="top"
77
- width="280"
78
- >
79
- <template #trigger>
80
- <n-tag type="warning" size="small" style="cursor:pointer">
81
- ⚠ 未就绪
82
- </n-tag>
83
- </template>
84
- <div class="popover-fail-detail">
85
- <div v-if="sceneFailed.failedDelete?.length" class="popover-fail-section">
86
- <div class="popover-fail-title">🔒 删除失败(被占用)</div>
87
- <div v-for="name in sceneFailed.failedDelete" :key="'fd-'+name" class="popover-fail-item">
88
- {{ name }}
89
- </div>
90
- </div>
91
- <div v-if="sceneFailed.failedInstall?.length" class="popover-fail-section">
92
- <div class="popover-fail-title">⚠️ 安装失败</div>
93
- <div v-for="name in sceneFailed.failedInstall" :key="'fi-'+name" class="popover-fail-item">
94
- {{ name }}
95
- </div>
96
- </div>
97
- <div style="margin-top:8px;border-top:1px solid #eee;padding-top:6px;text-align:center">
98
- <n-button size="tiny" type="warning" secondary @click="reopenFailedModal">
99
- 🔄 重新尝试处理
100
- </n-button>
101
- </div>
102
- </div>
103
- </n-popover>
104
- <n-popover
105
- v-if="profile.skills !== null"
106
- trigger="hover"
107
- placement="top"
108
- width="240"
109
- >
110
- <template #trigger>
111
- <n-tag size="small" style="cursor:pointer">
112
- {{ profile.skills.length }} 个技能
113
- </n-tag>
114
- </template>
115
- <div class="skill-preview-list">
116
- <div
117
- v-for="skillName in profile.skills"
118
- :key="skillName"
119
- class="skill-preview-item"
120
- >
121
- {{ skillName }}
122
- </div>
123
- </div>
124
- </n-popover>
125
- <n-tag v-else-if="profile.skills === null" size="small">
126
- 自定义
127
- </n-tag>
128
- </n-space>
129
-
130
- <n-button
131
- v-if="profile.id !== activeId && profile.skills !== null"
132
- size="small"
133
- type="primary"
134
- :loading="applying === profile.id"
135
- @click="onApply(profile)"
136
- >
137
- 应用
138
- </n-button>
139
- <n-button
140
- v-if="profile.id !== activeId && profile.skills === null"
141
- size="small"
142
- @click="openCustomDialog"
143
- >
144
- 自定义勾选
145
- </n-button>
146
- </div>
147
- </template>
148
- </n-card>
149
- </div>
150
- </n-spin>
151
-
152
- <!-- 切换确认弹窗 -->
153
- <ModalComp
154
- :show="showApplyConfirm"
155
- :title="applyState === 'executing' ? '正在切换场景' : applyState === 'failed' ? '切换未完成' : '确认切换场景'"
156
- width="540px"
157
- :mask-closable="false"
158
- @update:show="(v) => { if (!v && applyState !== 'executing') showApplyConfirm = false }"
159
- >
160
- <!-- 状态:diff 预览 -->
161
- <div v-if="applyState === 'preview' && targetProfile" class="diff-preview">
162
- <p class="diff-hint">
163
- 从 <strong>{{ currentProfileName }}</strong> 切换到
164
- <strong>{{ targetProfile.name }}</strong>
165
- </p>
166
- <div v-if="diffResult.added.length > 0" class="diff-section">
167
- <div class="diff-label added-label">
168
- <n-icon size="16" color="#18a058"><AddOutline /></n-icon>
169
- <span>新增({{ diffResult.added.length }})</span>
170
- </div>
171
- <div class="diff-tags">
172
- <n-tag
173
- v-for="name in diffResult.added"
174
- :key="name"
175
- size="tiny"
176
- type="success"
177
- :bordered="false"
178
- >
179
- {{ name }}
180
- </n-tag>
181
- </div>
182
- </div>
183
- <div v-if="diffResult.removed.length > 0" class="diff-section">
184
- <div class="diff-label removed-label">
185
- <n-icon size="16" color="#d03050"><TrashOutline /></n-icon>
186
- <span>移除({{ diffResult.removed.length }})</span>
187
- </div>
188
- <div class="diff-tags">
189
- <n-tag
190
- v-for="name in diffResult.removed"
191
- :key="name"
192
- size="tiny"
193
- type="error"
194
- :bordered="false"
195
- >
196
- {{ name }}
197
- </n-tag>
198
- </div>
199
- </div>
200
- <div v-if="diffResult.kept.length > 0" class="diff-section">
201
- <div class="diff-label kept-label">
202
- <n-icon size="16" color="#999"><CheckmarkOutline /></n-icon>
203
- <span>不变({{ diffResult.kept.length }})</span>
204
- </div>
205
- <div class="diff-tags">
206
- <n-tag
207
- v-for="name in diffResult.kept"
208
- :key="name"
209
- size="tiny"
210
- :bordered="false"
211
- >
212
- {{ name }}
213
- </n-tag>
214
- </div>
215
- </div>
216
- </div>
217
-
218
- <!-- 状态:执行中 -->
219
- <div v-if="applyState === 'executing'" class="executing-state">
220
- <div class="executing-spinner">
221
- <n-spin size="large" />
222
- </div>
223
- <p class="executing-text">正在切换到「{{ targetProfile?.name }}」...</p>
224
- <p class="executing-hint">处理中,请稍候</p>
225
- </div>
226
-
227
- <!-- 状态:失败详情 -->
228
- <div v-if="applyState === 'failed'" class="diff-preview">
229
- <p class="diff-hint">
230
- 切换到 <strong>{{ targetProfile?.name }}</strong> 时部分技能未能处理
231
- </p>
232
- <div v-if="diffResult.added.length > 0" class="diff-section">
233
- <div class="diff-label added-label">
234
- <n-icon size="16" color="#18a058"><AddOutline /></n-icon>
235
- <span>新增({{ diffResult.added.length }})</span>
236
- </div>
237
- <div class="diff-tags">
238
- <n-tag
239
- v-for="name in diffResult.added"
240
- :key="name"
241
- size="tiny"
242
- type="success"
243
- :bordered="false"
244
- >
245
- {{ name }}
246
- </n-tag>
247
- </div>
248
- </div>
249
- <div v-if="diffResult.removed.length > 0" class="diff-section">
250
- <div class="diff-label removed-label">
251
- <n-icon size="16" color="#d03050"><TrashOutline /></n-icon>
252
- <span>移除({{ diffResult.removed.length }})</span>
253
- </div>
254
- <div class="diff-tags">
255
- <n-tag
256
- v-for="name in diffResult.removed"
257
- :key="name"
258
- size="tiny"
259
- type="error"
260
- :bordered="false"
261
- >
262
- {{ name }}<span v-if="failedDelete.includes(name)" class="fail-marker" title="删除失败(被占用)">🔒</span>
263
- </n-tag>
264
- </div>
265
- </div>
266
- <div v-if="diffResult.kept.length > 0" class="diff-section">
267
- <div class="diff-label kept-label">
268
- <n-icon size="16" color="#999"><CheckmarkOutline /></n-icon>
269
- <span>不变({{ diffResult.kept.length }})</span>
270
- </div>
271
- <div class="diff-tags">
272
- <n-tag
273
- v-for="name in diffResult.kept"
274
- :key="name"
275
- size="tiny"
276
- :bordered="false"
277
- >
278
- {{ name }}
279
- </n-tag>
280
- </div>
281
- </div>
282
- <div v-if="failedDelete.length > 0" class="failure-section">
283
- <div class="failure-title removal-failure">
284
- <n-icon size="16" color="#d03050"><TrashOutline /></n-icon>
285
- <span>删除失败(被程序占用)</span>
286
- </div>
287
- <div class="failure-list">
288
- <div v-for="name in failedDelete" :key="'del-'+name" class="failure-item">
289
- <span class="failure-name">🔒 {{ name }}</span>
290
- <n-button size="tiny" secondary type="warning" @click="retrySingle('delete', name)">
291
- 重试
292
- </n-button>
293
- </div>
294
- </div>
295
- </div>
296
- <div v-if="failedInstall.length > 0" class="failure-section">
297
- <div class="failure-title install-failure">
298
- <n-icon size="16" color="#d03050"><AddOutline /></n-icon>
299
- <span>安装失败</span>
300
- </div>
301
- <div class="failure-list">
302
- <div v-for="name in failedInstall" :key="'inst-'+name" class="failure-item">
303
- <span class="failure-name">⚠️ {{ name }}</span>
304
- <n-button size="tiny" secondary type="warning" @click="retrySingle('install', name)">
305
- 重试
306
- </n-button>
307
- </div>
308
- </div>
309
- </div>
310
- <p class="failure-tip">请关闭相关程序后重试,或打开技能目录手动处理</p>
311
- </div>
312
-
313
- <template #footer>
314
- <template v-if="applyState === 'preview'">
315
- <n-button @click="showApplyConfirm = false">取消</n-button>
316
- <n-button type="primary" :loading="confirmLoading" @click="onConfirmApply">
317
- 确认切换
318
- </n-button>
319
- </template>
320
- <template v-if="applyState === 'failed'">
321
- <n-button size="small" @click="openSkillsDestFolder">📂 打开技能目录</n-button>
322
- <n-button size="small" @click="onAbandon">放弃</n-button>
323
- <n-button
324
- size="small"
325
- type="primary"
326
- :loading="confirmLoading"
327
- @click="retryAll"
328
- >
329
- 重试所有失败项
330
- </n-button>
331
- </template>
332
- </template>
333
- </ModalComp>
334
-
335
- <!-- 未就绪时切换确认弹窗 -->
336
- <ModalComp
337
- :show="showPendingConfirm"
338
- title="当前场景未就绪"
339
- width="420px"
340
- @update:show="(v) => { if (!v) showPendingConfirm = false }"
341
- >
342
- <div class="pending-confirm-body">
343
- <p v-if="sceneFailed">
344
- 当前场景 <strong>{{ currentProfileName }}</strong> 还有未就绪的技能
345
- </p>
346
- <div v-if="sceneFailed?.failedDelete?.length" class="pending-confirm-section">
347
- <div class="pending-confirm-title">🔒 删除失败(被占用)</div>
348
- <div v-for="name in sceneFailed.failedDelete" :key="'pd-'+name" class="pending-confirm-item">{{ name }}</div>
349
- </div>
350
- <div v-if="sceneFailed?.failedInstall?.length" class="pending-confirm-section">
351
- <div class="pending-confirm-title">⚠️ 安装失败</div>
352
- <div v-for="name in sceneFailed.failedInstall" :key="'pi-'+name" class="pending-confirm-item">{{ name }}</div>
353
- </div>
354
- <p class="pending-confirm-hint">确定要切换到其他场景吗?</p>
355
- </div>
356
- <template #footer>
357
- <n-button size="small" @click="showPendingConfirm = false">取消</n-button>
358
- <n-button size="small" @click="onIgnoreAndSwitch">忽略并切换</n-button>
359
- <n-button size="small" type="primary" @click="handlePendingRetry">处理未就绪</n-button>
360
- </template>
361
- </ModalComp>
362
-
363
- <!-- 自定义勾选弹窗 -->
364
- <ModalComp
365
- :show="showCustom"
366
- title="自定义技能组合"
367
- width="560px"
368
- @update:show="
369
- (v) => {
370
- if (!v) showCustom = false;
371
- }
372
- "
373
- >
374
- <n-input
375
- v-model:value="customSearch"
376
- placeholder="搜索技能..."
377
- size="small"
378
- clearable
379
- class="search-input"
380
- />
381
- <n-space class="batch-actions" :size="6">
382
- <n-button size="tiny" @click="customSelected = selectAllVisible(customSearch, customSelected)">
383
- 全选
384
- </n-button>
385
- <n-button size="tiny" @click="customSelected = invertVisible(customSearch, customSelected)">
386
- 反选
387
- </n-button>
388
- <n-button size="tiny" @click="customSelected = clearAll()">
389
- 清空
390
- </n-button>
391
- </n-space>
392
- <n-checkbox-group v-model:value="customSelected">
393
- <div class="modal-skill-groups">
394
- <div
395
- v-for="group in groupedSkills"
396
- :key="group.key"
397
- class="skill-group"
398
- >
399
- <div class="skill-group-header" @click="toggleGroup(group.key)">
400
- <span class="fold-arrow" :class="{ open: groupOpen[group.key] }"
401
- >▶</span
402
- >
403
- <span class="skill-group-label">{{
404
- group.type === "remote" ? group.url : "本地技能"
405
- }}</span>
406
- <span class="skill-group-count">{{ filteredGroupSkills(group, customSearch).length }}</span>
407
- </div>
408
- <div v-show="groupOpen[group.key]" class="skill-group-body">
409
- <n-checkbox
410
- v-for="skill in filteredGroupSkills(group, customSearch)"
411
- :key="skill.name"
412
- :value="skill.name"
413
- >
414
- <span v-html="highlightText(skill.name, customSearch)" />
415
- </n-checkbox>
416
- </div>
417
- </div>
418
- </div>
419
- </n-checkbox-group>
420
- <template #footer>
421
- <n-button
422
- type="primary"
423
- :loading="customLoading"
424
- @click="onApplyCustom"
425
- >
426
- 确认安装
427
- </n-button>
428
- </template>
429
- </ModalComp>
430
-
431
- <!-- 编辑弹窗 -->
432
- <ModalComp
433
- :show="showEditor"
434
- :title="editingProfile ? '编辑场景' : '新建场景'"
435
- width="560px"
436
- :mask-closable="false"
437
- @update:show="
438
- (v) => {
439
- if (!v) showEditor = false;
440
- }
441
- "
442
- >
443
- <n-form :model="formData" label-placement="top">
444
- <n-form-item label="场景名称" required>
445
- <n-input
446
- v-model:value="formData.name"
447
- placeholder="给场景起个名字"
448
- :maxlength="30"
449
- />
450
- </n-form-item>
451
- <n-form-item label="场景描述">
452
- <n-input
453
- v-model:value="formData.description"
454
- type="textarea"
455
- :rows="2"
456
- placeholder="简短描述这个场景的用途"
457
- />
458
- </n-form-item>
459
- <n-divider />
460
- <n-form-item label="包含的技能">
461
- <n-input
462
- v-model:value="formSearch"
463
- placeholder="搜索技能..."
464
- size="small"
465
- clearable
466
- class="search-input"
467
- />
468
- <n-space class="batch-actions" :size="6">
469
- <n-button size="tiny" @click="formData.skills = selectAllVisible(formSearch, formData.skills)">
470
- 全选
471
- </n-button>
472
- <n-button size="tiny" @click="formData.skills = invertVisible(formSearch, formData.skills)">
473
- 反选
474
- </n-button>
475
- <n-button size="tiny" @click="formData.skills = clearAll()">
476
- 清空
477
- </n-button>
478
- </n-space>
479
- <n-checkbox-group v-model:value="formData.skills">
480
- <div class="modal-skill-groups">
481
- <div
482
- v-for="group in groupedSkills"
483
- :key="group.key"
484
- class="skill-group"
485
- >
486
- <div class="skill-group-header" @click="toggleGroup(group.key)">
487
- <span
488
- class="fold-arrow"
489
- :class="{ open: groupOpen[group.key] }"
490
- >▶</span
491
- >
492
- <span class="skill-group-label">{{
493
- group.type === "remote" ? group.url : "本地技能"
494
- }}</span>
495
- <span class="skill-group-count">{{
496
- filteredGroupSkills(group, formSearch).length
497
- }}</span>
498
- </div>
499
- <div v-show="groupOpen[group.key]" class="skill-group-body">
500
- <n-checkbox
501
- v-for="skill in filteredGroupSkills(group, formSearch)"
502
- :key="skill.name"
503
- :value="skill.name"
504
- >
505
- <span v-html="highlightText(skill.name, formSearch)" />
506
- </n-checkbox>
507
- </div>
508
- </div>
509
- </div>
510
- </n-checkbox-group>
511
- </n-form-item>
512
- <n-divider />
513
- <n-form-item label="始终加载(always_apply)">
514
- <template v-if="formData.skills.length === 0">
515
- <span class="hint-text">请先勾选包含的技能</span>
516
- </template>
517
- <n-checkbox-group v-else v-model:value="formData.always_apply">
518
- <div class="always-apply-grid">
519
- <n-checkbox
520
- v-for="skillName in formData.skills"
521
- :key="skillName"
522
- :value="skillName"
523
- :label="skillName"
524
- />
525
- </div>
526
- </n-checkbox-group>
527
- </n-form-item>
528
- </n-form>
529
- <template #footer>
530
- <n-button @click="showEditor = false">取消</n-button>
531
- <n-button type="primary" :loading="saving" @click="onSave">
532
- 保存
533
- </n-button>
534
- </template>
535
- </ModalComp>
536
- </div>
537
- </template>
538
-
539
- <script setup>
540
- import { ref, onMounted, computed } from "vue";
541
- import { useMessage } from "naive-ui";
542
- import { NIcon } from "naive-ui";
543
- import { AddOutline, PencilOutline, TrashOutline, CheckmarkOutline } from "@vicons/ionicons5";
544
- import { marked } from "marked";
545
- import hljs from "highlight.js";
546
- import ModalComp from "../components/ModalComp.vue";
547
- import {
548
- getProfiles,
549
- applyProfile,
550
- getSkills,
551
- saveProfile,
552
- deleteProfile,
553
- installSkills,
554
- getSkillsReadme,
555
- retryDelete,
556
- retryInstall,
557
- openSkillsDest,
558
- } from "../api/skills";
559
- import { recordUsage } from "../utils/usage";
560
-
561
- // marked 配置:代码高亮 + 外链安全
562
- marked.use({
563
- renderer: {
564
- code({ text, lang }) {
565
- const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
566
- let highlighted;
567
- try {
568
- highlighted = hljs.highlight(text, { language }).value;
569
- } catch {
570
- highlighted = hljs.highlightAuto(text).value;
571
- }
572
- return `<pre><button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentNode.querySelector('code').textContent);this.textContent='已复制';setTimeout(()=>this.textContent='复制',1500)">复制</button><code class="hljs language-${language}">${highlighted}</code></pre>`;
573
- },
574
- link({ href, text }) {
575
- const isExternal =
576
- href && (href.startsWith("http://") || href.startsWith("https://"));
577
- const target = isExternal
578
- ? ' target="_blank" rel="noopener noreferrer"'
579
- : "";
580
- return `<a href="${href}"${target}>${text}</a>`;
581
- },
582
- },
583
- });
584
-
585
- const emit = defineEmits(["refresh"]);
586
- const message = useMessage();
587
-
588
- // 技能说明 README
589
- const skillsReadmeContent = ref(null);
590
- const skillsReadmeOpen = ref(true);
591
- const renderedSkillsReadme = ref("");
592
-
593
- const profiles = ref([]);
594
- const activeId = ref(null);
595
- const allSkills = ref([]);
596
- const loading = ref(false);
597
- const applying = ref(null);
598
-
599
- // 切换确认弹窗
600
- const showApplyConfirm = ref(false);
601
- const confirmLoading = ref(false);
602
- const targetProfile = ref(null);
603
- const diffResult = ref({ added: [], removed: [], kept: [] });
604
- // 弹窗状态:preview → executing → failed
605
- const applyState = ref("preview");
606
- const failedDelete = ref([]);
607
- const failedInstall = ref([]);
608
- // 卡片角标:只跟踪当前激活场景的失败状态
609
- const sceneFailed = ref(null);
610
- // 未就绪时切换场景的确认弹窗
611
- const showPendingConfirm = ref(false);
612
- const pendingApplyProfile = ref(null);
613
- const currentProfileName = computed(() => {
614
- if (!activeId.value) return "无";
615
- const p = profiles.value.find((p) => p.id === activeId.value);
616
- return p ? p.name : "无";
617
- });
618
-
619
- // 自定义勾选
620
- const showCustom = ref(false);
621
- const customSelected = ref([]);
622
- const customLoading = ref(false);
623
- const customSearch = ref("");
624
-
625
- // 编辑器
626
- const showEditor = ref(false);
627
- const editingProfile = ref(null);
628
- const saving = ref(false);
629
- const formSearch = ref("");
630
- const formData = ref({
631
- name: "",
632
- description: "",
633
- skills: [],
634
- always_apply: [],
635
- });
636
-
637
- // 技能分组折叠
638
- const groupOpen = ref({});
639
- function toggleGroup(key) {
640
- groupOpen.value[key] = !groupOpen.value[key];
641
- }
642
-
643
- // 过滤掉不可见的场景(如已有内置)
644
- const filteredProfiles = computed(() => profiles.value);
645
-
646
- // 技能按源分组
647
- const groupedSkills = computed(() => {
648
- const groups = {};
649
- for (const skill of allSkills.value) {
650
- const isRemote = skill.source?.type === "remote";
651
- const key = isRemote ? skill.source.url : "__local__";
652
- if (!groups[key]) {
653
- groups[key] = {
654
- key,
655
- type: isRemote ? "remote" : "local",
656
- url: skill.source?.url || "",
657
- skills: [],
658
- };
659
- }
660
- groups[key].skills.push(skill);
661
- }
662
- for (const key of Object.keys(groups)) {
663
- if (groupOpen.value[key] === undefined) groupOpen.value[key] = true;
664
- }
665
- return Object.values(groups).sort((a, b) => {
666
- if (a.type === "remote" && b.type === "local") return -1;
667
- if (a.type === "local" && b.type === "remote") return 1;
668
- return 0;
669
- });
670
- });
671
-
672
- function filteredGroupSkills(group, searchText) {
673
- if (!searchText) return group.skills;
674
- const q = searchText.toLowerCase();
675
- return group.skills.filter((s) => s.name.toLowerCase().includes(q));
676
- }
677
-
678
- // 获取搜索过滤后的所有技能名称
679
- function getFilteredSkillNames(searchText) {
680
- const names = [];
681
- for (const group of groupedSkills.value) {
682
- names.push(...filteredGroupSkills(group, searchText).map((s) => s.name));
683
- }
684
- return names;
685
- }
686
-
687
- // 全选当前可见技能
688
- function selectAllVisible(searchText, selectedList) {
689
- const visible = getFilteredSkillNames(searchText);
690
- return [...new Set([...selectedList, ...visible])];
691
- }
692
-
693
- // 反选当前可见技能
694
- function invertVisible(searchText, selectedList) {
695
- const visible = getFilteredSkillNames(searchText);
696
- const visibleSet = new Set(visible);
697
- const kept = selectedList.filter((s) => !visibleSet.has(s));
698
- const added = visible.filter((s) => !selectedList.includes(s));
699
- return [...kept, ...added];
700
- }
701
-
702
- // 清空所有选中
703
- function clearAll() {
704
- return [];
705
- }
706
-
707
- // 搜索关键字高亮
708
- function highlightText(text, query) {
709
- if (!query) return text;
710
- const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
711
- return text.replace(new RegExp(`(${escaped})`, 'gi'), '<mark class="search-highlight">$1</mark>');
712
- }
713
-
714
- function stripFrontmatter(text) {
715
- if (!text) return text;
716
- return text.replace(/^---[\s\S]*?---\s*/, "");
717
- }
718
-
719
- function renderMd(text) {
720
- if (!text) return "";
721
- const body = stripFrontmatter(text);
722
- if (!body.trim()) return "";
723
- try {
724
- return marked.parse(body);
725
- } catch {
726
- return body;
727
- }
728
- }
729
-
730
- async function loadSkillsReadme() {
731
- try {
732
- const res = await getSkillsReadme();
733
- if (res.content) {
734
- renderedSkillsReadme.value = renderMd(res.content);
735
- skillsReadmeContent.value = res.content;
736
- }
737
- } catch {
738
- // 静默失败
739
- }
740
- }
741
-
742
- async function loadData() {
743
- loading.value = true;
744
- try {
745
- const res = await getProfiles();
746
- profiles.value = res.profiles || [];
747
- activeId.value = res.activeProfile;
748
- allSkills.value = await getSkills();
749
- } finally {
750
- loading.value = false;
751
- }
752
- }
753
-
754
- function openCreate() {
755
- editingProfile.value = null;
756
- formData.value = {
757
- name: "",
758
- description: "",
759
- skills: [],
760
- always_apply: [],
761
- };
762
- showEditor.value = true;
763
- }
764
-
765
- function openEdit(profile) {
766
- editingProfile.value = profile;
767
- formData.value = {
768
- name: profile.name,
769
- description: profile.description || "",
770
- skills: [...(profile.skills || [])],
771
- always_apply: [...(profile.always_apply || [])],
772
- };
773
- showEditor.value = true;
774
- }
775
-
776
- function openCustomDialog() {
777
- customSelected.value = allSkills.value
778
- .filter((s) => s.enabled)
779
- .map((s) => s.name);
780
- showCustom.value = true;
781
- }
782
-
783
- async function onApplyCustom() {
784
- customLoading.value = true;
785
- try {
786
- const res = await installSkills(customSelected.value);
787
- if (res.ok) {
788
- if (res.locked?.length) {
789
- message.warning(`已安装 ${customSelected.value.length - res.locked.length} 个技能,${res.locked.length} 个被占用`);
790
- } else {
791
- message.success(`已安装 ${customSelected.value.length} 个技能`);
792
- }
793
- activeId.value = "custom";
794
- showCustom.value = false;
795
- customSelected.value.forEach(recordUsage);
796
- emit("refresh");
797
- }
798
- } catch {
799
- message.error("安装失败");
800
- } finally {
801
- customLoading.value = false;
802
- }
803
- }
804
-
805
- async function onSave() {
806
- if (!formData.value.name.trim()) {
807
- message.warning("请填写场景名称");
808
- return;
809
- }
810
- saving.value = true;
811
- try {
812
- const data = {
813
- id: editingProfile.value?.id,
814
- name: formData.value.name.trim(),
815
- description: formData.value.description.trim(),
816
- skills: formData.value.skills,
817
- always_apply: formData.value.always_apply,
818
- };
819
- const res = await saveProfile(data);
820
- if (res.ok) {
821
- profiles.value = res.profiles;
822
- message.success(editingProfile.value ? "场景已更新" : "场景已创建");
823
- showEditor.value = false;
824
- emit("refresh");
825
- } else if (res.error) {
826
- message.error(res.error);
827
- }
828
- } catch {
829
- message.error("保存失败");
830
- } finally {
831
- saving.value = false;
832
- }
833
- }
834
-
835
- async function onDelete(profile) {
836
- try {
837
- const res = await deleteProfile(profile.id);
838
- if (res.ok) {
839
- profiles.value = profiles.value.filter((p) => p.id !== profile.id);
840
- message.success(`场景「${profile.name}」已删除`);
841
- emit("refresh");
842
- } else if (res.error) {
843
- message.error(res.error);
844
- }
845
- } catch {
846
- message.error("删除失败");
847
- }
848
- }
849
-
850
- function calcDiff(profile) {
851
- // 当前启用的技能
852
- const currentSkills = allSkills.value
853
- .filter((s) => s.enabled)
854
- .map((s) => s.name);
855
- const targetSkills = profile.skills || [];
856
- return {
857
- added: targetSkills.filter((s) => !currentSkills.includes(s)),
858
- removed: currentSkills.filter((s) => !targetSkills.includes(s)),
859
- kept: currentSkills.filter((s) => targetSkills.includes(s)),
860
- };
861
- }
862
-
863
- function onApply(profile) {
864
- // 当前场景有未就绪时弹出确认
865
- if (sceneFailed.value) {
866
- pendingApplyProfile.value = profile;
867
- showPendingConfirm.value = true;
868
- return;
869
- }
870
- targetProfile.value = profile;
871
- diffResult.value = calcDiff(profile);
872
- failedDelete.value = [];
873
- failedInstall.value = [];
874
- applyState.value = "preview";
875
- showApplyConfirm.value = true;
876
- }
877
-
878
- // 忽略未就绪,直接切
879
- function onIgnoreAndSwitch() {
880
- sceneFailed.value = null;
881
- showPendingConfirm.value = false;
882
- const profile = pendingApplyProfile.value;
883
- pendingApplyProfile.value = null;
884
- if (profile) onApply(profile);
885
- }
886
-
887
- // 从确认弹窗进入重试
888
- function handlePendingRetry() {
889
- showPendingConfirm.value = false;
890
- // 直接用 sceneFailed 打开重试弹窗
891
- if (!sceneFailed.value) return;
892
- failedDelete.value = [...(sceneFailed.value.failedDelete || [])];
893
- failedInstall.value = [...(sceneFailed.value.failedInstall || [])];
894
- applyState.value = "failed";
895
- targetProfile.value = profiles.value.find((p) => p.id === activeId.value) || null;
896
- showApplyConfirm.value = true;
897
- }
898
-
899
- async function onConfirmApply() {
900
- if (!targetProfile.value) return;
901
- applyState.value = "executing";
902
- confirmLoading.value = true;
903
- applying.value = targetProfile.value.id;
904
- try {
905
- const res = await applyProfile(targetProfile.value.id);
906
- if (res.ok) {
907
- activeId.value = targetProfile.value.id;
908
- failedDelete.value = res.failedDelete || [];
909
- failedInstall.value = res.failedInstall || [];
910
- const hasFailed = failedDelete.value.length > 0 || failedInstall.value.length > 0;
911
-
912
- if (hasFailed) {
913
- applyState.value = "failed";
914
- // 记录失败信息到卡片角标
915
- sceneFailed.value = {
916
- failedDelete: [...failedDelete.value],
917
- failedInstall: [...failedInstall.value],
918
- };
919
- } else {
920
- // 全部成功,清理角标
921
- sceneFailed.value = null;
922
- message.success(`已切换到「${targetProfile.value.name}」`);
923
- showApplyConfirm.value = false;
924
- }
925
- (targetProfile.value.skills || []).forEach(recordUsage);
926
- emit("refresh");
927
- allSkills.value = await getSkills();
928
- }
929
- } catch {
930
- message.error("切换失败");
931
- applyState.value = "preview";
932
- } finally {
933
- confirmLoading.value = false;
934
- applying.value = null;
935
- }
936
- }
937
-
938
- // 单技能重试
939
- async function retrySingle(type, name) {
940
- const fn = type === "delete" ? retryDelete : retryInstall;
941
- const label = type === "delete" ? "删除" : "安装";
942
- try {
943
- const res = await fn(name);
944
- if (res.ok) {
945
- message.success(`技能 "${name}" ${label}成功`);
946
- if (type === "delete") {
947
- failedDelete.value = failedDelete.value.filter((n) => n !== name);
948
- } else {
949
- failedInstall.value = failedInstall.value.filter((n) => n !== name);
950
- }
951
- // 更新角标
952
- if (failedDelete.value.length === 0 && failedInstall.value.length === 0) {
953
- sceneFailed.value = null;
954
- } else if (sceneFailed.value) {
955
- sceneFailed.value = {
956
- failedDelete: [...failedDelete.value],
957
- failedInstall: [...failedInstall.value],
958
- };
959
- }
960
- // 如果所有失败都解决了,同步 settings 并关闭弹窗
961
- if (failedDelete.value.length === 0 && failedInstall.value.length === 0) {
962
- await syncApplySettings();
963
- }
964
- } else if (res.locked) {
965
- message.warning(`技能 "${name}" 仍被占用,请关闭相关程序后重试`);
966
- } else {
967
- message.error(res.error || `${label}失败`);
968
- }
969
- } catch {
970
- message.error(`${label}失败`);
971
- }
972
- }
973
-
974
- // 重试所有失败项
975
- async function retryAll() {
976
- // 先重试所有安装失败,再重试所有删除失败
977
- for (const name of [...failedInstall.value]) {
978
- await retrySingle("install", name);
979
- if (showApplyConfirm.value === false) return; // 弹窗已关闭
980
- }
981
- for (const name of [...failedDelete.value]) {
982
- await retrySingle("delete", name);
983
- if (showApplyConfirm.value === false) return;
984
- }
985
- }
986
-
987
- // 放弃切换
988
- function onAbandon() {
989
- showApplyConfirm.value = false;
990
- if (sceneFailed.value) {
991
- const parts = [];
992
- if (sceneFailed.value.failedDelete?.length) parts.push(`删除失败 ${sceneFailed.value.failedDelete.length} 个`);
993
- if (sceneFailed.value.failedInstall?.length) parts.push(`安装失败 ${sceneFailed.value.failedInstall.length} 个`);
994
- message.warning(`部分技能未就绪:${parts.join("、")}`);
995
- }
996
- }
997
-
998
- // 打开项目技能目录
999
- function openSkillsDestFolder() {
1000
- openSkillsDest().catch(() => message.error("打开目录失败"));
1001
- }
1002
-
1003
- // 所有失败解决后同步 settings(applyProfile 有失败时不写 settings)
1004
- async function syncApplySettings() {
1005
- if (!targetProfile.value) return;
1006
- // 重新调 applyProfile,此时文件操作全是幂等的,全部成功后会写 settings
1007
- const res = await applyProfile(targetProfile.value.id);
1008
- if (res.ok) {
1009
- message.success(`已切换到「${targetProfile.value.name}」`);
1010
- showApplyConfirm.value = false;
1011
- emit("refresh");
1012
- allSkills.value = await getSkills();
1013
- }
1014
- }
1015
-
1016
- // 从卡片角标重新打开失败重试弹窗
1017
- function reopenFailedModal() {
1018
- if (!sceneFailed.value) return;
1019
- failedDelete.value = [...(sceneFailed.value.failedDelete || [])];
1020
- failedInstall.value = [...(sceneFailed.value.failedInstall || [])];
1021
- applyState.value = "failed";
1022
- // 找到当前场景 profile
1023
- targetProfile.value = profiles.value.find((p) => p.id === activeId.value) || null;
1024
- showApplyConfirm.value = true;
1025
- }
1026
-
1027
- onMounted(() => {
1028
- loadData();
1029
- loadSkillsReadme();
1030
- });
1031
- </script>
1032
-
1033
- <style scoped>
1034
- .page-header {
1035
- display: flex;
1036
- align-items: center;
1037
- justify-content: space-between;
1038
- margin-bottom: 20px;
1039
- }
1040
-
1041
- .page-header h2 {
1042
- font-size: 20px;
1043
- font-weight: 600;
1044
- }
1045
-
1046
- .scene-grid {
1047
- display: grid;
1048
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
1049
- gap: 16px;
1050
- margin: 2px;
1051
- }
1052
-
1053
- .scene-card {
1054
- transition: box-shadow 0.2s;
1055
- }
1056
-
1057
- .scene-card.active {
1058
- box-shadow: 0 0 0 2px #2080f0;
1059
- }
1060
-
1061
- .scene-desc {
1062
- font-size: 13px;
1063
- color: #666;
1064
- margin: 0;
1065
- }
1066
-
1067
- .scene-footer {
1068
- display: flex;
1069
- align-items: center;
1070
- justify-content: space-between;
1071
- }
1072
-
1073
- .hint-text {
1074
- font-size: 12px;
1075
- color: #999;
1076
- }
1077
-
1078
- /* 差异预览 */
1079
- .diff-preview {
1080
- display: flex;
1081
- flex-direction: column;
1082
- gap: 14px;
1083
- }
1084
- .diff-hint {
1085
- margin: 0 0 4px;
1086
- font-size: 14px;
1087
- }
1088
- .diff-section {
1089
- display: flex;
1090
- flex-direction: column;
1091
- gap: 6px;
1092
- }
1093
- .diff-label {
1094
- display: flex;
1095
- align-items: center;
1096
- gap: 4px;
1097
- font-size: 13px;
1098
- font-weight: 600;
1099
- }
1100
- .diff-tags {
1101
- display: flex;
1102
- flex-wrap: wrap;
1103
- gap: 4px;
1104
- }
1105
-
1106
- /* 技能预览浮层 */
1107
- .skill-preview-list {
1108
- display: flex;
1109
- flex-direction: column;
1110
- gap: 2px;
1111
- max-height: 240px;
1112
- overflow-y: auto;
1113
- }
1114
- .skill-preview-item {
1115
- font-size: 13px;
1116
- padding: 2px 0;
1117
- }
1118
-
1119
- /* 技能分组 */
1120
- .modal-skill-groups {
1121
- max-height: 50vh;
1122
- overflow-y: auto;
1123
- }
1124
-
1125
- .skill-group {
1126
- margin-bottom: 4px;
1127
- border: 1px solid #eee;
1128
- border-radius: 6px;
1129
- overflow: hidden;
1130
- }
1131
-
1132
- .skill-group-header {
1133
- display: flex;
1134
- align-items: center;
1135
- gap: 6px;
1136
- padding: 8px 10px;
1137
- cursor: pointer;
1138
- user-select: none;
1139
- font-size: 13px;
1140
- background: #f8f9fa;
1141
- border-bottom: 1px solid #eee;
1142
- transition: background 0.15s;
1143
- }
1144
-
1145
- .skill-group-header:hover {
1146
- background: #eef0f4;
1147
- }
1148
-
1149
- .fold-arrow {
1150
- font-size: 10px;
1151
- transition: transform 0.2s;
1152
- color: #999;
1153
- flex-shrink: 0;
1154
- }
1155
-
1156
- .fold-arrow.open {
1157
- transform: rotate(90deg);
1158
- }
1159
-
1160
- .skill-group-label {
1161
- font-weight: 600;
1162
- overflow: hidden;
1163
- text-overflow: ellipsis;
1164
- white-space: nowrap;
1165
- flex: 1;
1166
- min-width: 0;
1167
- }
1168
-
1169
- .skill-group-count {
1170
- color: #999;
1171
- font-size: 12px;
1172
- flex-shrink: 0;
1173
- }
1174
-
1175
- .skill-group-body {
1176
- padding: 8px 10px;
1177
- display: grid;
1178
- grid-template-columns: repeat(2, 1fr);
1179
- gap: 6px;
1180
- }
1181
-
1182
- .always-apply-grid {
1183
- display: grid;
1184
- grid-template-columns: repeat(2, 1fr);
1185
- gap: 6px;
1186
- }
1187
-
1188
- .batch-actions {
1189
- margin-bottom: 10px;
1190
- }
1191
-
1192
- .search-highlight {
1193
- background: #ffd666;
1194
- padding: 0 2px;
1195
- border-radius: 2px;
1196
- }
1197
-
1198
- .search-input {
1199
- margin-bottom: 8px;
1200
- }
1201
-
1202
- /* 执行中状态 */
1203
- .executing-state {
1204
- display: flex;
1205
- flex-direction: column;
1206
- align-items: center;
1207
- padding: 32px 0;
1208
- gap: 12px;
1209
- }
1210
- .executing-text {
1211
- margin: 0;
1212
- font-size: 15px;
1213
- font-weight: 600;
1214
- }
1215
- .executing-hint {
1216
- margin: 0;
1217
- font-size: 13px;
1218
- color: #999;
1219
- }
1220
-
1221
- /* 失败区域 */
1222
- .failure-section {
1223
- display: flex;
1224
- flex-direction: column;
1225
- gap: 6px;
1226
- margin-top: 4px;
1227
- }
1228
- .failure-title {
1229
- display: flex;
1230
- align-items: center;
1231
- gap: 4px;
1232
- font-size: 13px;
1233
- font-weight: 600;
1234
- }
1235
- .failure-list {
1236
- display: flex;
1237
- flex-direction: column;
1238
- gap: 4px;
1239
- }
1240
- .failure-item {
1241
- display: flex;
1242
- align-items: center;
1243
- justify-content: space-between;
1244
- padding: 4px 8px;
1245
- background: #fff1f0;
1246
- border-radius: 4px;
1247
- font-size: 13px;
1248
- }
1249
- .failure-name {
1250
- font-weight: 500;
1251
- }
1252
- .failure-tip {
1253
- margin: 4px 0 0;
1254
- font-size: 12px;
1255
- color: #999;
1256
- }
1257
- .fail-marker {
1258
- margin-left: 2px;
1259
- font-size: 12px;
1260
- }
1261
- .popover-fail-detail {
1262
- font-size: 13px;
1263
- }
1264
- .popover-fail-section {
1265
- margin-bottom: 6px;
1266
- }
1267
- .popover-fail-section:last-child {
1268
- margin-bottom: 0;
1269
- }
1270
- .popover-fail-title {
1271
- font-weight: 600;
1272
- margin-bottom: 3px;
1273
- font-size: 12px;
1274
- }
1275
- .popover-fail-item {
1276
- padding: 1px 0 1px 8px;
1277
- font-size: 12px;
1278
- color: #555;
1279
- }
1280
-
1281
- /* 未就绪确认弹窗 */
1282
- .pending-confirm-body {
1283
- display: flex;
1284
- flex-direction: column;
1285
- gap: 6px;
1286
- }
1287
- .pending-confirm-body p {
1288
- margin: 0;
1289
- font-size: 14px;
1290
- }
1291
- .pending-confirm-section {
1292
- display: flex;
1293
- flex-direction: column;
1294
- gap: 2px;
1295
- }
1296
- .pending-confirm-title {
1297
- font-size: 12px;
1298
- font-weight: 600;
1299
- }
1300
- .pending-confirm-item {
1301
- font-size: 13px;
1302
- padding: 2px 0 2px 12px;
1303
- color: #555;
1304
- }
1305
- .pending-confirm-hint {
1306
- margin-top: 4px;
1307
- font-size: 13px;
1308
- color: #999;
1309
- }
1310
- </style>
1311
-
1312
- <style>
1313
- [data-theme="dark"] .scene-card.active {
1314
- box-shadow: 0 0 0 2px #6a8cff;
1315
- }
1316
- [data-theme="dark"] .hint-text,
1317
- [data-theme="dark"] .skill-group-count {
1318
- color: #6c7086;
1319
- }
1320
- [data-theme="dark"] .skill-group-header:hover {
1321
- background: rgba(255, 255, 255, 0.04);
1322
- }
1323
- [data-theme="dark"] .fold-arrow {
1324
- color: #6c7086;
1325
- }
1326
- [data-theme="dark"] .skill-group-label {
1327
- color: #cdd6f4 !important;
1328
- }
1329
- [data-theme="dark"] .skill-group {
1330
- border-color: #363b4a;
1331
- }
1332
- [data-theme="dark"] .skill-group-header {
1333
- background: #1e2129;
1334
- border-color: #363b4a;
1335
- }
1336
- [data-theme="dark"] .skill-group-header:hover {
1337
- background: #2a2e3a;
1338
- }
1339
- [data-theme="dark"] .search-highlight {
1340
- background: #8a6d00;
1341
- color: #fff;
1342
- }
1343
- [data-theme="dark"] .failure-item {
1344
- background: rgba(208, 48, 80, 0.15);
1345
- }
1346
- [data-theme="dark"] .executing-hint {
1347
- color: #6c7086;
1348
- }
1349
- [data-theme="dark"] .failure-tip {
1350
- color: #6c7086;
1351
- }
1352
- [data-theme="dark"] .popover-fail-item {
1353
- color: #b0b6cc;
1354
- }
1355
- [data-theme="dark"] .pending-confirm-item {
1356
- color: #b0b6cc;
1357
- }
1358
- [data-theme="dark"] .pending-confirm-hint {
1359
- color: #6c7086;
1360
- }
1361
- </style>