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 +15 -2
- package/client.js +9 -4
- package/package.json +1 -1
- package/src/extra-sources.js +8 -6
- package/src/http.js +89 -0
- package/src/sources.js +55 -4
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
|
+

|
|
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
|
-
| `
|
|
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"
|
|
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
|
-
:
|
|
180
|
-
?
|
|
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.
|
|
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",
|
package/src/extra-sources.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
60
|
-
// box is a legitimate state, so
|
|
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
|
-
|
|
339
|
+
: hints.length
|
|
340
|
+
? hints
|
|
341
|
+
: ["所选市场暂时没有取到数据,可能是上游不可达或响应超时,请稍后重试"];
|
|
342
|
+
return { items, degraded, hints, sourceErrors, total: items.length };
|
|
292
343
|
}
|
|
293
344
|
|
|
294
345
|
/**
|