mdk-skills 2.3.12 → 2.3.13

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>mdk-skills 管理面板</title>
7
- <script type="module" crossorigin src="/assets/index-D3otR9pd.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BB6MUl8O.css">
7
+ <script type="module" crossorigin src="/assets/index-pHDOtonU.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DdbAYqm7.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="app"></div>
@@ -121,6 +121,19 @@ function cleanNpxTemp() {
121
121
  }
122
122
  }
123
123
 
124
+ // pull 缓存:预览克隆到临时目录后暂存,拉取时直接复用
125
+ const pullCache = new Map(); // url → { tmpDir, createdAt }
126
+
127
+ function cleanPullCache() {
128
+ const now = Date.now();
129
+ for (const [url, entry] of pullCache) {
130
+ if (now - entry.createdAt > 10 * 60 * 1000) {
131
+ try { fs.rmSync(entry.tmpDir, { recursive: true, force: true }); } catch {}
132
+ pullCache.delete(url);
133
+ }
134
+ }
135
+ }
136
+
124
137
  // 计算技能目录的文件指纹(基于所有文件内容 md5),用于检测是否有真实变更
125
138
  function calcSkillFingerprint(dir) {
126
139
  if (!fs.existsSync(dir)) return "";
@@ -438,95 +451,76 @@ async function handleApi(req, res) {
438
451
  const result = installSelectedSkills(selected);
439
452
  return sendJSON(res, { ok: true, ...result });
440
453
  }
441
- // POST /api/skills/preview预览远程仓库的可用技能(只列举,不安装)
442
- if (method === "POST" && pathname === "/api/skills/preview") {
454
+ // POST /api/skills/pull智能路由:预览或拉取远程技能
455
+ // - names 为空 预览模式:克隆到临时目录并缓存,返回技能列表
456
+ // - names 非空 → 拉取模式:从缓存读取 tmpDir,复制选中技能
457
+ if (method === "POST" && pathname === "/api/skills/pull") {
443
458
  const body = await parseBody(req);
444
459
  const url = (body.url || "").trim();
445
460
  if (!url) return sendJSON(res, { error: "请输入仓库地址" }, 400);
446
- const tmpDir = path.join(os.tmpdir(), "mdk-preview-" + Date.now());
447
- fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
448
- let allSkills = [];
449
- try {
450
- const result = await runAsync("npx --yes skills add \"" + url + "\" -l --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-preview" });
451
- // 从 stdout 解析技能名
452
- // 实际输出格式类似:
453
- // │ skill-name-here
454
- // 可能带 ANSI 颜色码,先去掉
455
- const stdout = (result.stdout || "").replace(/\x1b\[[0-9;]*m/g, "");
456
- const seen = new Set();
457
- for (const line of stdout.split("\n")) {
458
- // 匹配 "│ skillname" — 竖线 + 4空格 + 技能名
459
- const m = line.match(/│\s{4}([a-zA-Z][\w.-]*)/);
460
- if (m && !seen.has(m[1])) {
461
- seen.add(m[1]);
462
- allSkills.push(m[1]);
463
- }
461
+ const names = body.names;
462
+
463
+ // ---------- 预览模式 ----------
464
+ if (!Array.isArray(names) || names.length === 0) {
465
+ cleanPullCache();
466
+ // 已有缓存则直接返回
467
+ const cached = pullCache.get(url);
468
+ if (cached) {
469
+ const skillsDir = path.join(cached.tmpDir, ".claude", "skills");
470
+ const skills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
471
+ return sendJSON(res, { ok: true, skills, total: skills.length });
464
472
  }
465
- // 也试读临时目录,作为补充(-l 可能不会复制到 tmpDir 但备着)
466
- const skillsDir = path.join(tmpDir, ".claude", "skills");
467
- if (fs.existsSync(skillsDir)) {
468
- for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
469
- if (entry.isDirectory() && fs.existsSync(path.join(skillsDir, entry.name, "SKILL.md")) && !seen.has(entry.name)) {
470
- seen.add(entry.name);
471
- allSkills.push(entry.name);
472
- }
473
- }
473
+ const tmpDir = path.join(os.tmpdir(), "mdk-preview-" + Date.now());
474
+ fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
475
+ try {
476
+ await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code", { cwd: tmpDir, taskId: "npx-pull-preview" });
477
+ } catch (e) {
478
+ cleanNpxTemp();
479
+ fs.rmSync(tmpDir, { recursive: true, force: true });
480
+ if (e.killed) return sendJSON(res, { cancelled: true }, 499);
481
+ return sendJSON(res, { error: "预览失败:" + (e.stderr || e.message) }, 400);
474
482
  }
475
- } catch (e) {
476
483
  cleanNpxTemp();
477
- fs.rmSync(tmpDir, { recursive: true, force: true });
478
- if (e.killed) return sendJSON(res, { cancelled: true }, 499);
479
- return sendJSON(res, { error: "预览失败:" + (e.stderr || e.message) }, 400);
484
+ const skillsDir = path.join(tmpDir, ".claude", "skills");
485
+ const skills = fs.existsSync(skillsDir) ? core.listSkillDirs(skillsDir) : [];
486
+ if (skills.length === 0) {
487
+ fs.rmSync(tmpDir, { recursive: true, force: true });
488
+ return sendJSON(res, { error: "未找到有效技能" }, 400);
489
+ }
490
+ // 缓存,不删 tmpDir
491
+ pullCache.set(url, { tmpDir, createdAt: Date.now() });
492
+ return sendJSON(res, { ok: true, skills, total: skills.length });
480
493
  }
481
- cleanNpxTemp();
482
- fs.rmSync(tmpDir, { recursive: true, force: true });
483
- return sendJSON(res, { ok: true, skills: allSkills, total: allSkills.length });
484
- }
485
494
 
486
- // POST /api/skills/pull — 从远程仓库拉取技能(支持选择性拉取)
487
- if (method === "POST" && pathname === "/api/skills/pull") {
488
- const body = await parseBody(req);
489
- const url = (body.url || "").trim();
490
- if (!url) return sendJSON(res, { error: "请输入仓库地址" }, 400);
491
- const tmpDir = path.join(os.tmpdir(), "mdk-pull-" + Date.now());
492
- fs.mkdirSync(path.join(tmpDir, ".claude", "skills"), { recursive: true });
493
- // 选择性拉取:指定 names 时追加 --skill 参数
494
- const names = body.names;
495
- const skillArg = Array.isArray(names) && names.length > 0
496
- ? " --skill " + names.join(",")
497
- : "";
498
- try {
499
- await runAsync("npx --yes skills add \"" + url + "\" --copy -y -a claude-code" + skillArg, { cwd: tmpDir, taskId: "npx-pull" });
500
- } catch (e) {
501
- cleanNpxTemp();
502
- fs.rmSync(tmpDir, { recursive: true, force: true });
503
- if (e.killed) return sendJSON(res, { cancelled: true }, 499);
504
- return sendJSON(res, { error: "拉取失败:" + (e.stderr || e.message) }, 400);
495
+ // ---------- 拉取模式 ----------
496
+ const entry = pullCache.get(url);
497
+ if (!entry) {
498
+ return sendJSON(res, { error: "请先预览该仓库" }, 400);
505
499
  }
506
- cleanNpxTemp();
500
+ const tmpDir = entry.tmpDir;
507
501
  const imported = [];
508
502
  const skipped = [];
509
503
  const skillsDir = path.join(tmpDir, ".claude", "skills");
510
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
511
- for (const entry of entries) {
512
- if (!entry.isDirectory()) continue;
513
- const skillPath = path.join(skillsDir, entry.name);
514
- if (fs.existsSync(path.join(skillPath, "SKILL.md"))) {
515
- const dest = path.join(pkgSkillsSource, entry.name);
516
- if (fs.existsSync(dest)) {
517
- fs.rmSync(dest, { recursive: true, force: true });
518
- }
519
- copyDirSync(skillPath, dest);
520
- writeSkillMeta(dest, false);
521
- addPullSource(entry.name, url);
522
- imported.push(entry.name);
523
- } else {
524
- skipped.push(entry.name);
504
+ for (const name of names) {
505
+ const skillPath = path.join(skillsDir, name);
506
+ if (!fs.existsSync(path.join(skillPath, "SKILL.md"))) {
507
+ skipped.push(name);
508
+ continue;
509
+ }
510
+ const dest = path.join(pkgSkillsSource, name);
511
+ if (fs.existsSync(dest)) {
512
+ fs.rmSync(dest, { recursive: true, force: true });
525
513
  }
514
+ copyDirSync(skillPath, dest);
515
+ writeSkillMeta(dest, false);
516
+ addPullSource(name, url);
517
+ imported.push(name);
526
518
  }
527
- fs.rmSync(tmpDir, { recursive: true, force: true });
519
+ // 清理缓存 + 临时目录
520
+ pullCache.delete(url);
521
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
528
522
  if (imported.length === 0) {
529
- return sendJSON(res, { error: "未找到有效的技能(目录需包含 SKILL.md)", skipped }, 400);
523
+ return sendJSON(res, { error: "所选技能均无效(需包含 SKILL.md)", skipped }, 400);
530
524
  }
531
525
  return sendJSON(res, { ok: true, imported, skipped });
532
526
  }
@@ -109,13 +109,6 @@ export function updateSkillMeta(name, data) {
109
109
  });
110
110
  }
111
111
 
112
- export function previewSkills(url) {
113
- return request("/skills/preview", {
114
- method: "POST",
115
- body: JSON.stringify({ url }),
116
- });
117
- }
118
-
119
112
  export function pullSkills(url, names = []) {
120
113
  return request("/skills/pull", {
121
114
  method: "POST",
@@ -328,7 +328,7 @@ import { marked } from "marked";
328
328
  import hljs from "highlight.js";
329
329
  import SkillCard from "../components/SkillCard.vue";
330
330
  import ModalComp from "../components/ModalComp.vue";
331
- import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, previewSkills, pullSkills, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir, cancelTask } from "../api/skills";
331
+ import { getSkills, getReadme, getSkillReadme, updateSkillMeta, deleteSkill, pullSkills, installSkills, getSkillSource, updateSkill, batchUpdateSkills, openSkillDir, cancelTask } from "../api/skills";
332
332
  import { sortSkills, getUsageMap, recordUsage } from "../utils/usage";
333
333
 
334
334
  // marked 配置:代码高亮 + 外链安全
@@ -477,7 +477,7 @@ async function handlePreview() {
477
477
  pullResult.value = null;
478
478
  pullError.value = "";
479
479
  try {
480
- const res = await previewSkills(pullUrl.value.trim());
480
+ const res = await pullSkills(pullUrl.value.trim());
481
481
  if (!showPullModal.value) return;
482
482
  if (res.ok) {
483
483
  previewSkillList.value = res.skills || [];