dsh-skill-mcp-market 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -7,6 +7,18 @@ DSH 的**技能市场**与 **MCP 连接器市场**:浏览实时技能源、装
7
7
  - 默认技能目录:`~/.dsh/market/skills`;默认 MCP 目录:`~/.dsh/market/mcp`
8
8
  - 左侧面板:**「SKILL技能&MCP连接器」**
9
9
 
10
+ ## 截图
11
+
12
+ 真机截图(DSH 桌面版,左侧菜单「SKILL技能&MCP连接器」)。
13
+
14
+ ### 技能市场
15
+
16
+ 顶部按来源切换(`skills.sh` / `clawhub` / `skillsmp` / `trusttools` / `browse.sh` / `lobehub-agent` / `skillhub`),卡片上直接「预览 / 安装」:
17
+
18
+ ![技能市场:来源筛选与卡片](docs/screenshots/panel-skills.png)
19
+
20
+ > 「MCP 连接器」「已安装」「路径」三个页签的截图待补。
21
+
10
22
  ## 安装
11
23
 
12
24
  已发布到 npm:**`dsh-skill-mcp-market`**(MIT,公共包,无需登录)。
@@ -161,10 +173,11 @@ DSH 本来就有技能运行时和 MCP 运行时,缺的只是市场这一层
161
173
  | 源 | 说明 |
162
174
  |---|---|
163
175
  | **MCP 官方注册表** | `registry.modelcontextprotocol.io`,免费、实时的 MCP 目录,自带真实启动方式(npm / streamable-http) |
164
- | `skills.sh` | 技能索引,支持页面 URL 与裸 `owner/repo` |
176
+ | `skills.sh` | 技能索引,支持页面 URL 与裸 `owner/repo`;搜索接口要求 ≥2 字符,空搜索回退到首页推荐 |
165
177
  | `skillsmp` | 带反爬(Cloudflare),走 python→curl→fetch 三级取数 |
166
178
  | `browse.sh` | 启动预热 + 内存缓存,冷启动超时不缓存空结果 |
167
- | `clawhub` / `trusttools` / `lobehub-agent` / `skillhub` | 其余实时技能源 |
179
+ | `trusttools` | `ai.trusttools.cn/api/skills/search`(POST);该站会丢弃本进程的 TLS 握手,走 python → curl → fetch 三级通道 |
180
+ | `clawhub` / `lobehub-agent` / `skillhub` | 其余实时技能源 |
168
181
  | `github` | 直接给仓库或 `@ref`;jsDelivr 列树失败时回退 GitHub contents API 与 codeload tarball |
169
182
 
170
183
  ## 目录结构
package/client.js CHANGED
@@ -172,14 +172,19 @@ window.__ModuleLoader__.load({
172
172
  pageSize: (options && options.size) || 24,
173
173
  });
174
174
  setSkills(d.items || []);
