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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdk-skills",
3
- "version": "2.2.12",
3
+ "version": "2.2.14",
4
4
  "description": "mdk-engineer - 沉稳靠谱的前端开发助手 Claude Skills 配置包,一键注入 .claude/ 技能目录和 CLAUDE.md 人设配置",
5
5
  "author": "XiaoMa",
6
6
  "license": "MIT",
@@ -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-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
-
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>