mdk-skills 2.2.8 → 2.2.9
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/package.json +1 -1
- package/scripts/web-ui/server.js +80 -1
- package/scripts/web-ui/src/api/skills.js +14 -0
- package/scripts/web-ui/src/views/SceneSwitch.vue +177 -47
- 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/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();
|
|
@@ -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
|
}
|
|
@@ -2,19 +2,38 @@
|
|
|
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 @positive-click="onDelete(profile)">
|
|
27
|
+
<template #trigger>
|
|
28
|
+
<n-button size="tiny" quaternary>
|
|
29
|
+
<template #icon><n-icon size="16"><TrashOutline /></n-icon></template>
|
|
30
|
+
</n-button>
|
|
31
|
+
</template>
|
|
32
|
+
确定删除场景「{{ profile.name }}」?
|
|
33
|
+
</n-popconfirm>
|
|
34
|
+
</n-space>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
18
37
|
<p class="scene-desc">{{ profile.description }}</p>
|
|
19
38
|
|
|
20
39
|
<template #footer>
|
|
@@ -30,7 +49,7 @@
|
|
|
30
49
|
</n-tag>
|
|
31
50
|
|
|
32
51
|
<n-button
|
|
33
|
-
v-if="profile.id !== activeId
|
|
52
|
+
v-if="profile.id !== activeId"
|
|
34
53
|
size="small"
|
|
35
54
|
type="primary"
|
|
36
55
|
:loading="applying === profile.id"
|
|
@@ -38,44 +57,90 @@
|
|
|
38
57
|
>
|
|
39
58
|
应用
|
|
40
59
|
</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
60
|
</div>
|
|
49
61
|
</template>
|
|
50
62
|
</n-card>
|
|
51
63
|
</div>
|
|
52
64
|
</n-spin>
|
|
53
65
|
|
|
54
|
-
<!--
|
|
55
|
-
<n-modal
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
<!-- 编辑弹窗 -->
|
|
67
|
+
<n-modal
|
|
68
|
+
v-model:show="showEditor"
|
|
69
|
+
:title="editingProfile ? '编辑场景' : '新建场景'"
|
|
70
|
+
preset="card"
|
|
71
|
+
style="width: 520px"
|
|
72
|
+
:mask-closable="false"
|
|
73
|
+
>
|
|
74
|
+
<n-form :model="formData" label-placement="top">
|
|
75
|
+
<n-form-item label="场景名称" required>
|
|
76
|
+
<n-input
|
|
77
|
+
v-model:value="formData.name"
|
|
78
|
+
placeholder="给场景起个名字"
|
|
79
|
+
:maxlength="30"
|
|
63
80
|
/>
|
|
64
|
-
</n-
|
|
65
|
-
|
|
81
|
+
</n-form-item>
|
|
82
|
+
<n-form-item label="场景描述">
|
|
83
|
+
<n-input
|
|
84
|
+
v-model:value="formData.description"
|
|
85
|
+
type="textarea"
|
|
86
|
+
:rows="2"
|
|
87
|
+
placeholder="简短描述这个场景的用途"
|
|
88
|
+
/>
|
|
89
|
+
</n-form-item>
|
|
90
|
+
<n-divider />
|
|
91
|
+
<n-form-item label="包含的技能">
|
|
92
|
+
<n-checkbox-group v-model:value="formData.skills">
|
|
93
|
+
<n-space vertical>
|
|
94
|
+
<n-checkbox
|
|
95
|
+
v-for="skill in allSkills"
|
|
96
|
+
:key="skill.name"
|
|
97
|
+
:value="skill.name"
|
|
98
|
+
:label="skill.name"
|
|
99
|
+
/>
|
|
100
|
+
</n-space>
|
|
101
|
+
</n-checkbox-group>
|
|
102
|
+
</n-form-item>
|
|
103
|
+
<n-divider />
|
|
104
|
+
<n-form-item label="始终加载(always_apply)">
|
|
105
|
+
<template v-if="formData.skills.length === 0">
|
|
106
|
+
<span class="hint-text">请先勾选包含的技能</span>
|
|
107
|
+
</template>
|
|
108
|
+
<n-checkbox-group v-else v-model:value="formData.always_apply">
|
|
109
|
+
<n-space vertical>
|
|
110
|
+
<n-checkbox
|
|
111
|
+
v-for="skillName in formData.skills"
|
|
112
|
+
:key="skillName"
|
|
113
|
+
:value="skillName"
|
|
114
|
+
:label="skillName"
|
|
115
|
+
/>
|
|
116
|
+
</n-space>
|
|
117
|
+
</n-checkbox-group>
|
|
118
|
+
</n-form-item>
|
|
119
|
+
</n-form>
|
|
66
120
|
<template #footer>
|
|
67
|
-
<n-
|
|
68
|
-
|
|
69
|
-
|
|
121
|
+
<n-space justify="end">
|
|
122
|
+
<n-button @click="showEditor = false">取消</n-button>
|
|
123
|
+
<n-button type="primary" :loading="saving" @click="onSave">
|
|
124
|
+
保存
|
|
125
|
+
</n-button>
|
|
126
|
+
</n-space>
|
|
70
127
|
</template>
|
|
71
128
|
</n-modal>
|
|
72
129
|
</div>
|
|
73
130
|
</template>
|
|
74
131
|
|
|
75
132
|
<script setup>
|
|
76
|
-
import { ref, onMounted } from "vue";
|
|
133
|
+
import { ref, onMounted, computed } from "vue";
|
|
77
134
|
import { useMessage } from "naive-ui";
|
|
78
|
-
import {
|
|
135
|
+
import { NIcon } from "naive-ui";
|
|
136
|
+
import { AddOutline, PencilOutline, TrashOutline } from "@vicons/ionicons5";
|
|
137
|
+
import {
|
|
138
|
+
getProfiles,
|
|
139
|
+
applyProfile,
|
|
140
|
+
getSkills,
|
|
141
|
+
saveProfile,
|
|
142
|
+
deleteProfile,
|
|
143
|
+
} from "../api/skills";
|
|
79
144
|
|
|
80
145
|
const emit = defineEmits(["refresh"]);
|
|
81
146
|
const message = useMessage();
|
|
@@ -85,10 +150,21 @@ const activeId = ref(null);
|
|
|
85
150
|
const allSkills = ref([]);
|
|
86
151
|
const loading = ref(false);
|
|
87
152
|
const applying = ref(null);
|
|
153
|
+
const readonly = ref(true);
|
|
88
154
|
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
155
|
+
// 编辑器
|
|
156
|
+
const showEditor = ref(false);
|
|
157
|
+
const editingProfile = ref(null);
|
|
158
|
+
const saving = ref(false);
|
|
159
|
+
const formData = ref({
|
|
160
|
+
name: "",
|
|
161
|
+
description: "",
|
|
162
|
+
skills: [],
|
|
163
|
+
always_apply: [],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 过滤掉不可见的场景(如已有内置)
|
|
167
|
+
const filteredProfiles = computed(() => profiles.value);
|
|
92
168
|
|
|
93
169
|
async function loadData() {
|
|
94
170
|
loading.value = true;
|
|
@@ -96,47 +172,93 @@ async function loadData() {
|
|
|
96
172
|
const res = await getProfiles();
|
|
97
173
|
profiles.value = res.profiles || [];
|
|
98
174
|
activeId.value = res.activeProfile;
|
|
175
|
+
readonly.value = res.readonly !== false;
|
|
99
176
|
allSkills.value = await getSkills();
|
|
100
177
|
} finally {
|
|
101
178
|
loading.value = false;
|
|
102
179
|
}
|
|
103
180
|
}
|
|
104
181
|
|
|
105
|
-
|
|
106
|
-
|
|
182
|
+
function openCreate() {
|
|
183
|
+
editingProfile.value = null;
|
|
184
|
+
formData.value = {
|
|
185
|
+
name: "",
|
|
186
|
+
description: "",
|
|
187
|
+
skills: [],
|
|
188
|
+
always_apply: [],
|
|
189
|
+
};
|
|
190
|
+
showEditor.value = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function openEdit(profile) {
|
|
194
|
+
editingProfile.value = profile;
|
|
195
|
+
formData.value = {
|
|
196
|
+
name: profile.name,
|
|
197
|
+
description: profile.description || "",
|
|
198
|
+
skills: [...(profile.skills || [])],
|
|
199
|
+
always_apply: [...(profile.always_apply || [])],
|
|
200
|
+
};
|
|
201
|
+
showEditor.value = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function onSave() {
|
|
205
|
+
if (!formData.value.name.trim()) {
|
|
206
|
+
message.warning("请填写场景名称");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
saving.value = true;
|
|
107
210
|
try {
|
|
108
|
-
const
|
|
211
|
+
const data = {
|
|
212
|
+
id: editingProfile.value?.id,
|
|
213
|
+
name: formData.value.name.trim(),
|
|
214
|
+
description: formData.value.description.trim(),
|
|
215
|
+
skills: formData.value.skills,
|
|
216
|
+
always_apply: formData.value.always_apply,
|
|
217
|
+
};
|
|
218
|
+
const res = await saveProfile(data);
|
|
109
219
|
if (res.ok) {
|
|
110
|
-
|
|
111
|
-
message.success(
|
|
220
|
+
profiles.value = res.profiles;
|
|
221
|
+
message.success(editingProfile.value ? "场景已更新" : "场景已创建");
|
|
222
|
+
showEditor.value = false;
|
|
112
223
|
emit("refresh");
|
|
224
|
+
} else if (res.error) {
|
|
225
|
+
message.error(res.error);
|
|
113
226
|
}
|
|
114
227
|
} catch {
|
|
115
|
-
message.error("
|
|
228
|
+
message.error("保存失败");
|
|
116
229
|
} finally {
|
|
117
|
-
|
|
230
|
+
saving.value = false;
|
|
118
231
|
}
|
|
119
232
|
}
|
|
120
233
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
234
|
+
async function onDelete(profile) {
|
|
235
|
+
try {
|
|
236
|
+
const res = await deleteProfile(profile.id);
|
|
237
|
+
if (res.ok) {
|
|
238
|
+
profiles.value = profiles.value.filter((p) => p.id !== profile.id);
|
|
239
|
+
message.success(`场景「${profile.name}」已删除`);
|
|
240
|
+
emit("refresh");
|
|
241
|
+
} else if (res.error) {
|
|
242
|
+
message.error(res.error);
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
message.error("删除失败");
|
|
246
|
+
}
|
|
124
247
|
}
|
|
125
248
|
|
|
126
|
-
async function
|
|
127
|
-
|
|
249
|
+
async function onApply(profile) {
|
|
250
|
+
applying.value = profile.id;
|
|
128
251
|
try {
|
|
129
|
-
const res = await
|
|
252
|
+
const res = await applyProfile(profile.id);
|
|
130
253
|
if (res.ok) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
showCustom.value = false;
|
|
254
|
+
activeId.value = profile.id;
|
|
255
|
+
message.success(`已切换到「${profile.name}」`);
|
|
134
256
|
emit("refresh");
|
|
135
257
|
}
|
|
136
258
|
} catch {
|
|
137
|
-
message.error("
|
|
259
|
+
message.error("切换失败");
|
|
138
260
|
} finally {
|
|
139
|
-
|
|
261
|
+
applying.value = null;
|
|
140
262
|
}
|
|
141
263
|
}
|
|
142
264
|
|
|
@@ -145,6 +267,9 @@ onMounted(loadData);
|
|
|
145
267
|
|
|
146
268
|
<style scoped>
|
|
147
269
|
.page-header {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
justify-content: space-between;
|
|
148
273
|
margin-bottom: 20px;
|
|
149
274
|
}
|
|
150
275
|
|
|
@@ -178,4 +303,9 @@ onMounted(loadData);
|
|
|
178
303
|
align-items: center;
|
|
179
304
|
justify-content: space-between;
|
|
180
305
|
}
|
|
306
|
+
|
|
307
|
+
.hint-text {
|
|
308
|
+
font-size: 12px;
|
|
309
|
+
color: #999;
|
|
310
|
+
}
|
|
181
311
|
</style>
|