dsh-plugin 1.4.12 → 1.4.14
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 +11 -11
- package/README.md +11 -11
- package/client/client.js +374 -303
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +17 -10
- package/lib/services/install/npm-resolve.js +29 -13
- package/lib/services/install/preflight.js +5 -0
- package/lib/services/install/release-target.js +38 -0
- package/lib/services/profile/profile.js +6 -0
- package/lib/types/services/install/npm-resolve.d.ts +22 -0
- package/lib/types/services/install/release-target.d.ts +28 -0
- package/package.json +1 -1
- package/src/client/components/modals/AboutModal.tsx +18 -2
- package/src/client/components/modals/NotificationsModal.tsx +11 -1
- package/src/client/components/modals/modals.tsx +12 -1
- package/src/client/locales.ts +7 -0
- package/src/client/logic/failures.ts +31 -5
- package/src/client/logic/install-command.ts +18 -8
- package/src/client/logic/urls.ts +1 -0
- package/src/server/http/routes.ts +17 -10
- package/src/server/services/install/npm-resolve.ts +41 -13
- package/src/server/services/install/preflight.ts +4 -0
- package/src/server/services/install/release-target.ts +46 -0
- package/src/server/services/profile/profile.ts +5 -0
package/lib/http/routes.js
CHANGED
|
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
14
14
|
import { spawn, spawnSync } from 'node:child_process';
|
|
15
15
|
import { fetchViaCurl, gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
|
|
16
16
|
import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, globalNpmPackagesOf, hasQueuedTarget, installTargetOf, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName } from '../services/install/install.js';
|
|
17
|
+
import { githubReleaseTarget } from '../services/install/release-target.js';
|
|
17
18
|
import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/profile/installed-versions.js';
|
|
18
19
|
import { resolveNpmPackage } from '../services/install/npm-resolve.js';
|
|
19
20
|
import { preflightTarget } from '../services/install/preflight.js';
|
|
@@ -801,12 +802,14 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
801
802
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
802
803
|
return;
|
|
803
804
|
}
|
|
804
|
-
//
|
|
805
|
-
//
|
|
805
|
+
// 目标语法三态:固定 GitHub release .tgz(保留其包根目录,不转成仓库 HEAD);
|
|
806
|
+
// GitHub 地址(owner/repo、github:、https/ssh 链接等任意写法)→ 显式 HTTPS Git 源
|
|
807
|
+
// (走装前预检);npm 包名(@scope/name 或 name)→ 信任 registry 直接安装。
|
|
806
808
|
// githubRepoOf 先把各种 GitHub 地址归一成 owner/repo,让「输入地址即装」兼容粘贴完整链接。
|
|
807
809
|
const gitRepo = githubRepoOf(rawRepo);
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
+
const release = githubReleaseTarget(rawRepo);
|
|
811
|
+
const repoTarget = gitRepo !== null && release === null ? githubTarget(gitRepo) : null;
|
|
812
|
+
let target = release?.target ?? repoTarget ?? (validPackageName(rawRepo) ? rawRepo : null);
|
|
810
813
|
if (target === null) {
|
|
811
814
|
sendJson(response, 400, { error: 'unsupported install target' });
|
|
812
815
|
return;
|
|
@@ -836,9 +839,9 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
836
839
|
}
|
|
837
840
|
// 命令行安装(目录外)按通道门禁:GitHub 源码通道受「启用 GitHub 源码安装」控制,
|
|
838
841
|
// npm 通道受「启用 NPM 安装」控制 —— 目录插件安装走目录白名单,不受开关影响。
|
|
839
|
-
// 通道判定看反查后的最终 target
|
|
842
|
+
// 通道判定看反查后的最终 target:release 包与 git 目标同属 GitHub 通道,否则走 npm。
|
|
840
843
|
if (source !== 'catalog') {
|
|
841
|
-
const isGitChannel = repoTarget !== null && target === repoTarget;
|
|
844
|
+
const isGitChannel = release !== null || (repoTarget !== null && target === repoTarget);
|
|
842
845
|
if (isGitChannel && !settings.enableGitInstall) {
|
|
843
846
|
sendJson(response, 403, { error: 'git installs are disabled by the security settings' });
|
|
844
847
|
return;
|
|
@@ -874,8 +877,12 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
874
877
|
const effectiveProxy = settings.proxy !== ''
|
|
875
878
|
? settings.proxy
|
|
876
879
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
877
|
-
const isGitChannel = repoTarget !== null && target === repoTarget;
|
|
878
|
-
|
|
880
|
+
const isGitChannel = release !== null || (repoTarget !== null && target === repoTarget);
|
|
881
|
+
// release 包是 https://github.com/ 直链下载(非 git 克隆),探针按 HTTP 打 github.com 主页;
|
|
882
|
+
// npm 通道探 registry 根。两者都是通道的稳定入口,不探具体包/仓库(404 会被误判成网络故障)。
|
|
883
|
+
const httpProbeTarget = release !== null
|
|
884
|
+
? 'https://github.com/'
|
|
885
|
+
: `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`;
|
|
879
886
|
// git 通道的探针换成克隆握手本身:git ls-remote 走的就是 pnpm 克隆前的同一套
|
|
880
887
|
// https 传输。curl 打 github.com 主页会被按协议/端口区分的防火墙与 TLS 复检
|
|
881
888
|
// 拦截(能 git 克隆、打不开网页),把可装的仓库误判成 [network] 而中止
|
|
@@ -893,9 +900,9 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
893
900
|
}
|
|
894
901
|
}
|
|
895
902
|
else {
|
|
896
|
-
net = await probeUrl(
|
|
903
|
+
net = await probeUrl(httpProbeTarget, effectiveProxy, 6000);
|
|
897
904
|
}
|
|
898
|
-
const probeTarget = gitProbeTarget ??
|
|
905
|
+
const probeTarget = gitProbeTarget ?? httpProbeTarget;
|
|
899
906
|
if (!net.ok) {
|
|
900
907
|
// 错误消息按客户端界面语言提示:中文界面给中文、英文界面给英文,
|
|
901
908
|
// 让用户一眼看懂是网络问题而非插件问题
|
|
@@ -16,6 +16,8 @@ import { get } from 'node:https';
|
|
|
16
16
|
const cache = new Map();
|
|
17
17
|
/** 单次 registry 查询超时(ms):慢网络下失败返回 null,不阻塞安装流程。 */
|
|
18
18
|
const REQUEST_TIMEOUT_MS = 8000;
|
|
19
|
+
/** npm search 是相关性搜索而非 repository 精确索引;拉满单页结果后再本地做严格 repo 过滤。 */
|
|
20
|
+
const SEARCH_RESULT_LIMIT = 250;
|
|
19
21
|
/**
|
|
20
22
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
21
23
|
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
@@ -37,6 +39,31 @@ export function resolveNpmPackage(repo, registry = '') {
|
|
|
37
39
|
});
|
|
38
40
|
return name.then((found) => found ?? null);
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Pick the installable npm package for one GitHub repository. npm search is a
|
|
44
|
+
* relevance-ranked text search, not an exact repository index, so first filter
|
|
45
|
+
* by repository URL. If a monorepo publishes both a CLI/helper package and a
|
|
46
|
+
* DSH plugin, prefer the package that explicitly advertises the `dsh-plugin`
|
|
47
|
+
* keyword instead of whichever result npm happened to rank first.
|
|
48
|
+
*/
|
|
49
|
+
export function selectNpmPackageForRepo(objects, repo) {
|
|
50
|
+
const needle = `github.com/${repo}`.toLowerCase();
|
|
51
|
+
const matches = [];
|
|
52
|
+
for (const obj of objects) {
|
|
53
|
+
const pkg = obj.package;
|
|
54
|
+
if (typeof pkg?.name !== 'string' || pkg.name === '')
|
|
55
|
+
continue;
|
|
56
|
+
const repoUrl = String(pkg.repository?.url ?? pkg.links?.repository ?? '');
|
|
57
|
+
if (!repoUrl.toLowerCase().includes(needle))
|
|
58
|
+
continue;
|
|
59
|
+
const keywords = Array.isArray(pkg.keywords) ? pkg.keywords : [];
|
|
60
|
+
matches.push({
|
|
61
|
+
name: pkg.name,
|
|
62
|
+
isDshPlugin: keywords.some((keyword) => typeof keyword === 'string' && keyword.toLowerCase() === 'dsh-plugin'),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return matches.find((candidate) => candidate.isDshPlugin)?.name ?? matches[0]?.name ?? null;
|
|
66
|
+
}
|
|
40
67
|
/** 用 npm search 接口按 repository 地址反查,并做铁证校验(包元数据必须指向该仓库)。
|
|
41
68
|
* 返回:包名 = 命中;null = 200 响应下确认无匹配;undefined = 本次未能确认(网络/超时/解析失败)。 */
|
|
42
69
|
function searchRepo(repo, registry) {
|
|
@@ -44,7 +71,7 @@ function searchRepo(repo, registry) {
|
|
|
44
71
|
if (owner === undefined || name === undefined || name === '')
|
|
45
72
|
return Promise.resolve(null);
|
|
46
73
|
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '');
|
|
47
|
-
const url = `${base}/-/v1/search?text=${encodeURIComponent(`repository:${owner}/${name}`)}&size
|
|
74
|
+
const url = `${base}/-/v1/search?text=${encodeURIComponent(`repository:${owner}/${name}`)}&size=${SEARCH_RESULT_LIMIT}`;
|
|
48
75
|
return new Promise((resolve) => {
|
|
49
76
|
const req = get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
50
77
|
if (res.statusCode !== 200) {
|
|
@@ -57,18 +84,7 @@ function searchRepo(repo, registry) {
|
|
|
57
84
|
res.on('end', () => {
|
|
58
85
|
try {
|
|
59
86
|
const data = JSON.parse(body);
|
|
60
|
-
|
|
61
|
-
for (const obj of data.objects ?? []) {
|
|
62
|
-
const pkg = obj.package;
|
|
63
|
-
if (typeof pkg?.name !== 'string' || pkg.name === '')
|
|
64
|
-
continue;
|
|
65
|
-
const repoUrl = String(pkg.repository?.url ?? pkg.links?.repository ?? '');
|
|
66
|
-
if (repoUrl.toLowerCase().includes(needle)) {
|
|
67
|
-
resolve(pkg.name);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
resolve(null);
|
|
87
|
+
resolve(selectNpmPackageForRepo(data.objects ?? [], `${owner}/${name}`));
|
|
72
88
|
}
|
|
73
89
|
catch {
|
|
74
90
|
resolve(undefined);
|
|
@@ -18,12 +18,17 @@ import { join } from 'node:path';
|
|
|
18
18
|
import { pipeline } from 'node:stream/promises';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
20
|
import { githubRepoOf } from '../profile/profile.js';
|
|
21
|
+
import { githubReleaseTarget } from './release-target.js';
|
|
21
22
|
import { resolvePackageEntry } from './package-entry.js';
|
|
22
23
|
const execFileAsync = promisify(execFile);
|
|
23
24
|
/** 单次网络操作超时(ms):git ls-remote / codeload 下载。网络不可达时不能无限挂起,
|
|
24
25
|
* 否则 `/install` 请求永远不返回、任务不入队、前端进度卡 0%。超时后放行(交给装后校验)。 */
|
|
25
26
|
const PREFLIGHT_TIMEOUT_MS = 15_000;
|
|
26
27
|
export async function preflightTarget(target) {
|
|
28
|
+
// release 包自带包根:拿 monorepo HEAD 的源码分发去校验它没有意义,会把完整产物
|
|
29
|
+
// 误判成缺入口文件而拦下。该包的真实性交给安装本身与装后 verifyInstalledEntry 兜底。
|
|
30
|
+
if (githubReleaseTarget(target) !== null)
|
|
31
|
+
return { ok: true, missing: null, name: null };
|
|
27
32
|
const source = githubRepoOf(target);
|
|
28
33
|
if (source === null)
|
|
29
34
|
return { ok: true, missing: null, name: null };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* 预构建 GitHub release 安装目标(目录权威命令 → 可安装直链)。
|
|
7
|
+
* 部分插件(如 loopx)把构建产物发布成 GitHub release 的 .tgz,仓库 HEAD 反而是
|
|
8
|
+
* monorepo 根、走 git 直装会因缺入口文件失败;目录下发的权威安装命令直接指向该
|
|
9
|
+
* release 包。这里把「目录命令」解析成安全的 release 目标,供客户端选路与服务端
|
|
10
|
+
* 校验共用(单一事实来源,避免两端口径漂移)。
|
|
11
|
+
*
|
|
12
|
+
* 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
|
|
13
|
+
* 目录命令是展示数据、绝非 shell 代码,绝不执行命令文本;跨仓跳转、外部主机、凭据、
|
|
14
|
+
* 移动的 latest 链接、query/fragment、多余 CLI 参数与 shell 片段一律拒绝。
|
|
15
|
+
*/
|
|
16
|
+
/** 目录权威命令:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同口径)。 */
|
|
17
|
+
const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i;
|
|
18
|
+
/** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */
|
|
19
|
+
const RELEASE_URL_RE = /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/releases\/download\/([A-Za-z0-9._+-]+)\/([A-Za-z0-9._+-]+\.tgz)$/;
|
|
20
|
+
/**
|
|
21
|
+
* 解析一个 release 安装目标。
|
|
22
|
+
* @param value - 目录命令(`dsh plugin --profile web add <url>`)或裸直链。
|
|
23
|
+
* @returns 命中返回 `{ target, repo }`,否则 null。
|
|
24
|
+
*/
|
|
25
|
+
export function githubReleaseTarget(value) {
|
|
26
|
+
const input = typeof value === 'string' ? value.trim() : '';
|
|
27
|
+
if (input === '')
|
|
28
|
+
return null;
|
|
29
|
+
const command = DSH_PLUGIN_CMD_RE.exec(input);
|
|
30
|
+
const raw = command !== null ? command[1].trim() : input;
|
|
31
|
+
const match = RELEASE_URL_RE.exec(raw);
|
|
32
|
+
if (match === null)
|
|
33
|
+
return null;
|
|
34
|
+
// 拒绝 `.` / `..` 路径段,杜绝借路径穿越跳出 release 目录
|
|
35
|
+
if (match.slice(1).some((part) => part === '.' || part === '..'))
|
|
36
|
+
return null;
|
|
37
|
+
return { target: raw, repo: `${match[1]}/${match[2]}` };
|
|
38
|
+
}
|
|
@@ -10,6 +10,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { basename, dirname, join } from 'node:path';
|
|
12
12
|
import { PACKAGE_RE, REPO_RE } from '../install/install-types.js';
|
|
13
|
+
import { githubReleaseTarget } from '../install/release-target.js';
|
|
13
14
|
/** Resolve the active profile from the booted CLI args, falling back to `web`. */
|
|
14
15
|
export function readProfileArg(fallback = 'web') {
|
|
15
16
|
return profileFromArgv(process.argv, fallback);
|
|
@@ -90,6 +91,11 @@ export function githubRepoOf(value) {
|
|
|
90
91
|
if (typeof value !== 'string')
|
|
91
92
|
return null;
|
|
92
93
|
const input = value.trim();
|
|
94
|
+
// release 直链先归一到仓库身份:目录命令里的固定 HTTPS release 包不是 git 地址,
|
|
95
|
+
// 但它的所属仓库就是 owner/repo —— 已装态读回、去重、展示都要拿到这层身份。
|
|
96
|
+
const release = githubReleaseTarget(input);
|
|
97
|
+
if (release !== null)
|
|
98
|
+
return release.repo;
|
|
93
99
|
if (REPO_RE.test(input))
|
|
94
100
|
return input;
|
|
95
101
|
const patterns = [
|
|
@@ -9,3 +9,25 @@
|
|
|
9
9
|
* 慢网络下失败不阻塞安装。
|
|
10
10
|
*/
|
|
11
11
|
export declare function resolveNpmPackage(repo: string, registry?: string): Promise<string | null>;
|
|
12
|
+
type NpmSearchPackage = {
|
|
13
|
+
name?: unknown;
|
|
14
|
+
keywords?: unknown;
|
|
15
|
+
links?: {
|
|
16
|
+
repository?: unknown;
|
|
17
|
+
};
|
|
18
|
+
repository?: {
|
|
19
|
+
url?: unknown;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type NpmSearchObject = {
|
|
23
|
+
package?: NpmSearchPackage;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Pick the installable npm package for one GitHub repository. npm search is a
|
|
27
|
+
* relevance-ranked text search, not an exact repository index, so first filter
|
|
28
|
+
* by repository URL. If a monorepo publishes both a CLI/helper package and a
|
|
29
|
+
* DSH plugin, prefer the package that explicitly advertises the `dsh-plugin`
|
|
30
|
+
* keyword instead of whichever result npm happened to rank first.
|
|
31
|
+
*/
|
|
32
|
+
export declare function selectNpmPackageForRepo(objects: NpmSearchObject[], repo: string): string | null;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* 预构建 GitHub release 安装目标(目录权威命令 → 可安装直链)。
|
|
7
|
+
* 部分插件(如 loopx)把构建产物发布成 GitHub release 的 .tgz,仓库 HEAD 反而是
|
|
8
|
+
* monorepo 根、走 git 直装会因缺入口文件失败;目录下发的权威安装命令直接指向该
|
|
9
|
+
* release 包。这里把「目录命令」解析成安全的 release 目标,供客户端选路与服务端
|
|
10
|
+
* 校验共用(单一事实来源,避免两端口径漂移)。
|
|
11
|
+
*
|
|
12
|
+
* 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
|
|
13
|
+
* 目录命令是展示数据、绝非 shell 代码,绝不执行命令文本;跨仓跳转、外部主机、凭据、
|
|
14
|
+
* 移动的 latest 链接、query/fragment、多余 CLI 参数与 shell 片段一律拒绝。
|
|
15
|
+
*/
|
|
16
|
+
/** release 包目标及其所属仓库身份(owner/repo);非法返回 null。 */
|
|
17
|
+
export interface GithubReleaseTarget {
|
|
18
|
+
/** 规范化后的安装目标(HTTPS 直链) */
|
|
19
|
+
target: string;
|
|
20
|
+
/** 该直链所属仓库(owner/repo):供已装态读回、队列去重与展示身份复用 */
|
|
21
|
+
repo: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 解析一个 release 安装目标。
|
|
25
|
+
* @param value - 目录命令(`dsh plugin --profile web add <url>`)或裸直链。
|
|
26
|
+
* @returns 命中返回 `{ target, repo }`,否则 null。
|
|
27
|
+
*/
|
|
28
|
+
export declare function githubReleaseTarget(value: string): GithubReleaseTarget | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.14",
|
|
4
4
|
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 10000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):10000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 客户端按接口返回渲染,不内置固定文案。
|
|
9
9
|
* 图片(反馈群二维码)经 renderMarkdown 的  语法嵌入,居中、最大高度受控。
|
|
10
10
|
*/
|
|
11
|
-
import { createElement as h } from 'react'
|
|
11
|
+
import { createElement as h, useMemo } from 'react'
|
|
12
12
|
import type { MouseEvent } from 'react'
|
|
13
13
|
import styles from '../../styles/Modal.module.css'
|
|
14
14
|
import type { HubAboutInfo } from '../../types.ts'
|
|
@@ -16,6 +16,19 @@ import type { LocaleId, Translate } from '../../types.ts'
|
|
|
16
16
|
import { renderMarkdown } from '../../logic/renderMarkdown.ts'
|
|
17
17
|
import { CloseIcon } from '../ui/icons.tsx'
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 反馈群二维码 URL 恒定(每次换码都是同名覆盖,文件名不变),浏览器 / CDN 会按
|
|
21
|
+
* URL 缓存旧图。这里在渲染前给二维码 URL 实时拼一个新的时间戳,强制每次打开
|
|
22
|
+
* 「关注我们」都回源拉取最新图片,无需重新构建 / 部署接口中心。
|
|
23
|
+
* 匹配规则与 renderMarkdown 的二维码识别保持一致(dsh-plugin-user-group-qr 前缀)。
|
|
24
|
+
*/
|
|
25
|
+
function bustQrCache(md: string, ts: number): string {
|
|
26
|
+
return md.replace(
|
|
27
|
+
/(!\[[^\]]*\]\()([^)\s]*dsh-plugin-user-group-qr[^)\s]*)/g,
|
|
28
|
+
(_m, pre, url) => `${pre}${String(url).split('?')[0]}?v=${ts}`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
export function AboutModal({ info, lang, t, onClose }: {
|
|
20
33
|
info: HubAboutInfo | null
|
|
21
34
|
lang: LocaleId
|
|
@@ -33,7 +46,10 @@ export function AboutModal({ info, lang, t, onClose }: {
|
|
|
33
46
|
: (info.content.zh ?? info.content.en ?? '')
|
|
34
47
|
: ''
|
|
35
48
|
: ''
|
|
36
|
-
|
|
49
|
+
// 每次打开弹窗(组件重新挂载)生成一个时间戳,用于二维码破缓存;
|
|
50
|
+
// 同一次打开内保持稳定,避免父组件重渲染导致图片被反复重载。
|
|
51
|
+
const qrTs = useMemo(() => Date.now(), [])
|
|
52
|
+
const contentHtml = contentRaw.trim() ? renderMarkdown(bustQrCache(contentRaw, qrTs)) : null
|
|
37
53
|
// 更新时间:ISO 字符串转本地可读格式;非法值静默隐藏
|
|
38
54
|
let updated: string | null = null
|
|
39
55
|
if (info?.updatedAt) {
|
|
@@ -18,7 +18,7 @@ import type { MouseEvent } from 'react'
|
|
|
18
18
|
import styles from '../../styles/Modal.module.css'
|
|
19
19
|
import type { EnvInfo, Translate } from '../../types.ts'
|
|
20
20
|
import type { NotificationRecord } from '../../logic/failures.ts'
|
|
21
|
-
import { classifyFailure, npmTooLowVersion, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
21
|
+
import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
22
22
|
import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
|
|
23
23
|
import { pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
|
|
24
24
|
import { CloseIcon } from '../ui/icons.tsx'
|
|
@@ -307,6 +307,16 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
307
307
|
// 发现补丁没被用上就中止安装,任何插件都装不进来 —— 不引导提 Issue
|
|
308
308
|
return h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
|
|
309
309
|
}
|
|
310
|
+
if (kind === 'pnpmMissingDep') {
|
|
311
|
+
// profile 里留着指向 registry 上不存在(404)的包的依赖:pnpm 连得上 registry,
|
|
312
|
+
// 是被明确告知包不存在 —— 该 profile 的任何安装都会先卡在这条依赖上,与本次要装的
|
|
313
|
+
// 插件无关 → 点名缺失的包 + 删除条目指引,不引导提 Issue
|
|
314
|
+
const pkg = missingRegistryPackageOf(r.message)
|
|
315
|
+
return h('div', null, [
|
|
316
|
+
pkg ? h('div', { className: styles.failNetworkTarget }, t('failPnpmMissingDepTarget', { pkg })) : null,
|
|
317
|
+
h('div', { className: styles.failPrepareHint }, t('failPnpmMissingDepHint')),
|
|
318
|
+
])
|
|
319
|
+
}
|
|
310
320
|
if (kind === 'fileLocked') {
|
|
311
321
|
// profile 里的文件被其他进程占用(Windows os error 32):通常是宿主或杀毒软件
|
|
312
322
|
// 持有句柄 —— 给出完全退出宿主的指引,不引导提 Issue
|
|
@@ -16,7 +16,7 @@ import { CloseIcon, ConfirmIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
|
|
|
16
16
|
import { ProgressView } from './ProgressView.tsx'
|
|
17
17
|
import { installCommandOf } from '../../logic/install-command.ts'
|
|
18
18
|
import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
|
|
19
|
-
import { classifyFailure, npmTooLowVersion, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
19
|
+
import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
20
20
|
|
|
21
21
|
/** 完成结果视图:绿色对勾 + 标题/描述;
|
|
22
22
|
* needsRestart=true(插件需重启才生效)→ 「稍后重启 / 立即重启」按钮对,点稍后重启后
|
|
@@ -421,6 +421,17 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
421
421
|
// profile 里留着指向旧版本 dsh-plugin 的补丁声明(ERR_PNPM_UNUSED_PATCH):pnpm 发现
|
|
422
422
|
// 补丁没被用上就中止整次安装,任何插件都装不进来 —— 给删除条目的指引,不引导提 Issue
|
|
423
423
|
? h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
|
|
424
|
+
: failureKind === 'pnpmMissingDep'
|
|
425
|
+
// profile 里留着指向 registry 上不存在(404)的包的依赖:pnpm 连得上 registry,
|
|
426
|
+
// 是被明确告知包不存在 —— 该 profile 的任何安装都会先卡在这条依赖上,与本次要装的
|
|
427
|
+
// 插件无关 → 点名缺失的包 + 删除条目指引,不引导提 Issue
|
|
428
|
+
? h('div', null, [
|
|
429
|
+
(() => {
|
|
430
|
+
const pkg = missingRegistryPackageOf(message)
|
|
431
|
+
return pkg ? h('div', { className: styles.failNetworkTarget }, t('failPnpmMissingDepTarget', { pkg })) : null
|
|
432
|
+
})(),
|
|
433
|
+
h('div', { className: styles.failPrepareHint }, t('failPnpmMissingDepHint')),
|
|
434
|
+
])
|
|
424
435
|
: failureKind === 'fileLocked'
|
|
425
436
|
// profile 里的文件被其他进程占用(Windows os error 32):pnpm 无法替换被占用的文件,
|
|
426
437
|
// 通常是宿主或杀毒软件持有句柄 —— 给退出宿主的指引,不引导提 Issue
|
package/src/client/locales.ts
CHANGED
|
@@ -326,6 +326,9 @@ export const zh = {
|
|
|
326
326
|
failNpmMissingHint: '系统找不到 npm 命令(npm 未安装或未加入系统 PATH),无法执行全局安装。请先安装 Node.js(自带 npm),确认 npm 已加入 PATH 后重试。这不是插件本身的问题。',
|
|
327
327
|
failPnpmStoreHint: '安装器调用 pnpm 时报「store 位置不匹配」(ERR_PNPM_UNEXPECTED_STORE):你的 profile 目录里旧插件依赖是用另一个大版本的 pnpm 生成的,当前 pnpm 出于安全不认旧目录,导致任何插件装进该 profile 都会失败。请删除该 profile 目录下的 node_modules 与 pnpm-lock.yaml(可先到「系统日志」或 ~/.dsh/profiles/<profile>/hub.log 查看完整报错),再重新安装。这不是插件本身的问题。',
|
|
328
328
|
failPnpmUnusedPatchHint: '安装器调用 pnpm 时报「有些补丁没有被使用」(ERR_PNPM_UNUSED_PATCH):你的 profile 里留着一条指向旧版本 dsh-plugin 的补丁声明(例如 dsh-plugin@1.4.2),而本次安装解析到的版本已经不是它,pnpm 出于安全直接中止整次安装,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml(pnpm 11 及以上)或 package.json 的 pnpm.patchedDependencies(pnpm 10)里删除该 dsh-plugin 补丁条目后重试。',
|
|
329
|
+
// 404 依赖:点名 registry 上不存在的那个包(取不到包名时跳过这行),再给删除条目的指引
|
|
330
|
+
failPnpmMissingDepTarget: '{pkg} 在 npm registry 上不存在(404)',
|
|
331
|
+
failPnpmMissingDepHint: '安装器调用 pnpm 时报「包不存在」(ERR_PNPM_FETCH_404):你的 profile 里留着一条指向 registry 上未发布包的依赖,pnpm 每次解析依赖都会先卡在它上面,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 package.json 里删除该依赖条目(同时删掉对应的 pnpm-lock.yaml 条目或整个锁文件)后重试。',
|
|
329
332
|
failFileLockedHint: '安装过程中 pnpm 无法替换 profile 里的文件:有程序正占用着它(Windows 常见报错 os error 32「另一个程序正在使用此文件」)。通常是 DeepSeek Harness 自身或杀毒软件的实时扫描持有文件句柄。这不是插件本身的问题——请完全退出 DeepSeek Harness(Windows 可在任务管理器确认进程已结束,必要时临时关闭杀毒软件实时防护)后重试。',
|
|
330
333
|
failAccessDeniedHint: '安装过程中 pnpm 写入 profile 里的文件被系统拒绝访问(Windows 常见报错 os error 5「拒绝访问」,英文为 Access is denied;Node 下的 EPERM / EACCES 同理)。常见原因是该文件或目录正被其他进程占用、文件带只读属性、目录权限受限,或杀毒软件实时防护拦截了写入——这与「文件正被占用」(os error 32)同属本机文件系统问题,任何插件装进该目录都会一样失败,不是插件本身的问题。请完全退出 DeepSeek Harness(Windows 可在任务管理器确认进程已结束,必要时临时关闭杀毒软件实时防护)后重试;若仍然失败,请检查 profile 目录及其文件是否被设为只读,并以管理员身份重新运行。',
|
|
331
334
|
failFsUnavailableHint: '安装过程中 pnpm 无法把文件写进本机磁盘(这类报错与装哪个插件无关,任何插件装在同一目录都会失败,不是插件本身的问题)。请按报错对号入座:① 磁盘空间不足(ENOSPC / no space left on device,Windows 为 os error 112)——请清理 profile 所在磁盘的剩余空间后重试;② 磁盘或挂载点只读、带写保护(EROFS / read-only file system,Windows 为 os error 19)——请确认 profile 目录所在磁盘可写(外接盘/U 盘、带写保护的移动盘或只读挂载的分区都会触发),必要时把插件目录换到系统盘的可写位置;③ 进程可用的文件句柄被耗尽(EMFILE / ENFILE / too many open files)——请关闭占用大量文件的程序并重启 DeepSeek Harness 后重试。',
|
|
@@ -700,6 +703,10 @@ export const en = {
|
|
|
700
703
|
failNpmMissingHint: 'The npm command could not be found (npm is not installed or not on the system PATH), so a global install cannot run. Please install Node.js (which bundles npm), make sure npm is on your PATH, then retry. This is not a problem with the plugin itself.',
|
|
701
704
|
failPnpmStoreHint: 'pnpm reported an unexpected store location (ERR_PNPM_UNEXPECTED_STORE): the profile directory was previously set up with a different major version of pnpm, and the current pnpm refuses to reuse it — no plugin can be installed into that profile. Delete node_modules and pnpm-lock.yaml under the profile directory (see System Logs or ~/.dsh/profiles/<profile>/hub.log for the full output), then reinstall. This is not a problem with the plugin itself.',
|
|
702
705
|
failPnpmUnusedPatchHint: 'pnpm reported that some patches were not used (ERR_PNPM_UNUSED_PATCH): your profile still holds a patch entry for an older dsh-plugin version (for example dsh-plugin@1.4.2), while this install resolved a different version, so pnpm aborts the whole install for safety — no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that dsh-plugin patch entry from pnpm-workspace.yaml (pnpm 11+) or from pnpm.patchedDependencies in package.json (pnpm 10) inside the profile directory (~/.dsh/profiles/<profile>), then retry.',
|
|
706
|
+
// Missing dependency: name the package that 404'd (the line is skipped when it cannot be parsed),
|
|
707
|
+
// then give the removal instructions
|
|
708
|
+
failPnpmMissingDepTarget: '{pkg} was not found on the npm registry (404)',
|
|
709
|
+
failPnpmMissingDepHint: 'pnpm reported that a package does not exist on the npm registry (ERR_PNPM_FETCH_404): your profile keeps a dependency on a package that was never published, and pnpm hits it every time it resolves dependencies — no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that dependency from package.json inside the profile directory (~/.dsh/profiles/<profile>) — along with its entry in pnpm-lock.yaml, or the lockfile itself — then retry.',
|
|
703
710
|
failFileLockedHint: 'During install, pnpm could not replace a file inside the profile because another program is holding it (on Windows this shows as os error 32, "the process cannot access the file because it is being used by another process"). Usually DeepSeek Harness itself or a real-time antivirus scan holds the handle. This is not a problem with the plugin itself — fully quit DeepSeek Harness (on Windows, confirm in Task Manager that the process has ended, and temporarily disable real-time antivirus protection if needed), then retry.',
|
|
704
711
|
failAccessDeniedHint: 'During install, Windows denied pnpm access to a file inside the profile (this shows as os error 5, "Access is denied"; on Node the equivalent codes are EPERM / EACCES). Typical causes: the file or directory is held by another process, the file is marked read-only, the directory permissions are restricted, or a real-time antivirus scan blocked the write. This is the same class of local file-system problem as a locked file (os error 32) — no plugin can be installed into that directory, so it is not a problem with the plugin itself. Fully quit DeepSeek Harness (on Windows, confirm in Task Manager that the process has ended, and temporarily disable real-time antivirus protection if needed), then retry. If it still fails, check whether the profile directory or its files are read-only and run as administrator.',
|
|
705
712
|
failFsUnavailableHint: 'During install, pnpm could not write files to the local disk — this has nothing to do with which plugin you are installing: any plugin installed into the same directory would fail the same way, so it is not a problem with the plugin itself. Match the error you see: ① Out of space (ENOSPC / no space left on device, os error 112 on Windows) — free up space on the drive that holds the profile and retry. ② Read-only or write-protected volume (EROFS / read-only file system, os error 19 on Windows) — make sure the drive holding the profile directory is writable (external or USB drives, write-protected media, and read-only mounts all trigger this); move the plugin directory to a writable location on the system drive if needed. ③ File handles exhausted (EMFILE / ENFILE / too many open files) — close programs that hold many files open, restart DeepSeek Harness, then retry.',
|
|
@@ -151,7 +151,7 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
151
151
|
return next
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* 失败归类:把安装输出归到具体成因,供弹窗文案、是否引导提 Issue 与 issue 预填原因使用。
|
|
@@ -191,6 +191,9 @@ export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissi
|
|
|
191
191
|
* - pnpmUnusedPatch:profile 里留着指向旧版本 dsh-plugin 的 patch 声明,本次安装解析到的版本
|
|
192
192
|
* 已经不是它(`ERR_PNPM_UNUSED_PATCH` / `The following patches were not used: dsh-plugin@1.4.2`)
|
|
193
193
|
* —— pnpm 发现补丁没被用上即中止整次安装 → 提示删掉该条目后重试
|
|
194
|
+
* - pnpmMissingDep:profile 里留着一条指向 registry 上不存在的包的依赖,pnpm 解析时收到 404
|
|
195
|
+
* (`ERR_PNPM_FETCH_404` / `Not Found - 404`)—— 该 profile 的任何安装都会先卡在这条依赖上
|
|
196
|
+
* → 提示按包名删掉该条目后重试(404 的包通常不是正在安装的插件,故不引导提 Issue)
|
|
194
197
|
* - fileLocked:pnpm 无法替换 profile 里被其他进程占用的文件(Windows `os error 32`
|
|
195
198
|
* 「另一个程序正在使用此文件」/ `EBUSY` / `resource busy or locked`)—— 通常是宿主进程或
|
|
196
199
|
* 杀毒软件实时扫描持有句柄 → 提示完全退出宿主后重试
|
|
@@ -311,8 +314,18 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
311
314
|
// 装后校验拦截(服务端 verifyInstalledEntry 标记):入口文件缺失 = git 分发缺构建产物,
|
|
312
315
|
// 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
|
|
313
316
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
314
|
-
//
|
|
315
|
-
|
|
317
|
+
// 依赖在 registry 上不存在(`ERR_PNPM_FETCH_404` / `Not Found - 404`):pnpm 连得上 registry,
|
|
318
|
+
// 是被明确告知「这个包不存在」—— 属于 profile 的本地依赖问题,不是插件问题。profile 里留着
|
|
319
|
+
// 一条指向未发布包的依赖条目时,该 profile 的**任何**安装都会先卡在这条依赖上。
|
|
320
|
+
// 必须在 pluginPrepare 之前判定:此时宿主尾部只有通用的 `dsh: plugin command failed`,
|
|
321
|
+
// 落到下面会被当成构建失败。
|
|
322
|
+
// (两个形态:pnpm 的 `[ERR_PNPM_FETCH_404] GET <url>: Not Found - 404` 与 npm 的
|
|
323
|
+
// `npm error 404 Not Found - GET <url> - Not found`)
|
|
324
|
+
if (/ERR_PNPM_FETCH_404|404 Not Found|Not Found - 404/i.test(message)) return 'pnpmMissingDep'
|
|
325
|
+
// 再判 prepare 实际执行失败:只有构建脚本真的跑挂了才是插件问题。
|
|
326
|
+
// `Command failed` 区分大小写 —— 只匹配 pnpm 包装出的报错,不匹配宿主那句小写的
|
|
327
|
+
// `dsh: plugin command failed`(那只是「这条命令失败了」的通用说明)。
|
|
328
|
+
if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|prepare-guard/i.test(message) || /Command failed/.test(message)) return 'pluginPrepare'
|
|
316
329
|
// 其余失败(含 git prepare 被 pnpm 白名单拦截):当前通道装不上 = 插件分发/依赖的问题,一律提 Issue
|
|
317
330
|
return 'repo'
|
|
318
331
|
}
|
|
@@ -371,12 +384,25 @@ export function summarizeError(message: string, maxChars: number = MAX_CORE_CHAR
|
|
|
371
384
|
return out.length > maxChars ? `${out.slice(0, maxChars)}\n… (truncated)` : out
|
|
372
385
|
}
|
|
373
386
|
|
|
374
|
-
/** 提取首个错误代码(如 ERR_PNPM_PREPARE_PACKAGE),无则 null。
|
|
387
|
+
/** 提取首个错误代码(如 ERR_PNPM_PREPARE_PACKAGE),无则 null。
|
|
388
|
+
* 代码里可能含数字(ERR_PNPM_FETCH_404 / ERR_PNPM_EBADPLATFORM_...),字符类需带上 0-9,
|
|
389
|
+
* 否则会被截断成 `ERR_PNPM_FETCH_`。 */
|
|
375
390
|
export function coreErrorCode(message: string): string | null {
|
|
376
|
-
const m = message.match(/\[?ERR_[A-
|
|
391
|
+
const m = message.match(/\[?ERR_[A-Z0-9_]+\]?/)
|
|
377
392
|
return m ? m[0].replace(/^\[|\]$/g, '') : null
|
|
378
393
|
}
|
|
379
394
|
|
|
395
|
+
/** 提取「registry 上不存在的包名」:从 404 行(`ERR_PNPM_FETCH_404 GET <registry>/<pkg>: Not Found - 404`)
|
|
396
|
+
* 里取请求路径中的包名(含 scope,如 `@scope/name`);非 404 或提取不到返回 null。 */
|
|
397
|
+
export function missingRegistryPackageOf(message: string): string | null {
|
|
398
|
+
for (const line of message.split(/\r?\n/)) {
|
|
399
|
+
if (!/ERR_PNPM_FETCH_404|404/.test(line)) continue
|
|
400
|
+
const m = line.match(/GET\s+https?:\/\/[^\s/]+\/((?:@[^/\s:]+\/)?[^/\s:]+)/)
|
|
401
|
+
if (m) return m[1]
|
|
402
|
+
}
|
|
403
|
+
return null
|
|
404
|
+
}
|
|
405
|
+
|
|
380
406
|
/**
|
|
381
407
|
* 从网络类失败消息里提取「具体连不上的地址」,供弹窗精准提示(你的网络无法访问什么)。
|
|
382
408
|
* 优先取服务端 [network] 预检消息括号里的探测地址(中文「(…)」/ 英文「(…)」),
|
|
@@ -4,20 +4,29 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* Install target/command helpers: decide the install channel (npm package
|
|
7
|
-
* vs explicit-HTTPS GitHub), build the display
|
|
8
|
-
* install spec back to its owner/repo identity.
|
|
7
|
+
* vs prebuilt GitHub release vs explicit-HTTPS GitHub), build the display
|
|
8
|
+
* command and normalize a raw install spec back to its owner/repo identity.
|
|
9
9
|
*/
|
|
10
10
|
import type { HubPlugin } from '../types.ts'
|
|
11
|
+
import { githubReleaseTarget } from '../../server/services/install/release-target.ts'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 安装通道决策(用户无感知):目录探测到 npm 包名 → 用 npm 包名安装
|
|
14
|
-
* (走 npm registry tarball,更快、与 GitHub
|
|
15
|
-
*
|
|
15
|
+
* (走 npm registry tarball,更快、与 GitHub 网络无关);其次用「同仓库」的权威
|
|
16
|
+
* release 包直链(仓库 HEAD 往往是 monorepo 根、装不上,release .tgz 才是完整产物);
|
|
17
|
+
* 都没有 → git 直装。
|
|
18
|
+
* 返回值 target 即传给后端 /install 的安装目标(npm 包名 / release URL / owner/repo)。
|
|
16
19
|
*/
|
|
17
|
-
export function installTargetOf(p: HubPlugin): { target: string; via: 'npm' | 'github' } {
|
|
20
|
+
export function installTargetOf(p: HubPlugin): { target: string; via: 'npm' | 'release' | 'github' } {
|
|
18
21
|
const pkg = (p.source?.npmPackage ?? '').trim()
|
|
19
22
|
const repo = (p.source?.repo ?? '').trim()
|
|
20
23
|
if (pkg && repo) return { target: pkg, via: 'npm' }
|
|
24
|
+
// 目录下发的权威命令若是「同仓库」的固定 release 直链,改走该预构建包;
|
|
25
|
+
// 跨仓 / 非法 release 目标不采信,退回 git 直装。
|
|
26
|
+
const release = githubReleaseTarget(p.install?.githubCommand ?? '')
|
|
27
|
+
if (release !== null && repo !== '' && release.repo.toLowerCase() === repo.toLowerCase()) {
|
|
28
|
+
return { target: release.target, via: 'release' }
|
|
29
|
+
}
|
|
21
30
|
return { target: repo, via: 'github' }
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -30,12 +39,13 @@ export function installCommandOf(p: HubPlugin, withProfile = false): string {
|
|
|
30
39
|
const cmd = p.install?.command
|
|
31
40
|
if (cmd) return cmd
|
|
32
41
|
} else {
|
|
42
|
+
// release 与 git 通道共用目录下发的 githubCommand(git+ 显式 HTTPS 或 release 直链)
|
|
33
43
|
const cmd = p.install?.githubCommand
|
|
34
44
|
if (cmd) return cmd
|
|
35
45
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const prefix = `dsh plugin${withProfile ? ' --profile web' : ''} add `
|
|
47
|
+
if (via === 'npm' || via === 'release') return `${prefix}${target}`
|
|
48
|
+
return `${prefix}git+https://github.com/${target}.git`
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
/** Normalize a task/install target to its owner/repo display identity. */
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -53,6 +53,7 @@ function reasonTitleOf(kind: FailureKind): string {
|
|
|
53
53
|
case 'originRejected': return 'request origin rejected by the local service on the user machine'
|
|
54
54
|
case 'pnpmPolicy': return 'pnpm supply-chain policy blocked the install on the user machine'
|
|
55
55
|
case 'pnpmUnusedPatch': return 'stale pnpm patch entry on the user machine'
|
|
56
|
+
case 'pnpmMissingDep': return 'dependency not found on the npm registry on the user machine'
|
|
56
57
|
case 'fileLocked': return 'file locked by another process on the user machine'
|
|
57
58
|
case 'accessDenied': return 'file write access denied on the user machine'
|
|
58
59
|
case 'fsUnavailable': return 'local file system unavailable on the user machine'
|
|
@@ -15,6 +15,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
15
15
|
import { spawn, spawnSync } from 'node:child_process'
|
|
16
16
|
import { fetchViaCurl, gitLsRemote, probeUrl, systemProxy } from '../services/probe.ts'
|
|
17
17
|
import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, globalNpmPackagesOf, hasQueuedTarget, installTargetOf, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName, type LoaderHandle } from '../services/install/install.ts'
|
|
18
|
+
import { githubReleaseTarget } from '../services/install/release-target.ts'
|
|
18
19
|
import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/profile/installed-versions.ts'
|
|
19
20
|
import { resolveNpmPackage } from '../services/install/npm-resolve.ts'
|
|
20
21
|
import { preflightTarget } from '../services/install/preflight.ts'
|
|
@@ -791,12 +792,14 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
791
792
|
sendJson(response, 200, { ok: true, task: task.id })
|
|
792
793
|
return
|
|
793
794
|
}
|
|
794
|
-
//
|
|
795
|
-
//
|
|
795
|
+
// 目标语法三态:固定 GitHub release .tgz(保留其包根目录,不转成仓库 HEAD);
|
|
796
|
+
// GitHub 地址(owner/repo、github:、https/ssh 链接等任意写法)→ 显式 HTTPS Git 源
|
|
797
|
+
// (走装前预检);npm 包名(@scope/name 或 name)→ 信任 registry 直接安装。
|
|
796
798
|
// githubRepoOf 先把各种 GitHub 地址归一成 owner/repo,让「输入地址即装」兼容粘贴完整链接。
|
|
797
799
|
const gitRepo = githubRepoOf(rawRepo)
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
+
const release = githubReleaseTarget(rawRepo)
|
|
801
|
+
const repoTarget = gitRepo !== null && release === null ? githubTarget(gitRepo) : null
|
|
802
|
+
let target: string | null = release?.target ?? repoTarget ?? (validPackageName(rawRepo) ? rawRepo : null)
|
|
800
803
|
if (target === null) {
|
|
801
804
|
sendJson(response, 400, { error: 'unsupported install target' })
|
|
802
805
|
return
|
|
@@ -825,9 +828,9 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
825
828
|
}
|
|
826
829
|
// 命令行安装(目录外)按通道门禁:GitHub 源码通道受「启用 GitHub 源码安装」控制,
|
|
827
830
|
// npm 通道受「启用 NPM 安装」控制 —— 目录插件安装走目录白名单,不受开关影响。
|
|
828
|
-
// 通道判定看反查后的最终 target
|
|
831
|
+
// 通道判定看反查后的最终 target:release 包与 git 目标同属 GitHub 通道,否则走 npm。
|
|
829
832
|
if (source !== 'catalog') {
|
|
830
|
-
const isGitChannel = repoTarget !== null && target === repoTarget
|
|
833
|
+
const isGitChannel = release !== null || (repoTarget !== null && target === repoTarget)
|
|
831
834
|
if (isGitChannel && !settings.enableGitInstall) {
|
|
832
835
|
sendJson(response, 403, { error: 'git installs are disabled by the security settings' })
|
|
833
836
|
return
|
|
@@ -863,8 +866,12 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
863
866
|
const effectiveProxy = settings.proxy !== ''
|
|
864
867
|
? settings.proxy
|
|
865
868
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
866
|
-
const isGitChannel = repoTarget !== null && target === repoTarget
|
|
867
|
-
|
|
869
|
+
const isGitChannel = release !== null || (repoTarget !== null && target === repoTarget)
|
|
870
|
+
// release 包是 https://github.com/ 直链下载(非 git 克隆),探针按 HTTP 打 github.com 主页;
|
|
871
|
+
// npm 通道探 registry 根。两者都是通道的稳定入口,不探具体包/仓库(404 会被误判成网络故障)。
|
|
872
|
+
const httpProbeTarget = release !== null
|
|
873
|
+
? 'https://github.com/'
|
|
874
|
+
: `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`
|
|
868
875
|
// git 通道的探针换成克隆握手本身:git ls-remote 走的就是 pnpm 克隆前的同一套
|
|
869
876
|
// https 传输。curl 打 github.com 主页会被按协议/端口区分的防火墙与 TLS 复检
|
|
870
877
|
// 拦截(能 git 克隆、打不开网页),把可装的仓库误判成 [network] 而中止
|
|
@@ -880,9 +887,9 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
880
887
|
if (reachable.ok) net = reachable
|
|
881
888
|
}
|
|
882
889
|
} else {
|
|
883
|
-
net = await probeUrl(
|
|
890
|
+
net = await probeUrl(httpProbeTarget, effectiveProxy, 6000)
|
|
884
891
|
}
|
|
885
|
-
const probeTarget = gitProbeTarget ??
|
|
892
|
+
const probeTarget = gitProbeTarget ?? httpProbeTarget
|
|
886
893
|
if (!net.ok) {
|
|
887
894
|
// 错误消息按客户端界面语言提示:中文界面给中文、英文界面给英文,
|
|
888
895
|
// 让用户一眼看懂是网络问题而非插件问题
|