mdk-skills 2.2.7 → 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
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();
|
|
@@ -321,6 +400,7 @@ async function handleApi(req, res) {
|
|
|
321
400
|
const enabledCount = userSkills.filter((s) => s.enabled).length;
|
|
322
401
|
const mdExists = fs.existsSync(path.join(projectRoot, "CLAUDE.md"));
|
|
323
402
|
return sendJSON(res, {
|
|
403
|
+
pkgSkills,
|
|
324
404
|
enabledCount,
|
|
325
405
|
totalCount: pkgSkills.length,
|
|
326
406
|
activeProfile: activeProfile
|
|
@@ -344,11 +424,14 @@ async function handleApi(req, res) {
|
|
|
344
424
|
if (method === "POST" && pathname === "/api/source/connect") {
|
|
345
425
|
const body = await parseBody(req);
|
|
346
426
|
const repoPath = body.path;
|
|
347
|
-
if (!repoPath)
|
|
348
|
-
return sendJSON(res, { error: "缺少仓库路径" }, 400);
|
|
427
|
+
if (!repoPath) return sendJSON(res, { error: "缺少仓库路径" }, 400);
|
|
349
428
|
const resolved = path.resolve(repoPath);
|
|
350
429
|
if (!fs.existsSync(path.join(resolved, ".claude", "skills"))) {
|
|
351
|
-
return sendJSON(
|
|
430
|
+
return sendJSON(
|
|
431
|
+
res,
|
|
432
|
+
{ error: `路径 "${resolved}" 下没有 .claude/skills/` },
|
|
433
|
+
400,
|
|
434
|
+
);
|
|
352
435
|
}
|
|
353
436
|
const settings = readSettings();
|
|
354
437
|
settings._skill_source = resolved;
|
|
@@ -373,8 +456,7 @@ async function handleApi(req, res) {
|
|
|
373
456
|
if (method === "POST" && pathname === "/api/source/sync") {
|
|
374
457
|
const settings = readSettings();
|
|
375
458
|
const sourcePath = settings._skill_source;
|
|
376
|
-
if (!sourcePath)
|
|
377
|
-
return sendJSON(res, { error: "尚未绑定技能源" }, 400);
|
|
459
|
+
if (!sourcePath) return sendJSON(res, { error: "尚未绑定技能源" }, 400);
|
|
378
460
|
if (!fs.existsSync(path.join(sourcePath, ".claude"))) {
|
|
379
461
|
return sendJSON(res, { error: "绑定的路径已失效" }, 400);
|
|
380
462
|
}
|
|
@@ -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>
|