175
+ const hints = (d.hints || []).filter(Boolean);
175
176
  if (!(d.items || []).length) {
176
- // An empty market is either "no match" or "upstream refused us"; say which.
177
+ // An empty market is either "no match", "needs a keyword" or "upstream refused us".
177
178
  const reasons = (d.sourceErrors || []).length
178
179
  ? d.sourceErrors
179
- : (d.degraded || []).length
180
- ? [`部分来源不可用:${d.degraded.map((x) => x.source).join(', ')}`]
181
- : ['没有找到匹配的技能。'];
180
+ : hints.length
181
+ ? hints
182
+ : (d.degraded || []).length
183
+ ? [`部分来源不可用:${d.degraded.map((x) => x.source).join(', ')}`]
184
+ : ['没有找到匹配的技能。'];
182
185
  setNotice(reasons.join(';'));
186
+ } else if (hints.length) {
187
+ setNotice(hints.join(';'));
183
188
  } else if ((d.degraded || []).length) {
184
189
  setNotice(`部分来源不可用:${d.degraded.map((x) => x.source).join(', ')}`);
185
190
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-skill-mcp-market",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "DSH 技能市场与 MCP 连接器市场:浏览实时技能源,一键安装 SKILL.md 技能与 MCP 连接器,装完即刻可用、无需重启。SKILL & MCP marketplace for DeepSeek Harness — browse live sources, install skills and MCP connectors, effective immediately.",
6
6
  "license": "MIT",
@@ -5,7 +5,7 @@
5
5
  // skillsmp — skillsmp.com/api/skills (paged search; Cloudflare-fronted, needs the curl
6
6
  // transport because this process's own TLS fingerprint is refused)
7
7
  // modelscope (api/v1/dolphin/skills → 404) is intentionally absent: the endpoint is gone.
8
- import { getJsonEscalating } from "./http.js";
8
+ import { getJsonEscalating, postJsonEscalating } from "./http.js";
9
9
 
10
10
  const UA = { "User-Agent": "dsh-skill-mcp-market/0.4", accept: "application/json" };
11
11
 
@@ -60,11 +60,13 @@ export async function searchClawHub({ query = "", pageSize = 24 }) {
60
60
 
61
61
  // ---------------------------------------------------------------- trusttools
62
62
  export async function searchTrustTools({ query = "", pageSize = 24 }) {
63
- const data = await postJson("https://ai.trusttools.cn/api/skills/search", {
64
- q: query,
65
- page: 1,
66
- pageSize,
67
- });
63
+ // ai.trusttools.cn drops this process's TLS handshake (same class of failure as
64
+ // skillsmp), so the POST goes through python → curl → fetch instead of plain fetch.
65
+ const data = await postJsonEscalating(
66
+ "https://ai.trusttools.cn/api/skills/search",
67
+ { q: query, page: 1, pageSize },
68
+ { timeoutMs: 25000 },
69
+ );
68
70
  const rows = Array.isArray(data.data) ? data.data : [];
69
71
  const items = rows.map((raw) => {
70
72
  const repo = String(raw.repo_url || "").replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "");
package/src/http.js CHANGED
@@ -107,6 +107,14 @@ const PY_FETCH = [
107
107
  " sys.stdout.write(response.read().decode('utf-8', 'replace'))",
108
108
  ].join("\n");
109
109
 
110
+ const PY_POST = [
111
+ "import sys, json, urllib.request",
112
+ "url, body, headers, timeout = sys.argv[1], sys.argv[2], json.loads(sys.argv[3]), float(sys.argv[4])",
113
+ "request = urllib.request.Request(url, data=body.encode('utf-8'), headers=headers, method='POST')",
114
+ "with urllib.request.urlopen(request, timeout=timeout) as response:",
115
+ " sys.stdout.write(response.read().decode('utf-8', 'replace'))",
116
+ ].join("\n");
117
+
110
118
  async function isPython(binary) {
111
119
  // A relocatable CPython can be present but unusable (its stdlib may be pinned to the
112
120
  // build machine, e.g. "AssertionError: SRE module mismatch"), so the probe imports what
@@ -230,3 +238,84 @@ export async function getJsonEscalating(url, { headers = {}, timeoutMs = 20000 }
230
238
  }
231
239
  throw new Error(errors[0] || "所有传输方式都失败");
232
240
  }
241
+
242
+ /** POST a JSON body through curl (some hosts only answer a non-Node TLS client). */
243
+ export async function curlPostText(url, body, { headers = {}, timeoutMs = 15000 } = {}) {
244
+ const merged = { ...BROWSER_HEADERS, "content-type": "application/json", ...headers };
245
+ const payload = JSON.stringify(body ?? {});
246
+ const args = [
247
+ "-s", "-S", "--compressed",
248
+ "--max-time", String(Math.max(1, Math.ceil(timeoutMs / 1000))),
249
+ "-X", "POST",
250
+ "--data-binary", "@-",
251
+ ];
252
+ for (const [name, value] of Object.entries(merged)) args.push("-H", `${name}: ${value}`);
253
+ args.push(url);
254
+ return new Promise((resolve, reject) => {
255
+ const child = execFile("curl.exe", args, { maxBuffer: 16 * 1024 * 1024, timeout: timeoutMs + 5000, windowsHide: true },
256
+ (error, stdout) => (error ? reject(error) : resolve(String(stdout || ""))));
257
+ if (child.stdin) {
258
+ child.stdin.end(payload);
259
+ }
260
+ });
261
+ }
262
+
263
+ /** POST a JSON body through the Python transport. */
264
+ export async function pythonPostText(url, body, { headers = {}, timeoutMs = 20000 } = {}) {
265
+ const python = await findPython();
266
+ if (!python) throw new Error("没有可用的 Python 解释器,无法通过该传输取数");
267
+ return new Promise((resolve, reject) => {
268
+ execFile(
269
+ python,
270
+ ["-c", PY_POST, url, JSON.stringify(body ?? {}), JSON.stringify({ ...BROWSER_HEADERS, "content-type": "application/json", ...headers }), String(timeoutMs / 1000)],
271
+ {
272
+ timeout: timeoutMs + 5000,
273
+ windowsHide: true,
274
+ maxBuffer: 16 * 1024 * 1024,
275
+ env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" },
276
+ },
277
+ (error, stdout, stderr) => {
278
+ if (error) reject(new Error(`python POST 失败:${String(stderr || error.message).slice(0, 200)}`));
279
+ else resolve(String(stdout));
280
+ },
281
+ );
282
+ });
283
+ }
284
+
285
+ /**
286
+ * POST escalation for hosts whose edge drops this process's TLS handshake: python → curl →
287
+ * Node fetch, first JSON response wins.
288
+ */
289
+ export async function postJsonEscalating(url, body, { headers = {}, timeoutMs = 20000 } = {}) {
290
+ const parse = (text) => {
291
+ try {
292
+ return JSON.parse(text);
293
+ } catch {
294
+ throw new Error(`上游返回的不是 JSON:${String(text).slice(0, 80).replace(/\s+/g, " ")}`);
295
+ }
296
+ };
297
+ const attempts = [
298
+ async () => parse(await pythonPostText(url, body, { headers, timeoutMs })),
299
+ async () => parse(await curlPostText(url, body, { headers, timeoutMs })),
300
+ async () => {
301
+ const response = await fetch(url, {
302
+ method: "POST",
303
+ headers: { ...BROWSER_HEADERS, "content-type": "application/json", ...headers },
304
+ body: JSON.stringify(body ?? {}),
305
+ signal: AbortSignal.timeout(timeoutMs),
306
+ });
307
+ if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
308
+ return response.json();
309
+ },
310
+ ];
311
+ const errors = [];
312
+ for (const attempt of attempts) {
313
+ try {
314
+ // eslint-disable-next-line no-await-in-loop
315
+ return await attempt();
316
+ } catch (error) {
317
+ errors.push(String((error && error.message) || error).slice(0, 120));
318
+ }
319
+ }
320
+ throw new Error(errors[0] || "所有传输方式都失败");
321
+ }
package/src/sources.js CHANGED
@@ -54,11 +54,58 @@ export function makeCache(dir) {
54
54
  }
55
55
 
56
56
  // ------------------------------------------------------------------ skills.sh
57
+ /**
58
+ * skills.sh has no listing API, but its home page links the skills it is featuring right
59
+ * now. Scrape those links so an empty search box still shows a real catalogue instead of
60
+ * a blank market.
61
+ */
62
+ export async function browseSkillsSh({ pageSize = 24, cache } = {}) {
63
+ let rows = cache ? await cache.get("skills_sh_home", 30 * 60 * 1000) : null;
64
+ if (!Array.isArray(rows) || rows.length === 0) {
65
+ const html = await fetchText("https://www.skills.sh/", 25000);
66
+ const seen = new Set();
67
+ rows = [];
68
+ for (const match of html.matchAll(/\/([A-Za-z0-9_.-]+)\/skills\/([A-Za-z0-9_.-]+)/g)) {
69
+ const key = `${match[1]}/${match[2]}`;
70
+ if (seen.has(key)) continue;
71
+ seen.add(key);
72
+ rows.push({ owner: `${match[1]}/skills`, slug: match[2] });
73
+ }
74
+ if (rows.length > 0 && cache) await cache.put("skills_sh_home", rows);
75
+ }
76
+ const items = rows.slice(0, pageSize).map(({ owner, slug }) => ({
77
+ id: `skills.sh:${owner}/${slug}`,
78
+ slug,
79
+ name: slug,
80
+ description: "",
81
+ source: "skills.sh",
82
+ sourceLabel: "skills.sh",
83
+ owner,
84
+ homepage: `https://www.skills.sh/${owner}/${slug}`,
85
+ upstreamUrl: owner,
86
+ skillDir: "",
87
+ installs: 0,
88
+ installable: true,
89
+ }));
90
+ return { source: "skills.sh", items, total: rows.length };
91
+ }
92
+
57
93
  /** skills.sh exposes a real JSON search API (Algolia-backed). */
58
94
  export async function searchSkillsSh({ query = "", pageSize = 24, cache }) {
59
- // The upstream API rejects queries shorter than 2 characters; browsing with an empty
60
- // box is a legitimate state, so return an empty page with a hint instead of failing.
95
+ // The upstream search API rejects queries shorter than 2 characters. Browsing with an
96
+ // empty box is a legitimate state, so fall back to the home-page highlights and say so.
61
97
  if (query.trim().length < 2) {
98
+ try {
99
+ const browse = await browseSkillsSh({ pageSize, cache });
100
+ if (browse.items.length > 0) {
101
+ return {
102
+ ...browse,
103
+ hint: "未输入关键词:以下是 skills.sh 首页当前推荐的技能,输入至少 2 个字符可精确搜索",
104
+ };
105
+ }
106
+ } catch {
107
+ // fall through to the plain hint below
108
+ }
62
109
  return { source: "skills.sh", items: [], total: 0, hint: "skills.sh 需要至少 2 个字符的关键词" };
63
110
  }
64
111
  const url = `https://www.skills.sh/api/search?q=${encodeURIComponent(query)}&limit=${pageSize}`;
@@ -244,10 +291,12 @@ export async function searchSkills({ query, source = "all", pageSize = 24, cache
244
291
  const wanted = source === "all" ? ALL_SOURCES : [source];
245
292
  const items = [];
246
293
  const degraded = [];
294
+ const hints = [];
247
295
  for (const name of wanted) {
248
296
  try {
249
297
  if (name === "skills.sh") {
250
298
  const result = await searchSkillsSh({ query, pageSize, cache });
299
+ if (result.hint) hints.push(`skills.sh:${result.hint}`);
251
300
  items.push(...result.items);
252
301
  } else if (name === "clawhub") {
253
302
  const result = await searchClawHub({ query, pageSize });
@@ -287,8 +336,10 @@ export async function searchSkills({ query, source = "all", pageSize = 24, cache
287
336
  ? []
288
337
  : degraded.length
289
338
  ? degraded.map((entry) => `${entry.source} 不可达:${entry.error}`.slice(0, 200))
290
- : ["所选市场暂时没有取到数据,可能是上游不可达或响应超时,请稍后重试"];
291
- return { items, degraded, sourceErrors, total: items.length };
339
+ : hints.length
340
+ ? hints
341
+ : ["所选市场暂时没有取到数据,可能是上游不可达或响应超时,请稍后重试"];
342
+ return { items, degraded, hints, sourceErrors, total: items.length };
292
343
  }
293
344
 
294
345
  /**