dsh-plugin 2.3.4 → 2.3.6
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.en.md +9 -9
- package/README.md +9 -9
- package/client/client.js +315 -315
- package/client/client.js.map +1 -1
- package/lib/http/install.js +1 -1
- package/lib/services/install/npm-resolve.js +10 -4
- package/lib/services/probe.js +31 -8
- package/lib/types/services/install/npm-resolve.d.ts +5 -3
- package/lib/types/services/probe.d.ts +6 -0
- package/package.json +2 -2
- package/src/client/logic/failures.ts +4 -3
- package/src/server/http/install.ts +1 -1
- package/src/server/services/install/npm-resolve.ts +11 -4
- package/src/server/services/probe.ts +33 -8
package/lib/http/install.js
CHANGED
|
@@ -133,7 +133,7 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
133
133
|
// 命中则记录 repo → 包名映射,客户端据此把依赖 key 匹配回仓库、列表立即显示「已安装」。
|
|
134
134
|
const catalogRepo = source === 'catalog' ? githubRepoOf(displayRepo) : null;
|
|
135
135
|
if (catalogRepo !== null) {
|
|
136
|
-
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry, effectiveProxy);
|
|
136
|
+
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry, effectiveProxy, pinVersion);
|
|
137
137
|
if (catalogTargetIsDsh)
|
|
138
138
|
recordResolvedNpmPackage(profile, catalogRepo, rawRepo);
|
|
139
139
|
}
|
|
@@ -25,6 +25,12 @@ function repositoryUrlOf(pkg) {
|
|
|
25
25
|
}
|
|
26
26
|
return String(pkg.links?.repository ?? '');
|
|
27
27
|
}
|
|
28
|
+
/** registry 路径:指定版本时取该版本的 manifest,否则取 latest。 */
|
|
29
|
+
export function npmMetadataPath(packageName, version = '') {
|
|
30
|
+
const normalized = version.trim().replace(/^v/i, '');
|
|
31
|
+
const encoded = encodeURIComponent(packageName);
|
|
32
|
+
return normalized === '' ? `${encoded}/latest` : `${encoded}/${encodeURIComponent(normalized)}`;
|
|
33
|
+
}
|
|
28
34
|
export function isDshPackageMetadataForRepo(pkg, repo) {
|
|
29
35
|
if (githubRepoOf(repositoryUrlOf(pkg))?.toLowerCase() !== repo.toLowerCase())
|
|
30
36
|
return false;
|
|
@@ -34,14 +40,14 @@ export function isDshPackageMetadataForRepo(pkg, repo) {
|
|
|
34
40
|
return keywordMarked || manifestMarked;
|
|
35
41
|
}
|
|
36
42
|
/**
|
|
37
|
-
* 校验包名对应的 npm 包确实属于该仓库且声明为 DSH 插件:取 registry
|
|
38
|
-
*
|
|
43
|
+
* 校验包名对应的 npm 包确实属于该仓库且声明为 DSH 插件:取 registry 上指定版本(缺省 `latest`)
|
|
44
|
+
* 的元数据比对 repository 与插件标记。网络异常、超时、非 200 一律返回 false,不阻塞调用方。
|
|
39
45
|
*/
|
|
40
|
-
export function isDshNpmPackageForRepo(packageName, repo, registry = '', proxy = '') {
|
|
46
|
+
export function isDshNpmPackageForRepo(packageName, repo, registry = '', proxy = '', version = '') {
|
|
41
47
|
if (packageName === '' || repo === '')
|
|
42
48
|
return Promise.resolve(false);
|
|
43
49
|
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '');
|
|
44
|
-
const url = `${base}/${
|
|
50
|
+
const url = `${base}/${npmMetadataPath(packageName, version)}`;
|
|
45
51
|
return new Promise((resolve) => {
|
|
46
52
|
const req = get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
47
53
|
if (res.statusCode !== 200) {
|
package/lib/services/probe.js
CHANGED
|
@@ -30,6 +30,18 @@ export function curlTlsArgs() {
|
|
|
30
30
|
export function curlRevokeFlagUnsupported(code, stderr) {
|
|
31
31
|
return code === 2 && /ssl-revoke-best-effort/i.test(stderr) && /unknown|unrecognized/i.test(stderr);
|
|
32
32
|
}
|
|
33
|
+
/** curl 对某个可选参数报「不认识 / 不支持」:退出码 2 且 stderr 指向该参数。 */
|
|
34
|
+
function curlOptionUnsupported(code, stderr, option) {
|
|
35
|
+
return code === 2 && stderr.includes(option) && /unknown|unrecognized|support/i.test(stderr);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 本机 curl 未编入 zlib:连 --compressed 这个选项都不认识,参数解析阶段即以退出码 2
|
|
39
|
+
* 结束(stderr 形如 `curl: option --compressed: the installed libcurl version doesn't
|
|
40
|
+
* support this`)。必须整组去掉压缩协商后重跑,否则目录拉取每次都失败、市场 502。
|
|
41
|
+
*/
|
|
42
|
+
export function curlCompressedUnsupported(code, stderr) {
|
|
43
|
+
return curlOptionUnsupported(code, stderr, '--compressed');
|
|
44
|
+
}
|
|
33
45
|
/** 彻底关闭证书吊销检查的保底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
|
|
34
46
|
const NO_REVOKE_ARG = '--ssl-no-revoke';
|
|
35
47
|
/** stderr 是否指向 Schannel 取不到证书吊销信息(据此归因为环境 TLS 问题,而非网络不通)。 */
|
|
@@ -269,16 +281,27 @@ export function fetchViaCurl(url, proxy, timeoutMs) {
|
|
|
269
281
|
// 能力回 br / zstd,解不开时 curl 非零退出且正文为空,目录拉取直接失败;而 gzip 的解码器
|
|
270
282
|
// 每个 curl 都有,对 JSON 的压缩率也够。
|
|
271
283
|
const gzip = ['-H', 'Accept-Encoding: gzip', '--compressed'];
|
|
272
|
-
const attempt = (extra) => [
|
|
273
|
-
'-sS', ...
|
|
284
|
+
const attempt = (compressed, extra) => [
|
|
285
|
+
'-sS', ...(compressed ? gzip : []), ...extra,
|
|
286
|
+
'--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))), url,
|
|
274
287
|
];
|
|
275
|
-
|
|
276
|
-
|
|
288
|
+
const run = (compressed, extra = []) => curlWithTlsFallback(attempt(compressed, extra), proxyEnv(proxy), timeoutMs);
|
|
289
|
+
return run(true).then(async (first) => {
|
|
290
|
+
// 本机 curl 未编入 zlib 时不认识 --compressed(参数解析阶段就退出码 2,见
|
|
291
|
+
// curlCompressedUnsupported):整组去掉压缩协商重跑。只删 --compressed 而保留
|
|
292
|
+
// Accept-Encoding 会让服务端回 gzip 二进制,curl 不解压 → JSON 照样解析失败。
|
|
293
|
+
const compressed = !curlCompressedUnsupported(first.code, first.stderr);
|
|
294
|
+
const primary = compressed ? first : await run(false);
|
|
295
|
+
// 仍失败(连接被重置 / HTTP/2 流异常)时退到 HTTP/1.1 再试一次:
|
|
277
296
|
// Windows 自带 curl 与大响应 + HTTP/2 的组合偶发流级失败,换协议版本代价最小;
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
297
|
+
// 沿用已经验证可用的压缩模式,只在真失败时才多一次请求。
|
|
298
|
+
let r = primary;
|
|
299
|
+
if (primary.code !== 0) {
|
|
300
|
+
const retry = await run(compressed, ['--http1.1']);
|
|
301
|
+
// 极老的 curl 不认识 --http1.1:保留首轮的真实失败线索,不拿这个不相干的参数错误顶替
|
|
302
|
+
if (!curlOptionUnsupported(retry.code, retry.stderr, '--http1.1'))
|
|
303
|
+
r = retry;
|
|
304
|
+
}
|
|
282
305
|
const failed = r.code !== 0 || r.stdout === '';
|
|
283
306
|
return {
|
|
284
307
|
ok: !failed,
|
|
@@ -7,10 +7,12 @@ type NpmSearchPackage = {
|
|
|
7
7
|
repository?: unknown;
|
|
8
8
|
dsh?: unknown;
|
|
9
9
|
};
|
|
10
|
+
/** registry 路径:指定版本时取该版本的 manifest,否则取 latest。 */
|
|
11
|
+
export declare function npmMetadataPath(packageName: string, version?: string): string;
|
|
10
12
|
export declare function isDshPackageMetadataForRepo(pkg: NpmSearchPackage, repo: string): boolean;
|
|
11
13
|
/**
|
|
12
|
-
* 校验包名对应的 npm 包确实属于该仓库且声明为 DSH 插件:取 registry
|
|
13
|
-
*
|
|
14
|
+
* 校验包名对应的 npm 包确实属于该仓库且声明为 DSH 插件:取 registry 上指定版本(缺省 `latest`)
|
|
15
|
+
* 的元数据比对 repository 与插件标记。网络异常、超时、非 200 一律返回 false,不阻塞调用方。
|
|
14
16
|
*/
|
|
15
|
-
export declare function isDshNpmPackageForRepo(packageName: string, repo: string, registry?: string, proxy?: string): Promise<boolean>;
|
|
17
|
+
export declare function isDshNpmPackageForRepo(packageName: string, repo: string, registry?: string, proxy?: string, version?: string): Promise<boolean>;
|
|
16
18
|
export {};
|
|
@@ -25,6 +25,12 @@ export type ProbeFailReason = 'tlsRevocation';
|
|
|
25
25
|
export declare function curlTlsArgs(): string[];
|
|
26
26
|
/** 本机 curl 太老、不认识 --ssl-revoke-best-effort(curl < 7.70):退出码 2 + stderr 指向该参数。 */
|
|
27
27
|
export declare function curlRevokeFlagUnsupported(code: number | null, stderr: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 本机 curl 未编入 zlib:连 --compressed 这个选项都不认识,参数解析阶段即以退出码 2
|
|
30
|
+
* 结束(stderr 形如 `curl: option --compressed: the installed libcurl version doesn't
|
|
31
|
+
* support this`)。必须整组去掉压缩协商后重跑,否则目录拉取每次都失败、市场 502。
|
|
32
|
+
*/
|
|
33
|
+
export declare function curlCompressedUnsupported(code: number | null, stderr: string): boolean;
|
|
28
34
|
/** stderr 是否指向 Schannel 取不到证书吊销信息(据此归因为环境 TLS 问题,而非网络不通)。 */
|
|
29
35
|
export declare function tlsRevocationBlocked(stderr: string): boolean;
|
|
30
36
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "2.3.
|
|
4
|
-
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install
|
|
3
|
+
"version": "2.3.6",
|
|
4
|
+
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 20000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):20000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -313,9 +313,10 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
313
313
|
// 原生模块需要本机编译,缺编译器/构建工具属于用户环境,不是插件的 prepare 失败。
|
|
314
314
|
// 必须排在 pluginPrepare 之前:这类日志同样带 ELIFECYCLE,否则会被推给作者。
|
|
315
315
|
if (/Unable to detect compiler type|gyp ERR!|Could not find any Visual Studio|No Xcode or CLT version detected|xcode-select|python(?:3)? is (?:not set|not found)|Could not find Python|\bmake: not found\b|\bgcc: not found\b|\bg\+\+: not found\b|\bMSB\d{4}\b/i.test(message)) return 'buildToolchainMissing'
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
316
|
+
// 构建脚本真的跑挂了才算插件问题。必须带正向证据:`Command failed` 只是外层进程包装,
|
|
317
|
+
// 单凭它不能说明 prepare 失败(Electron 内置 pnpm 崩溃时日志里往往只剩这一句,
|
|
318
|
+
// 会把无关作者当成分发缺陷)。无证据的包装层失败交给 hub 分诊。
|
|
319
|
+
if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|prepare-guard|prepare script failed|Failed to prepare/i.test(message)) return 'pluginPrepare'
|
|
319
320
|
// 安装器自身产生的状态类消息(未安装 / 不在此处卸载 / 已在队列中 / 已安装):这几句由本服务的
|
|
320
321
|
// 守卫发出,不是 CLI 输出、也不是插件问题,而是插件中心的已安装列表与实读快照不同步。
|
|
321
322
|
// 单独归类,让前端给「刷新后重试」这类可行动提示,而不是当作未知失败引导去提 Issue。
|
|
@@ -138,7 +138,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
138
138
|
// 命中则记录 repo → 包名映射,客户端据此把依赖 key 匹配回仓库、列表立即显示「已安装」。
|
|
139
139
|
const catalogRepo = source === 'catalog' ? githubRepoOf(displayRepo) : null
|
|
140
140
|
if (catalogRepo !== null) {
|
|
141
|
-
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry, effectiveProxy)
|
|
141
|
+
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry, effectiveProxy, pinVersion)
|
|
142
142
|
if (catalogTargetIsDsh) recordResolvedNpmPackage(profile, catalogRepo, rawRepo)
|
|
143
143
|
}
|
|
144
144
|
// 命令行安装(目录外)受「启用 NPM 安装」开关控制;目录插件安装走目录白名单,不受开关影响。
|
|
@@ -34,6 +34,13 @@ function repositoryUrlOf(pkg: NpmSearchPackage): string {
|
|
|
34
34
|
return String(pkg.links?.repository ?? '')
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** registry 路径:指定版本时取该版本的 manifest,否则取 latest。 */
|
|
38
|
+
export function npmMetadataPath(packageName: string, version = ''): string {
|
|
39
|
+
const normalized = version.trim().replace(/^v/i, '')
|
|
40
|
+
const encoded = encodeURIComponent(packageName)
|
|
41
|
+
return normalized === '' ? `${encoded}/latest` : `${encoded}/${encodeURIComponent(normalized)}`
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
export function isDshPackageMetadataForRepo(pkg: NpmSearchPackage, repo: string): boolean {
|
|
38
45
|
if (githubRepoOf(repositoryUrlOf(pkg))?.toLowerCase() !== repo.toLowerCase()) return false
|
|
39
46
|
const keywords = Array.isArray(pkg.keywords) ? pkg.keywords : []
|
|
@@ -43,13 +50,13 @@ export function isDshPackageMetadataForRepo(pkg: NpmSearchPackage, repo: string)
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
|
-
* 校验包名对应的 npm 包确实属于该仓库且声明为 DSH 插件:取 registry
|
|
47
|
-
*
|
|
53
|
+
* 校验包名对应的 npm 包确实属于该仓库且声明为 DSH 插件:取 registry 上指定版本(缺省 `latest`)
|
|
54
|
+
* 的元数据比对 repository 与插件标记。网络异常、超时、非 200 一律返回 false,不阻塞调用方。
|
|
48
55
|
*/
|
|
49
|
-
export function isDshNpmPackageForRepo(packageName: string, repo: string, registry = '', proxy = ''): Promise<boolean> {
|
|
56
|
+
export function isDshNpmPackageForRepo(packageName: string, repo: string, registry = '', proxy = '', version = ''): Promise<boolean> {
|
|
50
57
|
if (packageName === '' || repo === '') return Promise.resolve(false)
|
|
51
58
|
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '')
|
|
52
|
-
const url = `${base}/${
|
|
59
|
+
const url = `${base}/${npmMetadataPath(packageName, version)}`
|
|
53
60
|
return new Promise((resolve) => {
|
|
54
61
|
const req = get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
55
62
|
if (res.statusCode !== 200) {
|
|
@@ -51,6 +51,20 @@ export function curlRevokeFlagUnsupported(code: number | null, stderr: string):
|
|
|
51
51
|
return code === 2 && /ssl-revoke-best-effort/i.test(stderr) && /unknown|unrecognized/i.test(stderr)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** curl 对某个可选参数报「不认识 / 不支持」:退出码 2 且 stderr 指向该参数。 */
|
|
55
|
+
function curlOptionUnsupported(code: number | null, stderr: string, option: string): boolean {
|
|
56
|
+
return code === 2 && stderr.includes(option) && /unknown|unrecognized|support/i.test(stderr)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 本机 curl 未编入 zlib:连 --compressed 这个选项都不认识,参数解析阶段即以退出码 2
|
|
61
|
+
* 结束(stderr 形如 `curl: option --compressed: the installed libcurl version doesn't
|
|
62
|
+
* support this`)。必须整组去掉压缩协商后重跑,否则目录拉取每次都失败、市场 502。
|
|
63
|
+
*/
|
|
64
|
+
export function curlCompressedUnsupported(code: number | null, stderr: string): boolean {
|
|
65
|
+
return curlOptionUnsupported(code, stderr, '--compressed')
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
/** 彻底关闭证书吊销检查的保底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
|
|
55
69
|
const NO_REVOKE_ARG = '--ssl-no-revoke'
|
|
56
70
|
|
|
@@ -293,16 +307,27 @@ export function fetchViaCurl(
|
|
|
293
307
|
// 能力回 br / zstd,解不开时 curl 非零退出且正文为空,目录拉取直接失败;而 gzip 的解码器
|
|
294
308
|
// 每个 curl 都有,对 JSON 的压缩率也够。
|
|
295
309
|
const gzip = ['-H', 'Accept-Encoding: gzip', '--compressed']
|
|
296
|
-
const attempt = (extra: string[]): string[] => [
|
|
297
|
-
'-sS', ...
|
|
310
|
+
const attempt = (compressed: boolean, extra: string[]): string[] => [
|
|
311
|
+
'-sS', ...(compressed ? gzip : []), ...extra,
|
|
312
|
+
'--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))), url,
|
|
298
313
|
]
|
|
299
|
-
|
|
300
|
-
|
|
314
|
+
const run = (compressed: boolean, extra: string[] = []): Promise<CurlRun> =>
|
|
315
|
+
curlWithTlsFallback(attempt(compressed, extra), proxyEnv(proxy), timeoutMs)
|
|
316
|
+
return run(true).then(async (first) => {
|
|
317
|
+
// 本机 curl 未编入 zlib 时不认识 --compressed(参数解析阶段就退出码 2,见
|
|
318
|
+
// curlCompressedUnsupported):整组去掉压缩协商重跑。只删 --compressed 而保留
|
|
319
|
+
// Accept-Encoding 会让服务端回 gzip 二进制,curl 不解压 → JSON 照样解析失败。
|
|
320
|
+
const compressed = !curlCompressedUnsupported(first.code, first.stderr)
|
|
321
|
+
const primary = compressed ? first : await run(false)
|
|
322
|
+
// 仍失败(连接被重置 / HTTP/2 流异常)时退到 HTTP/1.1 再试一次:
|
|
301
323
|
// Windows 自带 curl 与大响应 + HTTP/2 的组合偶发流级失败,换协议版本代价最小;
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
324
|
+
// 沿用已经验证可用的压缩模式,只在真失败时才多一次请求。
|
|
325
|
+
let r = primary
|
|
326
|
+
if (primary.code !== 0) {
|
|
327
|
+
const retry = await run(compressed, ['--http1.1'])
|
|
328
|
+
// 极老的 curl 不认识 --http1.1:保留首轮的真实失败线索,不拿这个不相干的参数错误顶替
|
|
329
|
+
if (!curlOptionUnsupported(retry.code, retry.stderr, '--http1.1')) r = retry
|
|
330
|
+
}
|
|
306
331
|
const failed = r.code !== 0 || r.stdout === ''
|
|
307
332
|
return {
|
|
308
333
|
ok: !failed,
|