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.
@@ -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(loadSkills);
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") loadSkills();
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
- </style>
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 @positive-click="onDelete(profile)">
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(loadData);
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
  }