mdk-skills 2.2.12 → 2.2.14
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
|
@@ -497,10 +497,21 @@ async function handleApi(req, res) {
|
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
// README.md 缺失状态(独立于骨架文件)
|
|
501
|
+
let readmeStatus = null;
|
|
502
|
+
if (sourcePath) {
|
|
503
|
+
const skillsDir = path.join(sourcePath, ".claude", "skills");
|
|
504
|
+
readmeStatus = {
|
|
505
|
+
root: fs.existsSync(path.join(sourcePath, "README.md")),
|
|
506
|
+
skills: fs.existsSync(path.join(skillsDir, "README.md")),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
500
510
|
return sendJSON(res, {
|
|
501
511
|
connected: !!sourcePath,
|
|
502
512
|
path: sourcePath,
|
|
503
513
|
needsInit,
|
|
514
|
+
readmeStatus,
|
|
504
515
|
});
|
|
505
516
|
}
|
|
506
517
|
|
|
@@ -1,201 +1,201 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="dashboard">
|
|
3
|
-
<!-- 仓库 README -->
|
|
4
|
-
<n-collapse v-
|
|
5
|
-
<n-collapse-item title="仓库说明" name="readme">
|
|
6
|
-
<div class="markdown-content" v-html="renderedReadme" />
|
|
7
|
-
</n-collapse-item>
|
|
8
|
-
</n-collapse>
|
|
9
|
-
|
|
10
|
-
<div class="page-header">
|
|
11
|
-
<h2>技能列表</h2>
|
|
12
|
-
<n-button size="small" @click="loadSkills">
|
|
13
|
-
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
|
14
|
-
刷新
|
|
15
|
-
</n-button>
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<n-spin :show="loading">
|
|
19
|
-
<SkillCard
|
|
20
|
-
v-for="skill in skills"
|
|
21
|
-
:key="skill.name"
|
|
22
|
-
:skill="skill"
|
|
23
|
-
@refresh="loadSkills"
|
|
24
|
-
@click="showSkillDetail(skill)"
|
|
25
|
-
/>
|
|
26
|
-
</n-spin>
|
|
27
|
-
|
|
28
|
-
<n-empty v-if="!loading && skills.length === 0" description="暂无技能数据">
|
|
29
|
-
<template #extra>
|
|
30
|
-
<n-button size="small" @click="$router.push({ name: 'Scenes' })">
|
|
31
|
-
前往场景切换
|
|
32
|
-
</n-button>
|
|
33
|
-
</template>
|
|
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>
|
|
51
|
-
</div>
|
|
52
|
-
</template>
|
|
53
|
-
|
|
54
|
-
<script setup>
|
|
55
|
-
import { ref, onMounted, onActivated, onUnmounted, nextTick } from "vue";
|
|
56
|
-
import { NIcon } from "naive-ui";
|
|
57
|
-
import { RefreshOutline } from "@vicons/ionicons5";
|
|
58
|
-
import { marked } from "marked";
|
|
59
|
-
import hljs from "highlight.js";
|
|
60
|
-
import SkillCard from "../components/SkillCard.vue";
|
|
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
|
-
});
|
|
83
|
-
|
|
84
|
-
const emit = defineEmits(["refresh"]);
|
|
85
|
-
const skills = ref([]);
|
|
86
|
-
const loading = ref(false);
|
|
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
|
-
|
|
117
|
-
async function loadSkills() {
|
|
118
|
-
loading.value = true;
|
|
119
|
-
try {
|
|
120
|
-
skills.value = await getSkills();
|
|
121
|
-
emit("refresh");
|
|
122
|
-
} finally {
|
|
123
|
-
loading.value = false;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
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
|
-
|
|
159
|
-
// 首次加载
|
|
160
|
-
onMounted(() => {
|
|
161
|
-
loadSkills();
|
|
162
|
-
loadReadme();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// keep-alive 切回来时自动刷新
|
|
166
|
-
onActivated(loadSkills);
|
|
167
|
-
|
|
168
|
-
// 从其他窗口切回浏览器时自动刷新
|
|
169
|
-
function onFocus() {
|
|
170
|
-
if (document.visibilityState === "visible") {
|
|
171
|
-
loadSkills();
|
|
172
|
-
loadReadme();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
onMounted(() => document.addEventListener("visibilitychange", onFocus));
|
|
176
|
-
onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
<style scoped>
|
|
180
|
-
.page-header {
|
|
181
|
-
display: flex;
|
|
182
|
-
align-items: center;
|
|
183
|
-
justify-content: space-between;
|
|
184
|
-
margin-bottom: 20px;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.page-header h2 {
|
|
188
|
-
font-size: 20px;
|
|
189
|
-
font-weight: 600;
|
|
190
|
-
}
|
|
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
|
-
}
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dashboard">
|
|
3
|
+
<!-- 仓库 README -->
|
|
4
|
+
<n-collapse v-show="readmeContent" :default-expanded-names="['readme']" class="readme-collapse" :animated="false">
|
|
5
|
+
<n-collapse-item title="仓库说明" name="readme">
|
|
6
|
+
<div class="markdown-content" v-html="renderedReadme" />
|
|
7
|
+
</n-collapse-item>
|
|
8
|
+
</n-collapse>
|
|
9
|
+
|
|
10
|
+
<div class="page-header">
|
|
11
|
+
<h2>技能列表</h2>
|
|
12
|
+
<n-button size="small" @click="loadSkills">
|
|
13
|
+
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
|
14
|
+
刷新
|
|
15
|
+
</n-button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<n-spin :show="loading">
|
|
19
|
+
<SkillCard
|
|
20
|
+
v-for="skill in skills"
|
|
21
|
+
:key="skill.name"
|
|
22
|
+
:skill="skill"
|
|
23
|
+
@refresh="loadSkills"
|
|
24
|
+
@click="showSkillDetail(skill)"
|
|
25
|
+
/>
|
|
26
|
+
</n-spin>
|
|
27
|
+
|
|
28
|
+
<n-empty v-if="!loading && skills.length === 0" description="暂无技能数据">
|
|
29
|
+
<template #extra>
|
|
30
|
+
<n-button size="small" @click="$router.push({ name: 'Scenes' })">
|
|
31
|
+
前往场景切换
|
|
32
|
+
</n-button>
|
|
33
|
+
</template>
|
|
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>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup>
|
|
55
|
+
import { ref, onMounted, onActivated, onUnmounted, nextTick } from "vue";
|
|
56
|
+
import { NIcon } from "naive-ui";
|
|
57
|
+
import { RefreshOutline } from "@vicons/ionicons5";
|
|
58
|
+
import { marked } from "marked";
|
|
59
|
+
import hljs from "highlight.js";
|
|
60
|
+
import SkillCard from "../components/SkillCard.vue";
|
|
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
|
+
});
|
|
83
|
+
|
|
84
|
+
const emit = defineEmits(["refresh"]);
|
|
85
|
+
const skills = ref([]);
|
|
86
|
+
const loading = ref(false);
|
|
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
|
+
|
|
117
|
+
async function loadSkills() {
|
|
118
|
+
loading.value = true;
|
|
119
|
+
try {
|
|
120
|
+
skills.value = await getSkills();
|
|
121
|
+
emit("refresh");
|
|
122
|
+
} finally {
|
|
123
|
+
loading.value = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
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
|
+
|
|
159
|
+
// 首次加载
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
loadSkills();
|
|
162
|
+
loadReadme();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// keep-alive 切回来时自动刷新
|
|
166
|
+
onActivated(loadSkills);
|
|
167
|
+
|
|
168
|
+
// 从其他窗口切回浏览器时自动刷新
|
|
169
|
+
function onFocus() {
|
|
170
|
+
if (document.visibilityState === "visible") {
|
|
171
|
+
loadSkills();
|
|
172
|
+
loadReadme();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
onMounted(() => document.addEventListener("visibilitychange", onFocus));
|
|
176
|
+
onUnmounted(() => document.removeEventListener("visibilitychange", onFocus));
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<style scoped>
|
|
180
|
+
.page-header {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: space-between;
|
|
184
|
+
margin-bottom: 20px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.page-header h2 {
|
|
188
|
+
font-size: 20px;
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
}
|
|
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
201
|
</style>
|