mdk-skills 2.2.9 → 2.2.11
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 +3 -1
- package/scripts/cli.js +552 -553
- package/scripts/core.js +15 -5
- package/scripts/web-ui/server.js +173 -8
- package/scripts/web-ui/src/App.vue +8 -2
- package/scripts/web-ui/src/api/skills.js +20 -0
- package/scripts/web-ui/src/components/SkillCard.vue +16 -8
- package/scripts/web-ui/src/components/StatusBar.vue +9 -1
- package/scripts/web-ui/src/main.js +1 -0
- package/scripts/web-ui/src/router/index.js +2 -0
- package/scripts/web-ui/src/styles/main.css +96 -0
- package/scripts/web-ui/src/views/Dashboard.vue +129 -5
- package/scripts/web-ui/src/views/ReadmeView.vue +100 -0
- package/scripts/web-ui/src/views/SceneSwitch.vue +131 -3
- package/scripts/web-ui/src/views/Settings.vue +43 -1
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="dashboard">
|
|
3
|
+
<!-- 仓库 README -->
|
|
4
|
+
<n-collapse v-if="readmeContent" :default-expanded-names="['readme']" class="readme-collapse">
|
|
5
|
+
<n-collapse-item title="仓库说明" name="readme">
|
|
6
|
+
<div class="markdown-content" v-html="renderedReadme" />
|
|
7
|
+
</n-collapse-item>
|
|
8
|
+
</n-collapse>
|
|
9
|
+
|
|
3
10
|
<div class="page-header">
|
|
4
11
|
<h2>技能列表</h2>
|
|
5
12
|
<n-button size="small" @click="loadSkills">
|
|
@@ -14,6 +21,7 @@
|
|
|
14
21
|
:key="skill.name"
|
|
15
22
|
:skill="skill"
|
|
16
23
|
@refresh="loadSkills"
|
|
24
|
+
@click="showSkillDetail(skill)"
|
|
17
25
|
/>
|
|
18
26
|
</n-spin>
|
|
19
27
|
|
|
@@ -24,20 +32,88 @@
|
|
|
24
32
|
</n-button>
|
|
25
33
|
</template>
|
|
26
34
|
</n-empty>
|
|
35
|
+
|
|
36
|
+
<!-- 技能详情弹窗 -->
|
|
37
|
+
<n-modal
|
|
38
|
+
v-model:show="detailVisible"
|
|
39
|
+
:title="detailSkill?.name || '技能详情'"
|
|
40
|
+
preset="card"
|
|
41
|
+
style="width: 720px;"
|
|
42
|
+
content-style="max-height: 65vh; overflow-y: auto; padding: 16px 24px; background: #fff;"
|
|
43
|
+
:mask-closable="true"
|
|
44
|
+
>
|
|
45
|
+
<div v-if="detailLoading" class="detail-status">
|
|
46
|
+
<n-spin />
|
|
47
|
+
</div>
|
|
48
|
+
<div v-else-if="detailContent" class="markdown-content" v-html="renderedDetail" />
|
|
49
|
+
<n-empty v-else description="该技能没有 SKILL.md 文档" />
|
|
50
|
+
</n-modal>
|
|
27
51
|
</div>
|
|
28
52
|
</template>
|
|
29
53
|
|
|
30
54
|
<script setup>
|
|
31
|
-
import { ref, onMounted, onActivated, onUnmounted } from "vue";
|
|
55
|
+
import { ref, onMounted, onActivated, onUnmounted, nextTick } from "vue";
|
|
32
56
|
import { NIcon } from "naive-ui";
|
|
33
57
|
import { RefreshOutline } from "@vicons/ionicons5";
|
|
58
|
+
import { marked } from "marked";
|
|
59
|
+
import hljs from "highlight.js";
|
|
34
60
|
import SkillCard from "../components/SkillCard.vue";
|
|
35
|
-
import { getSkills } from "../api/skills";
|
|
61
|
+
import { getSkills, getReadme, getSkillReadme } from "../api/skills";
|
|
62
|
+
|
|
63
|
+
// marked 配置:代码高亮 + 外链安全
|
|
64
|
+
marked.use({
|
|
65
|
+
renderer: {
|
|
66
|
+
code({ text, lang }) {
|
|
67
|
+
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
68
|
+
let highlighted;
|
|
69
|
+
try {
|
|
70
|
+
highlighted = hljs.highlight(text, { language }).value;
|
|
71
|
+
} catch {
|
|
72
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
73
|
+
}
|
|
74
|
+
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>`;
|
|
75
|
+
},
|
|
76
|
+
link({ href, text }) {
|
|
77
|
+
const isExternal = href && (href.startsWith("http://") || href.startsWith("https://"));
|
|
78
|
+
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
79
|
+
return `<a href="${href}"${target}>${text}</a>`;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
36
83
|
|
|
37
84
|
const emit = defineEmits(["refresh"]);
|
|
38
85
|
const skills = ref([]);
|
|
39
86
|
const loading = ref(false);
|
|
40
87
|
|
|
88
|
+
// README
|
|
89
|
+
const readmeContent = ref(null);
|
|
90
|
+
const renderedReadme = ref("");
|
|
91
|
+
|
|
92
|
+
// 详情弹窗
|
|
93
|
+
const detailVisible = ref(false);
|
|
94
|
+
const detailSkill = ref(null);
|
|
95
|
+
const detailLoading = ref(false);
|
|
96
|
+
const detailContent = ref(null);
|
|
97
|
+
const renderedDetail = ref("");
|
|
98
|
+
|
|
99
|
+
/** 去掉 YAML frontmatter(--- 包裹的元数据) */
|
|
100
|
+
function stripFrontmatter(text) {
|
|
101
|
+
if (!text) return text;
|
|
102
|
+
return text.replace(/^---[\s\S]*?---\s*/, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 渲染 markdown,返回 HTML */
|
|
106
|
+
function renderMd(text) {
|
|
107
|
+
if (!text) return "";
|
|
108
|
+
const body = stripFrontmatter(text);
|
|
109
|
+
if (!body.trim()) return "";
|
|
110
|
+
try {
|
|
111
|
+
return marked.parse(body);
|
|
112
|
+
} catch {
|
|
113
|
+
return body;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
41
117
|
async function loadSkills() {
|
|
42
118
|
loading.value = true;
|
|
43
119
|
try {
|
|
@@ -48,15 +124,53 @@ async function loadSkills() {
|
|
|
48
124
|
}
|
|
49
125
|
}
|
|
50
126
|
|
|
127
|
+
async function loadReadme() {
|
|
128
|
+
try {
|
|
129
|
+
const res = await getReadme();
|
|
130
|
+
if (res.content) {
|
|
131
|
+
readmeContent.value = res.content;
|
|
132
|
+
renderedReadme.value = renderMd(res.content);
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// 静默失败
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function showSkillDetail(skill) {
|
|
140
|
+
detailSkill.value = skill;
|
|
141
|
+
detailVisible.value = true;
|
|
142
|
+
detailLoading.value = true;
|
|
143
|
+
detailContent.value = null;
|
|
144
|
+
renderedDetail.value = "";
|
|
145
|
+
try {
|
|
146
|
+
const res = await getSkillReadme(skill.name);
|
|
147
|
+
if (res.content) {
|
|
148
|
+
detailContent.value = res.content;
|
|
149
|
+
await nextTick();
|
|
150
|
+
renderedDetail.value = renderMd(res.content);
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
detailContent.value = null;
|
|
154
|
+
} finally {
|
|
155
|
+
detailLoading.value = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
51
159
|
// 首次加载
|
|
52
|
-
onMounted(
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
loadSkills();
|
|
162
|
+
loadReadme();
|
|
163
|
+
});
|
|
53
164
|
|
|
54
165
|
// keep-alive 切回来时自动刷新
|
|
55
166
|
onActivated(loadSkills);
|
|
56
167
|
|
|
57
168
|
// 从其他窗口切回浏览器时自动刷新
|
|
58
169
|
function onFocus() {
|
|
59
|
-
if (document.visibilityState === "visible")
|
|
170
|
+
if (document.visibilityState === "visible") {
|
|
171
|
+
loadSkills();
|
|
172
|
+
loadReadme();
|
|
173
|
+
}
|
|
60
174
|
}
|
|
61
175
|
onMounted(() => document.addEventListener("visibilitychange", onFocus));
|
|
62
176
|
onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
|
|
@@ -74,4 +188,14 @@ onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
|
|
|
74
188
|
font-size: 20px;
|
|
75
189
|
font-weight: 600;
|
|
76
190
|
}
|
|
77
|
-
|
|
191
|
+
|
|
192
|
+
.readme-collapse {
|
|
193
|
+
margin-bottom: 20px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.detail-status {
|
|
197
|
+
display: flex;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
padding: 40px 0;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="readme-view">
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2>CLAUDE.md 文档</h2>
|
|
5
|
+
<n-button size="small" @click="loadClaudeMd">
|
|
6
|
+
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
|
7
|
+
刷新
|
|
8
|
+
</n-button>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<n-spin :show="loading">
|
|
12
|
+
<div v-if="content" class="markdown-content" v-html="rendered" />
|
|
13
|
+
<n-empty v-else description="暂无 CLAUDE.md 文档" />
|
|
14
|
+
</n-spin>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import { ref, onMounted, nextTick } from "vue";
|
|
20
|
+
import { NIcon } from "naive-ui";
|
|
21
|
+
import { RefreshOutline } from "@vicons/ionicons5";
|
|
22
|
+
import { marked } from "marked";
|
|
23
|
+
import hljs from "highlight.js";
|
|
24
|
+
import { getClaudeMd } from "../api/skills";
|
|
25
|
+
|
|
26
|
+
// marked 配置:代码高亮 + 外链安全
|
|
27
|
+
marked.use({
|
|
28
|
+
renderer: {
|
|
29
|
+
code({ text, lang }) {
|
|
30
|
+
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
31
|
+
let highlighted;
|
|
32
|
+
try {
|
|
33
|
+
highlighted = hljs.highlight(text, { language }).value;
|
|
34
|
+
} catch {
|
|
35
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
36
|
+
}
|
|
37
|
+
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>`;
|
|
38
|
+
},
|
|
39
|
+
link({ href, text }) {
|
|
40
|
+
const isExternal = href && (href.startsWith("http://") || href.startsWith("https://"));
|
|
41
|
+
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
42
|
+
return `<a href="${href}"${target}>${text}</a>`;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const loading = ref(false);
|
|
48
|
+
const content = ref(null);
|
|
49
|
+
const rendered = ref("");
|
|
50
|
+
|
|
51
|
+
function stripFrontmatter(text) {
|
|
52
|
+
if (!text) return text;
|
|
53
|
+
return text.replace(/^---[\s\S]*?---\s*/, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderMd(text) {
|
|
57
|
+
if (!text) return "";
|
|
58
|
+
const body = stripFrontmatter(text);
|
|
59
|
+
if (!body.trim()) return "";
|
|
60
|
+
try {
|
|
61
|
+
return marked.parse(body);
|
|
62
|
+
} catch {
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadClaudeMd() {
|
|
68
|
+
loading.value = true;
|
|
69
|
+
try {
|
|
70
|
+
const res = await getClaudeMd();
|
|
71
|
+
if (res.content) {
|
|
72
|
+
content.value = res.content;
|
|
73
|
+
await nextTick();
|
|
74
|
+
rendered.value = renderMd(res.content);
|
|
75
|
+
} else {
|
|
76
|
+
content.value = null;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
content.value = null;
|
|
80
|
+
} finally {
|
|
81
|
+
loading.value = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onMounted(loadClaudeMd);
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<style scoped>
|
|
89
|
+
.page-header {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
margin-bottom: 20px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.page-header h2 {
|
|
97
|
+
font-size: 20px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="scene-switch">
|
|
3
|
+
<!-- 技能说明文档 -->
|
|
4
|
+
<n-collapse v-if="skillsReadmeContent" :default-expanded-names="['skills-readme']" class="readme-collapse">
|
|
5
|
+
<n-collapse-item title="技能说明" name="skills-readme">
|
|
6
|
+
<div class="markdown-content" v-html="renderedSkillsReadme" />
|
|
7
|
+
</n-collapse-item>
|
|
8
|
+
</n-collapse>
|
|
9
|
+
|
|
3
10
|
<div class="page-header">
|
|
4
11
|
<h2>场景切换</h2>
|
|
5
12
|
<n-button v-if="!readonly" size="small" type="primary" @click="openCreate">
|
|
@@ -23,7 +30,11 @@
|
|
|
23
30
|
<n-button size="tiny" quaternary @click="openEdit(profile)">
|
|
24
31
|
<template #icon><n-icon size="16"><PencilOutline /></n-icon></template>
|
|
25
32
|
</n-button>
|
|
26
|
-
<n-popconfirm
|
|
33
|
+
<n-popconfirm
|
|
34
|
+
:negative-text="'取消'"
|
|
35
|
+
:positive-text="'确认删除'"
|
|
36
|
+
@positive-click="onDelete(profile)"
|
|
37
|
+
>
|
|
27
38
|
<template #trigger>
|
|
28
39
|
<n-button size="tiny" quaternary>
|
|
29
40
|
<template #icon><n-icon size="16"><TrashOutline /></n-icon></template>
|
|
@@ -49,7 +60,7 @@
|
|
|
49
60
|
</n-tag>
|
|
50
61
|
|
|
51
62
|
<n-button
|
|
52
|
-
v-if="profile.id !== activeId"
|
|
63
|
+
v-if="profile.id !== activeId && profile.skills !== null"
|
|
53
64
|
size="small"
|
|
54
65
|
type="primary"
|
|
55
66
|
:loading="applying === profile.id"
|
|
@@ -57,12 +68,38 @@
|
|
|
57
68
|
>
|
|
58
69
|
应用
|
|
59
70
|
</n-button>
|
|
71
|
+
<n-button
|
|
72
|
+
v-if="profile.id !== activeId && profile.skills === null"
|
|
73
|
+
size="small"
|
|
74
|
+
@click="openCustomDialog"
|
|
75
|
+
>
|
|
76
|
+
自定义勾选
|
|
77
|
+
</n-button>
|
|
60
78
|
</div>
|
|
61
79
|
</template>
|
|
62
80
|
</n-card>
|
|
63
81
|
</div>
|
|
64
82
|
</n-spin>
|
|
65
83
|
|
|
84
|
+
<!-- 自定义勾选弹窗 -->
|
|
85
|
+
<n-modal v-model:show="showCustom" title="自定义技能组合" preset="card" style="width: 480px">
|
|
86
|
+
<n-checkbox-group v-model:value="customSelected">
|
|
87
|
+
<n-space vertical>
|
|
88
|
+
<n-checkbox
|
|
89
|
+
v-for="skill in allSkills"
|
|
90
|
+
:key="skill.name"
|
|
91
|
+
:value="skill.name"
|
|
92
|
+
:label="skill.name"
|
|
93
|
+
/>
|
|
94
|
+
</n-space>
|
|
95
|
+
</n-checkbox-group>
|
|
96
|
+
<template #footer>
|
|
97
|
+
<n-button type="primary" :loading="customLoading" @click="onApplyCustom">
|
|
98
|
+
确认安装
|
|
99
|
+
</n-button>
|
|
100
|
+
</template>
|
|
101
|
+
</n-modal>
|
|
102
|
+
|
|
66
103
|
<!-- 编辑弹窗 -->
|
|
67
104
|
<n-modal
|
|
68
105
|
v-model:show="showEditor"
|
|
@@ -134,17 +171,46 @@ import { ref, onMounted, computed } from "vue";
|
|
|
134
171
|
import { useMessage } from "naive-ui";
|
|
135
172
|
import { NIcon } from "naive-ui";
|
|
136
173
|
import { AddOutline, PencilOutline, TrashOutline } from "@vicons/ionicons5";
|
|
174
|
+
import { marked } from "marked";
|
|
175
|
+
import hljs from "highlight.js";
|
|
137
176
|
import {
|
|
138
177
|
getProfiles,
|
|
139
178
|
applyProfile,
|
|
140
179
|
getSkills,
|
|
141
180
|
saveProfile,
|
|
142
181
|
deleteProfile,
|
|
182
|
+
installSkills,
|
|
183
|
+
getSkillsReadme,
|
|
143
184
|
} from "../api/skills";
|
|
144
185
|
|
|
186
|
+
// marked 配置:代码高亮 + 外链安全
|
|
187
|
+
marked.use({
|
|
188
|
+
renderer: {
|
|
189
|
+
code({ text, lang }) {
|
|
190
|
+
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
191
|
+
let highlighted;
|
|
192
|
+
try {
|
|
193
|
+
highlighted = hljs.highlight(text, { language }).value;
|
|
194
|
+
} catch {
|
|
195
|
+
highlighted = hljs.highlightAuto(text).value;
|
|
196
|
+
}
|
|
197
|
+
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>`;
|
|
198
|
+
},
|
|
199
|
+
link({ href, text }) {
|
|
200
|
+
const isExternal = href && (href.startsWith("http://") || href.startsWith("https://"));
|
|
201
|
+
const target = isExternal ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
202
|
+
return `<a href="${href}"${target}>${text}</a>`;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
145
207
|
const emit = defineEmits(["refresh"]);
|
|
146
208
|
const message = useMessage();
|
|
147
209
|
|
|
210
|
+
// 技能说明 README
|
|
211
|
+
const skillsReadmeContent = ref(null);
|
|
212
|
+
const renderedSkillsReadme = ref("");
|
|
213
|
+
|
|
148
214
|
const profiles = ref([]);
|
|
149
215
|
const activeId = ref(null);
|
|
150
216
|
const allSkills = ref([]);
|
|
@@ -152,6 +218,11 @@ const loading = ref(false);
|
|
|
152
218
|
const applying = ref(null);
|
|
153
219
|
const readonly = ref(true);
|
|
154
220
|
|
|
221
|
+
// 自定义勾选
|
|
222
|
+
const showCustom = ref(false);
|
|
223
|
+
const customSelected = ref([]);
|
|
224
|
+
const customLoading = ref(false);
|
|
225
|
+
|
|
155
226
|
// 编辑器
|
|
156
227
|
const showEditor = ref(false);
|
|
157
228
|
const editingProfile = ref(null);
|
|
@@ -166,6 +237,34 @@ const formData = ref({
|
|
|
166
237
|
// 过滤掉不可见的场景(如已有内置)
|
|
167
238
|
const filteredProfiles = computed(() => profiles.value);
|
|
168
239
|
|
|
240
|
+
function stripFrontmatter(text) {
|
|
241
|
+
if (!text) return text;
|
|
242
|
+
return text.replace(/^---[\s\S]*?---\s*/, "");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renderMd(text) {
|
|
246
|
+
if (!text) return "";
|
|
247
|
+
const body = stripFrontmatter(text);
|
|
248
|
+
if (!body.trim()) return "";
|
|
249
|
+
try {
|
|
250
|
+
return marked.parse(body);
|
|
251
|
+
} catch {
|
|
252
|
+
return body;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function loadSkillsReadme() {
|
|
257
|
+
try {
|
|
258
|
+
const res = await getSkillsReadme();
|
|
259
|
+
if (res.content) {
|
|
260
|
+
skillsReadmeContent.value = res.content;
|
|
261
|
+
renderedSkillsReadme.value = renderMd(res.content);
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// 静默失败
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
169
268
|
async function loadData() {
|
|
170
269
|
loading.value = true;
|
|
171
270
|
try {
|
|
@@ -201,6 +300,28 @@ function openEdit(profile) {
|
|
|
201
300
|
showEditor.value = true;
|
|
202
301
|
}
|
|
203
302
|
|
|
303
|
+
function openCustomDialog() {
|
|
304
|
+
customSelected.value = allSkills.value.filter((s) => s.enabled).map((s) => s.name);
|
|
305
|
+
showCustom.value = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function onApplyCustom() {
|
|
309
|
+
customLoading.value = true;
|
|
310
|
+
try {
|
|
311
|
+
const res = await installSkills(customSelected.value);
|
|
312
|
+
if (res.ok) {
|
|
313
|
+
message.success(`已安装 ${customSelected.value.length} 个技能`);
|
|
314
|
+
activeId.value = "custom";
|
|
315
|
+
showCustom.value = false;
|
|
316
|
+
emit("refresh");
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
message.error("安装失败");
|
|
320
|
+
} finally {
|
|
321
|
+
customLoading.value = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
204
325
|
async function onSave() {
|
|
205
326
|
if (!formData.value.name.trim()) {
|
|
206
327
|
message.warning("请填写场景名称");
|
|
@@ -262,7 +383,10 @@ async function onApply(profile) {
|
|
|
262
383
|
}
|
|
263
384
|
}
|
|
264
385
|
|
|
265
|
-
onMounted(
|
|
386
|
+
onMounted(() => {
|
|
387
|
+
loadData();
|
|
388
|
+
loadSkillsReadme();
|
|
389
|
+
});
|
|
266
390
|
</script>
|
|
267
391
|
|
|
268
392
|
<style scoped>
|
|
@@ -278,6 +402,10 @@ onMounted(loadData);
|
|
|
278
402
|
font-weight: 600;
|
|
279
403
|
}
|
|
280
404
|
|
|
405
|
+
.readme-collapse {
|
|
406
|
+
margin-bottom: 20px;
|
|
407
|
+
}
|
|
408
|
+
|
|
281
409
|
.scene-grid {
|
|
282
410
|
display: grid;
|
|
283
411
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
@@ -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
|
}
|