mdk-skills 2.2.8 → 2.2.10

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,67 +1,73 @@
1
- {
2
- "version": 1,
3
- "profiles": [
4
- {
5
- "id": "vue3-frontend",
6
- "name": "Vue3 前端专项",
7
- "description": "Vue3 技术栈 | 业务架构 | 界面设计 | 代码规范评审",
8
- "skills": [
9
- "vue",
10
- "v3-fe-biz-patterns",
11
- "frontend-design",
12
- "frontend-code-review",
13
- "ui-ux-pro-max",
14
- "skill-creator"
15
- ],
16
- "always_apply": [
17
- "vue",
18
- "frontend-design",
19
- "frontend-code-review",
20
- "ui-ux-pro-max"
21
- ]
22
- },
23
- {
24
- "id": "react-frontend",
25
- "name": "React 前端专项",
26
- "description": "React 技术栈 | 界面设计 | 代码评审 | 通用前端能力",
27
- "skills": [
28
- "frontend-design",
29
- "frontend-code-review",
30
- "ui-ux-pro-max",
31
- "skill-creator"
32
- ],
33
- "always_apply": [
34
- "frontend-design",
35
- "frontend-code-review",
36
- "ui-ux-pro-max"
37
- ]
38
- },
39
- {
40
- "id": "backend-java",
41
- "name": "Java 后端开发",
42
- "description": "接口架构设计 | 数据库建模 | Java 后端体系能力",
43
- "skills": ["skill-creator"],
44
- "always_apply": []
45
- },
46
- {
47
- "id": "backend-python",
48
- "name": "Python 后端开发",
49
- "description": "接口架构设计 | 数据层设计 | Python 后端体系能力",
50
- "skills": ["skill-creator"],
51
- "always_apply": []
52
- },
53
- {
54
- "id": "backend-node",
55
- "name": "Node.js 后端开发",
56
- "description": "接口架构设计 | 服务端架构 | Node 后端体系能力",
57
- "skills": ["skill-creator"],
58
- "always_apply": []
59
- },
60
- {
61
- "id": "custom",
62
- "name": "自定义技能组合",
63
- "description": "自由勾选技能项,按需个性化配置",
64
- "skills": null
65
- }
66
- ]
67
- }
1
+ {
2
+ "version": 1,
3
+ "profiles": [
4
+ {
5
+ "id": "vue3-frontend",
6
+ "name": "Vue3 前端专项",
7
+ "description": "Vue3 技术栈 | 业务架构 | 界面设计 | 代码规范评审",
8
+ "skills": [
9
+ "vue",
10
+ "v3-fe-biz-patterns",
11
+ "frontend-design",
12
+ "frontend-code-review",
13
+ "ui-ux-pro-max",
14
+ "skill-creator"
15
+ ],
16
+ "always_apply": [
17
+ "vue",
18
+ "frontend-design",
19
+ "frontend-code-review",
20
+ "ui-ux-pro-max"
21
+ ]
22
+ },
23
+ {
24
+ "id": "react-frontend",
25
+ "name": "React 前端专项",
26
+ "description": "React 技术栈 | 界面设计 | 代码评审 | 通用前端能力",
27
+ "skills": [
28
+ "frontend-design",
29
+ "frontend-code-review",
30
+ "ui-ux-pro-max",
31
+ "skill-creator"
32
+ ],
33
+ "always_apply": [
34
+ "frontend-design",
35
+ "frontend-code-review",
36
+ "ui-ux-pro-max"
37
+ ]
38
+ },
39
+ {
40
+ "id": "backend-java",
41
+ "name": "Java 后端开发",
42
+ "description": "接口架构设计 | 数据库建模 | Java 后端体系能力",
43
+ "skills": [
44
+ "skill-creator"
45
+ ],
46
+ "always_apply": []
47
+ },
48
+ {
49
+ "id": "backend-python",
50
+ "name": "Python 后端开发",
51
+ "description": "接口架构设计 | 数据层设计 | Python 后端体系能力",
52
+ "skills": [
53
+ "skill-creator"
54
+ ],
55
+ "always_apply": []
56
+ },
57
+ {
58
+ "id": "backend-node",
59
+ "name": "Node.js 后端开发",
60
+ "description": "接口架构设计 | 服务端架构 | Node 后端体系能力",
61
+ "skills": [
62
+ "skill-creator"
63
+ ],
64
+ "always_apply": []
65
+ },
66
+ {
67
+ "id": "custom",
68
+ "name": "自定义技能组合",
69
+ "description": "自由勾选技能项,按需个性化配置",
70
+ "skills": null
71
+ }
72
+ ]
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdk-skills",
3
- "version": "2.2.8",
3
+ "version": "2.2.10",
4
4
  "description": "mdk-engineer - 沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
5
5
  "author": "XiaoMa",
6
6
  "license": "MIT",
@@ -286,13 +286,14 @@ async function handleApi(req, res) {
286
286
  return sendJSON(res, { ok: true, ...result });
287
287
  }
288
288
 
289
- // GET /api/profiles — 获取场景列表
289
+ // GET /api/profiles — 获取场景列表(含只读状态)
290
290
  if (method === "GET" && pathname === "/api/profiles") {
291
291
  const profiles = loadProfiles();
292
292
  const settings = readSettings();
293
293
  return sendJSON(res, {
294
294
  profiles: profiles || [],
295
295
  activeProfile: settings._active_profile || null,
296
+ readonly: !settings._skill_source,
296
297
  });
297
298
  }
298
299
 
@@ -309,6 +310,84 @@ async function handleApi(req, res) {
309
310
  return sendJSON(res, { ok: true, ...result });
310
311
  }
311
312
 
313
+ // POST /api/profiles/save — 新增/编辑场景(仅本地源模式)
314
+ if (method === "POST" && pathname === "/api/profiles/save") {
315
+ const body = await parseBody(req);
316
+ const settings = readSettings();
317
+ if (!settings._skill_source)
318
+ return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
319
+
320
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
321
+ let data = { version: 1, profiles: [] };
322
+ if (fs.existsSync(profilesPath)) {
323
+ try {
324
+ data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
325
+ } catch {}
326
+ }
327
+
328
+ const { id, name, description, skills, always_apply } = body;
329
+ if (!name) return sendJSON(res, { error: "场景名称不能为空" }, 400);
330
+
331
+ if (id) {
332
+ const index = data.profiles.findIndex((p) => p.id === id);
333
+ if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
334
+ data.profiles[index] = {
335
+ ...data.profiles[index],
336
+ name,
337
+ description: description || "",
338
+ skills: skills || [],
339
+ always_apply: always_apply || [],
340
+ };
341
+ } else {
342
+ const newId = "profile_" + Date.now().toString(36);
343
+ data.profiles.push({
344
+ id: newId,
345
+ name,
346
+ description: description || "",
347
+ skills: skills || [],
348
+ always_apply: always_apply || [],
349
+ });
350
+ }
351
+
352
+ fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
353
+
354
+ // 同步到项目 .claude
355
+ const projectProfilesPath = path.join(claudeDest, "profiles.json");
356
+ fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
357
+
358
+ return sendJSON(res, { ok: true, profiles: data.profiles });
359
+ }
360
+
361
+ // POST /api/profiles/delete — 删除场景(仅本地源模式)
362
+ if (method === "POST" && pathname === "/api/profiles/delete") {
363
+ const body = await parseBody(req);
364
+ const settings = readSettings();
365
+ if (!settings._skill_source)
366
+ return sendJSON(res, { error: "npm 模式下不可编辑场景" }, 403);
367
+
368
+ const { id } = body;
369
+ if (!id) return sendJSON(res, { error: "缺少场景 ID" }, 400);
370
+ if (id === "custom")
371
+ return sendJSON(res, { error: "不能删除内置场景" }, 403);
372
+
373
+ const profilesPath = path.join(skillsSource, ".claude", "profiles.json");
374
+ if (!fs.existsSync(profilesPath))
375
+ return sendJSON(res, { error: "场景文件不存在" }, 404);
376
+
377
+ const data = JSON.parse(fs.readFileSync(profilesPath, "utf-8"));
378
+ const index = data.profiles.findIndex((p) => p.id === id);
379
+ if (index === -1) return sendJSON(res, { error: "场景不存在" }, 404);
380
+
381
+ data.profiles.splice(index, 1);
382
+ fs.writeFileSync(profilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
383
+
384
+ // 同步到项目 .claude
385
+ const projectProfilesPath = path.join(claudeDest, "profiles.json");
386
+ fs.writeFileSync(projectProfilesPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
387
+
388
+ return sendJSON(res, { ok: true });
389
+ }
390
+
312
391
  // GET /api/status — 当前状态总览
313
392
  if (method === "GET" && pathname === "/api/status") {
314
393
  const settings = readSettings();
@@ -332,12 +411,43 @@ async function handleApi(req, res) {
332
411
  });
333
412
  }
334
413
 
335
- // GET /api/source — 获取本地源信息
414
+ // GET /api/source — 获取本地源信息(含初始化检测)
336
415
  if (method === "GET" && pathname === "/api/source") {
337
416
  const settings = readSettings();
417
+ const sourcePath = settings._skill_source || null;
418
+
419
+ let needsInit = false;
420
+ if (sourcePath) {
421
+ const claudeDir = path.join(sourcePath, ".claude");
422
+ if (fs.existsSync(claudeDir)) {
423
+ // 检查 profiles.json
424
+ const hasProfiles = fs.existsSync(path.join(claudeDir, "profiles.json"));
425
+ // 检查 settings.json
426
+ const hasSettings = fs.existsSync(path.join(claudeDir, "settings.json"));
427
+ // 检查每个 skill 目录的 .meta.json
428
+ const skillsDir = path.join(claudeDir, "skills");
429
+ let allMeta = true;
430
+ if (fs.existsSync(skillsDir)) {
431
+ for (const name of fs.readdirSync(skillsDir)) {
432
+ const skillDir = path.join(skillsDir, name);
433
+ if (fs.statSync(skillDir).isDirectory()) {
434
+ if (!fs.existsSync(path.join(skillDir, ".meta.json"))) {
435
+ allMeta = false;
436
+ break;
437
+ }
438
+ }
439
+ }
440
+ }
441
+ needsInit = !hasProfiles || !hasSettings || !allMeta;
442
+ } else {
443
+ needsInit = true;
444
+ }
445
+ }
446
+
338
447
  return sendJSON(res, {
339
- connected: !!settings._skill_source,
340
- path: settings._skill_source || null,
448
+ connected: !!sourcePath,
449
+ path: sourcePath,
450
+ needsInit,
341
451
  });
342
452
  }
343
453
 
@@ -427,6 +537,81 @@ async function handleApi(req, res) {
427
537
  return sendJSON(res, { ok: true });
428
538
  }
429
539
 
540
+ // POST /api/source/init — 初始化本地源骨架文件(缺啥补啥)
541
+ if (method === "POST" && pathname === "/api/source/init") {
542
+ const settings = readSettings();
543
+ const sourcePath = settings._skill_source;
544
+ if (!sourcePath) return sendJSON(res, { error: "未绑定本地源" }, 400);
545
+
546
+ const claudeDir = path.join(sourcePath, ".claude");
547
+ const skillsDir = path.join(claudeDir, "skills");
548
+ const profilesPath = path.join(claudeDir, "profiles.json");
549
+ const settingsPath = path.join(claudeDir, "settings.json");
550
+
551
+ const created = { profiles: false, settings: false, metaFiles: 0 };
552
+
553
+ // 1. profiles.json
554
+ if (!fs.existsSync(profilesPath)) {
555
+ fs.writeFileSync(
556
+ profilesPath,
557
+ JSON.stringify(
558
+ {
559
+ version: 1,
560
+ profiles: [
561
+ {
562
+ id: "custom",
563
+ name: "自定义技能组合",
564
+ description: "自由勾选技能项,按需个性化配置",
565
+ skills: null,
566
+ },
567
+ ],
568
+ },
569
+ null,
570
+ 2,
571
+ ) + "\n",
572
+ "utf-8",
573
+ );
574
+ created.profiles = true;
575
+ }
576
+
577
+ // 2. settings.json
578
+ if (!fs.existsSync(settingsPath)) {
579
+ fs.writeFileSync(
580
+ settingsPath,
581
+ JSON.stringify({ skills: {}, always_apply_skills: [] }, null, 2) + "\n",
582
+ "utf-8",
583
+ );
584
+ created.settings = true;
585
+ }
586
+
587
+ // 3. 每个 skill 目录的 .meta.json
588
+ if (fs.existsSync(skillsDir)) {
589
+ for (const name of fs.readdirSync(skillsDir)) {
590
+ const skillDir = path.join(skillsDir, name);
591
+ if (!fs.statSync(skillDir).isDirectory()) continue;
592
+ const metaPath = path.join(skillDir, ".meta.json");
593
+ if (!fs.existsSync(metaPath)) {
594
+ fs.writeFileSync(
595
+ metaPath,
596
+ JSON.stringify(
597
+ {
598
+ version: "1.0.0",
599
+ description: name,
600
+ tags: [],
601
+ },
602
+ null,
603
+ 2,
604
+ ) + "\n",
605
+ "utf-8",
606
+ );
607
+ created.metaFiles++;
608
+ }
609
+ }
610
+ }
611
+
612
+ return sendJSON(res, { ok: true, ...created });
613
+ }
614
+
430
615
  // GET /api/diagnose — 健康检查
431
616
  if (method === "GET" && pathname === "/api/diagnose") {
432
617
  const results = [];
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <n-message-provider>
3
3
  <n-notification-provider>
4
- <n-config-provider :theme="theme">
4
+ <n-config-provider :theme="theme" :locale="zhCN" :date-locale="dateZhCN">
5
5
  <n-layout class="app-layout">
6
6
  <!-- 导航栏 -->
7
7
  <n-layout-header class="app-header" bordered>
@@ -37,7 +37,7 @@
37
37
  <script setup>
38
38
  import { h, ref, onMounted, computed } from "vue";
39
39
  import { useRouter, useRoute } from "vue-router";
40
- import { NIcon } from "naive-ui";
40
+ import { NIcon, zhCN, dateZhCN } from "naive-ui";
41
41
  import {
42
42
  AppsOutline,
43
43
  SwapHorizontalOutline,
@@ -41,6 +41,20 @@ export function applyProfile(profileId) {
41
41
  });
42
42
  }
43
43
 
44
+ export function saveProfile(data) {
45
+ return request("/profiles/save", {
46
+ method: "POST",
47
+ body: JSON.stringify(data),
48
+ });
49
+ }
50
+
51
+ export function deleteProfile(id) {
52
+ return request("/profiles/delete", {
53
+ method: "POST",
54
+ body: JSON.stringify({ id }),
55
+ });
56
+ }
57
+
44
58
  export function getStatus() {
45
59
  return request("/status");
46
60
  }
@@ -64,6 +78,10 @@ export function syncSource() {
64
78
  return request("/source/sync", { method: "POST" });
65
79
  }
66
80
 
81
+ export function initSource() {
82
+ return request("/source/init", { method: "POST" });
83
+ }
84
+
67
85
  export function diagnose() {
68
86
  return request("/diagnose");
69
87
  }
@@ -2,19 +2,42 @@
2
2
  <div class="scene-switch">
3
3
  <div class="page-header">
4
4
  <h2>场景切换</h2>
5
+ <n-button v-if="!readonly" size="small" type="primary" @click="openCreate">
6
+ <template #icon><n-icon><AddOutline /></n-icon></template>
7
+ 新建场景
8
+ </n-button>
5
9
  </div>
6
10
 
7
11
  <n-spin :show="loading">
8
12
  <div class="scene-grid">
9
13
  <n-card
10
- v-for="profile in profiles"
14
+ v-for="profile in filteredProfiles"
11
15
  :key="profile.id"
12
16
  class="scene-card"
13
17
  :class="{ active: profile.id === activeId }"
14
18
  :title="profile.name"
15
19
  size="small"
16
- hoverable
17
20
  >
21
+ <template #header-extra>
22
+ <n-space v-if="!readonly && profile.id !== 'custom' && profile.skills !== null" :size="4">
23
+ <n-button size="tiny" quaternary @click="openEdit(profile)">
24
+ <template #icon><n-icon size="16"><PencilOutline /></n-icon></template>
25
+ </n-button>
26
+ <n-popconfirm
27
+ :negative-text="'取消'"
28
+ :positive-text="'确认删除'"
29
+ @positive-click="onDelete(profile)"
30
+ >
31
+ <template #trigger>
32
+ <n-button size="tiny" quaternary>
33
+ <template #icon><n-icon size="16"><TrashOutline /></n-icon></template>
34
+ </n-button>
35
+ </template>
36
+ 确定删除场景「{{ profile.name }}」?
37
+ </n-popconfirm>
38
+ </n-space>
39
+ </template>
40
+
18
41
  <p class="scene-desc">{{ profile.description }}</p>
19
42
 
20
43
  <template #footer>
@@ -30,7 +53,7 @@
30
53
  </n-tag>
31
54
 
32
55
  <n-button
33
- v-if="profile.id !== activeId && profile.skills !== null"
56
+ v-if="profile.id !== activeId"
34
57
  size="small"
35
58
  type="primary"
36
59
  :loading="applying === profile.id"
@@ -38,44 +61,90 @@
38
61
  >
39
62
  应用
40
63
  </n-button>
41
- <n-button
42
- v-if="profile.id !== activeId && profile.skills === null"
43
- size="small"
44
- @click="openCustomDialog"
45
- >
46
- 自定义勾选
47
- </n-button>
48
64
  </div>
49
65
  </template>
50
66
  </n-card>
51
67
  </div>
52
68
  </n-spin>
53
69
 
54
- <!-- 自定义勾选弹窗 -->
55
- <n-modal v-model:show="showCustom" title="自定义技能组合" preset="card" style="width: 480px">
56
- <n-checkbox-group v-model:value="customSelected">
57
- <n-space vertical>
58
- <n-checkbox
59
- v-for="skill in allSkills"
60
- :key="skill.name"
61
- :value="skill.name"
62
- :label="skill.name"
70
+ <!-- 编辑弹窗 -->
71
+ <n-modal
72
+ v-model:show="showEditor"
73
+ :title="editingProfile ? '编辑场景' : '新建场景'"
74
+ preset="card"
75
+ style="width: 520px"
76
+ :mask-closable="false"
77
+ >
78
+ <n-form :model="formData" label-placement="top">
79
+ <n-form-item label="场景名称" required>
80
+ <n-input
81
+ v-model:value="formData.name"
82
+ placeholder="给场景起个名字"
83
+ :maxlength="30"
63
84
  />
64
- </n-space>
65
- </n-checkbox-group>
85
+ </n-form-item>
86
+ <n-form-item label="场景描述">
87
+ <n-input
88
+ v-model:value="formData.description"
89
+ type="textarea"
90
+ :rows="2"
91
+ placeholder="简短描述这个场景的用途"
92
+ />
93
+ </n-form-item>
94
+ <n-divider />
95
+ <n-form-item label="包含的技能">
96
+ <n-checkbox-group v-model:value="formData.skills">
97
+ <n-space vertical>
98
+ <n-checkbox
99
+ v-for="skill in allSkills"
100
+ :key="skill.name"
101
+ :value="skill.name"
102
+ :label="skill.name"
103
+ />
104
+ </n-space>
105
+ </n-checkbox-group>
106
+ </n-form-item>
107
+ <n-divider />
108
+ <n-form-item label="始终加载(always_apply)">
109
+ <template v-if="formData.skills.length === 0">
110
+ <span class="hint-text">请先勾选包含的技能</span>
111
+ </template>
112
+ <n-checkbox-group v-else v-model:value="formData.always_apply">
113
+ <n-space vertical>
114
+ <n-checkbox
115
+ v-for="skillName in formData.skills"
116
+ :key="skillName"
117
+ :value="skillName"
118
+ :label="skillName"
119
+ />
120
+ </n-space>
121
+ </n-checkbox-group>
122
+ </n-form-item>
123
+ </n-form>
66
124
  <template #footer>
67
- <n-button type="primary" :loading="customLoading" @click="onApplyCustom">
68
- 确认安装
69
- </n-button>
125
+ <n-space justify="end">
126
+ <n-button @click="showEditor = false">取消</n-button>
127
+ <n-button type="primary" :loading="saving" @click="onSave">
128
+ 保存
129
+ </n-button>
130
+ </n-space>
70
131
  </template>
71
132
  </n-modal>
72
133
  </div>
73
134
  </template>
74
135
 
75
136
  <script setup>
76
- import { ref, onMounted } from "vue";
137
+ import { ref, onMounted, computed } from "vue";
77
138
  import { useMessage } from "naive-ui";
78
- import { getProfiles, applyProfile, getSkills, installSkills } from "../api/skills";
139
+ import { NIcon } from "naive-ui";
140
+ import { AddOutline, PencilOutline, TrashOutline } from "@vicons/ionicons5";
141
+ import {
142
+ getProfiles,
143
+ applyProfile,
144
+ getSkills,
145
+ saveProfile,
146
+ deleteProfile,
147
+ } from "../api/skills";
79
148
 
80
149
  const emit = defineEmits(["refresh"]);
81
150
  const message = useMessage();
@@ -85,10 +154,21 @@ const activeId = ref(null);
85
154
  const allSkills = ref([]);
86
155
  const loading = ref(false);
87
156
  const applying = ref(null);
157
+ const readonly = ref(true);
158
+
159
+ // 编辑器
160
+ const showEditor = ref(false);
161
+ const editingProfile = ref(null);
162
+ const saving = ref(false);
163
+ const formData = ref({
164
+ name: "",
165
+ description: "",
166
+ skills: [],
167
+ always_apply: [],
168
+ });
88
169
 
89
- const showCustom = ref(false);
90
- const customSelected = ref([]);
91
- const customLoading = ref(false);
170
+ // 过滤掉不可见的场景(如已有内置)
171
+ const filteredProfiles = computed(() => profiles.value);
92
172
 
93
173
  async function loadData() {
94
174
  loading.value = true;
@@ -96,47 +176,93 @@ async function loadData() {
96
176
  const res = await getProfiles();
97
177
  profiles.value = res.profiles || [];
98
178
  activeId.value = res.activeProfile;
179
+ readonly.value = res.readonly !== false;
99
180
  allSkills.value = await getSkills();
100
181
  } finally {
101
182
  loading.value = false;
102
183
  }
103
184
  }
104
185
 
105
- async function onApply(profile) {
106
- applying.value = profile.id;
186
+ function openCreate() {
187
+ editingProfile.value = null;
188
+ formData.value = {
189
+ name: "",
190
+ description: "",
191
+ skills: [],
192
+ always_apply: [],
193
+ };
194
+ showEditor.value = true;
195
+ }
196
+
197
+ function openEdit(profile) {
198
+ editingProfile.value = profile;
199
+ formData.value = {
200
+ name: profile.name,
201
+ description: profile.description || "",
202
+ skills: [...(profile.skills || [])],
203
+ always_apply: [...(profile.always_apply || [])],
204
+ };
205
+ showEditor.value = true;
206
+ }
207
+
208
+ async function onSave() {
209
+ if (!formData.value.name.trim()) {
210
+ message.warning("请填写场景名称");
211
+ return;
212
+ }
213
+ saving.value = true;
107
214
  try {
108
- const res = await applyProfile(profile.id);
215
+ const data = {
216
+ id: editingProfile.value?.id,
217
+ name: formData.value.name.trim(),
218
+ description: formData.value.description.trim(),
219
+ skills: formData.value.skills,
220
+ always_apply: formData.value.always_apply,
221
+ };
222
+ const res = await saveProfile(data);
109
223
  if (res.ok) {
110
- activeId.value = profile.id;
111
- message.success(`已切换到「${profile.name}」`);
224
+ profiles.value = res.profiles;
225
+ message.success(editingProfile.value ? "场景已更新" : "场景已创建");
226
+ showEditor.value = false;
112
227
  emit("refresh");
228
+ } else if (res.error) {
229
+ message.error(res.error);
113
230
  }
114
231
  } catch {
115
- message.error("切换失败");
232
+ message.error("保存失败");
116
233
  } finally {
117
- applying.value = null;
234
+ saving.value = false;
118
235
  }
119
236
  }
120
237
 
121
- function openCustomDialog() {
122
- customSelected.value = allSkills.value.filter((s) => s.enabled).map((s) => s.name);
123
- showCustom.value = true;
238
+ async function onDelete(profile) {
239
+ try {
240
+ const res = await deleteProfile(profile.id);
241
+ if (res.ok) {
242
+ profiles.value = profiles.value.filter((p) => p.id !== profile.id);
243
+ message.success(`场景「${profile.name}」已删除`);
244
+ emit("refresh");
245
+ } else if (res.error) {
246
+ message.error(res.error);
247
+ }
248
+ } catch {
249
+ message.error("删除失败");
250
+ }
124
251
  }
125
252
 
126
- async function onApplyCustom() {
127
- customLoading.value = true;
253
+ async function onApply(profile) {
254
+ applying.value = profile.id;
128
255
  try {
129
- const res = await installSkills(customSelected.value);
256
+ const res = await applyProfile(profile.id);
130
257
  if (res.ok) {
131
- message.success(`已安装 ${customSelected.value.length} 个技能`);
132
- activeId.value = "custom";
133
- showCustom.value = false;
258
+ activeId.value = profile.id;
259
+ message.success(`已切换到「${profile.name}」`);
134
260
  emit("refresh");
135
261
  }
136
262
  } catch {
137
- message.error("安装失败");
263
+ message.error("切换失败");
138
264
  } finally {
139
- customLoading.value = false;
265
+ applying.value = null;
140
266
  }
141
267
  }
142
268
 
@@ -145,6 +271,9 @@ onMounted(loadData);
145
271
 
146
272
  <style scoped>
147
273
  .page-header {
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: space-between;
148
277
  margin-bottom: 20px;
149
278
  }
150
279
 
@@ -178,4 +307,9 @@ onMounted(loadData);
178
307
  align-items: center;
179
308
  justify-content: space-between;
180
309
  }
310
+
311
+ .hint-text {
312
+ font-size: 12px;
313
+ color: #999;
314
+ }
181
315
  </style>
@@ -16,6 +16,12 @@
16
16
  </n-alert>
17
17
  </div>
18
18
 
19
+ <!-- 初始化提示 -->
20
+ <n-alert v-if="sourceInfo.connected && sourceInfo.needsInit" type="warning" :bordered="false" class="init-alert">
21
+ <template #header>本地源缺少配置文件</template>
22
+ profiles.json、settings.json 或 .meta.json 缺失,初始化后将生成骨架文件
23
+ </n-alert>
24
+
19
25
  <div class="source-actions">
20
26
  <n-input
21
27
  v-if="!sourceInfo.connected"
@@ -35,6 +41,14 @@
35
41
  >
36
42
  连接
37
43
  </n-button>
44
+ <n-button
45
+ v-if="sourceInfo.connected && sourceInfo.needsInit"
46
+ type="warning"
47
+ :loading="initializing"
48
+ @click="onInit"
49
+ >
50
+ 初始化
51
+ </n-button>
38
52
  <n-button
39
53
  v-if="sourceInfo.connected"
40
54
  :loading="syncing"
@@ -97,15 +111,17 @@ import {
97
111
  connectSource,
98
112
  disconnectSource,
99
113
  syncSource,
114
+ initSource,
100
115
  diagnose,
101
116
  } from "../api/skills";
102
117
 
103
118
  const emit = defineEmits(["refresh"]);
104
119
  const message = useMessage();
105
120
 
106
- const sourceInfo = ref({ connected: false, path: null });
121
+ const sourceInfo = ref({ connected: false, path: null, needsInit: false });
107
122
  const repoPath = ref("");
108
123
  const connecting = ref(false);
124
+ const initializing = ref(false);
109
125
  const syncing = ref(false);
110
126
  const disconnecting = ref(false);
111
127
  const diagnosing = ref(false);
@@ -149,6 +165,28 @@ async function onSync() {
149
165
  }
150
166
  }
151
167
 
168
+ async function onInit() {
169
+ initializing.value = true;
170
+ try {
171
+ const res = await initSource();
172
+ if (res.ok) {
173
+ const parts = [];
174
+ if (res.profiles) parts.push("profiles.json");
175
+ if (res.settings) parts.push("settings.json");
176
+ if (res.metaFiles > 0) parts.push(`${res.metaFiles} 个 .meta.json`);
177
+ message.success(`初始化完成:${parts.join("、") || "无缺失文件"}`);
178
+ await loadSource();
179
+ emit("refresh");
180
+ } else {
181
+ message.error(res.error || "初始化失败");
182
+ }
183
+ } catch {
184
+ message.error("初始化失败");
185
+ } finally {
186
+ initializing.value = false;
187
+ }
188
+ }
189
+
152
190
  async function onDisconnect() {
153
191
  disconnecting.value = true;
154
192
  try {
@@ -193,6 +231,10 @@ onMounted(loadSource);
193
231
  margin-bottom: 20px;
194
232
  }
195
233
 
234
+ .init-alert {
235
+ margin-bottom: 12px;
236
+ }
237
+
196
238
  .source-status {
197
239
  margin-bottom: 16px;
198
240
  }
@@ -1,6 +0,0 @@
1
- {
2
- "name": "test1",
3
- "version": "1.0.0",
4
- "description": "测试技能1",
5
- "tags": ["test", "frontend"]
6
- }
@@ -1,6 +0,0 @@
1
- {
2
- "name": "test2",
3
- "version": "1.0.0",
4
- "description": "测试技能2",
5
- "tags": ["test", "frontend"]
6
- }
@@ -1,6 +0,0 @@
1
- {
2
- "name": "test3",
3
- "version": "1.0.0",
4
- "description": "测试技能3",
5
- "tags": ["test", "frontend"]
6
- }