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.
- package/.claude/profiles.json +73 -67
- package/package.json +1 -1
- package/scripts/web-ui/server.js +189 -4
- package/scripts/web-ui/src/App.vue +2 -2
- package/scripts/web-ui/src/api/skills.js +18 -0
- package/scripts/web-ui/src/views/SceneSwitch.vue +181 -47
- package/scripts/web-ui/src/views/Settings.vue +43 -1
- package/.claude/skills/test1/.meta.json +0 -6
- package/.claude/skills/test2/.meta.json +0 -6
- package/.claude/skills/test3/.meta.json +0 -6
package/.claude/profiles.json
CHANGED
|
@@ -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": [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
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
package/scripts/web-ui/server.js
CHANGED
|
@@ -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: !!
|
|
340
|
-
path:
|
|
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
|
|
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
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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-
|
|
65
|
-
|
|
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-
|
|
68
|
-
|
|
69
|
-
|
|
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 {
|
|
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
|
-
|
|
90
|
-
const
|
|
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
|
-
|
|
106
|
-
|
|
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
|
|
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
|
-
|
|
111
|
-
message.success(
|
|
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
|
-
|
|
234
|
+
saving.value = false;
|
|
118
235
|
}
|
|
119
236
|
}
|
|
120
237
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
127
|
-
|
|
253
|
+
async function onApply(profile) {
|
|
254
|
+
applying.value = profile.id;
|
|
128
255
|
try {
|
|
129
|
-
const res = await
|
|
256
|
+
const res = await applyProfile(profile.id);
|
|
130
257
|
if (res.ok) {
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
}
|