dsh-plugin 1.6.3 → 1.6.5
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 +10 -10
- package/README.md +10 -10
- package/client/client.js +318 -304
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +8 -6
- package/lib/services/install/npm-resolve.js +44 -15
- package/lib/services/probe.js +17 -0
- package/lib/types/services/install/npm-resolve.d.ts +3 -2
- package/lib/types/services/probe.d.ts +8 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +4 -2
- package/src/client/components/modals/HubUpdateModal.tsx +19 -10
- package/src/client/hooks/useCatalog.ts +13 -9
- package/src/client/locales.ts +6 -2
- package/src/client/logic/urls.ts +2 -2
- package/src/client/types.ts +11 -0
- package/src/server/http/routes.ts +8 -6
- package/src/server/services/install/npm-resolve.ts +41 -13
- package/src/server/services/probe.ts +18 -0
package/lib/http/routes.js
CHANGED
|
@@ -843,6 +843,11 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
843
843
|
return;
|
|
844
844
|
}
|
|
845
845
|
const attempts = [];
|
|
846
|
+
// 代理口径与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
847
|
+
// 供 npm 反查时跟随 GitHub 改名跳转用(Node https 读不到系统代理,交给 curl)。
|
|
848
|
+
const effectiveProxy = settings.proxy !== ''
|
|
849
|
+
? settings.proxy
|
|
850
|
+
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
846
851
|
// Catalog metadata may outlive a packaging split: one repository can publish both a
|
|
847
852
|
// CLI/helper package and a dedicated DSH plugin package. Keep the catalog target when
|
|
848
853
|
// registry metadata confirms it is a DSH package for this repo. Otherwise inspect the
|
|
@@ -852,7 +857,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
852
857
|
// correct package even with a large result window.
|
|
853
858
|
const catalogRepo = source === 'catalog' ? githubRepoOf(displayRepo) : null;
|
|
854
859
|
if (catalogRepo !== null && release === null && repoTarget === null && validPackageName(rawRepo)) {
|
|
855
|
-
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry);
|
|
860
|
+
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry, effectiveProxy);
|
|
856
861
|
if (catalogTargetIsDsh) {
|
|
857
862
|
recordResolvedNpmPackage(profile, catalogRepo, rawRepo);
|
|
858
863
|
}
|
|
@@ -862,7 +867,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
862
867
|
const root = await preflightTarget(catalogGitTarget);
|
|
863
868
|
const rootName = root.ok && root.name !== null && validPackageName(root.name) ? root.name : null;
|
|
864
869
|
if (rootName !== null && rootName !== rawRepo
|
|
865
|
-
&& await isDshNpmPackageForRepo(rootName, catalogRepo, settings.npmRegistry)) {
|
|
870
|
+
&& await isDshNpmPackageForRepo(rootName, catalogRepo, settings.npmRegistry, effectiveProxy)) {
|
|
866
871
|
attempts.push(`catalog npm target: \`${target}\` → repository root DSH package \`${rootName}\``);
|
|
867
872
|
target = rootName;
|
|
868
873
|
recordResolvedNpmPackage(profile, catalogRepo, rootName);
|
|
@@ -881,7 +886,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
881
886
|
if (repoTarget !== null && pinnedRef === null) {
|
|
882
887
|
// 反查/记录统一用归一化后的 owner/repo(gitRepo),保证「输入完整链接」也走同一套 npm 反查
|
|
883
888
|
const repoIdentity = gitRepo ?? rawRepo;
|
|
884
|
-
const npmName = await resolveNpmPackage(repoIdentity, settings.npmRegistry);
|
|
889
|
+
const npmName = await resolveNpmPackage(repoIdentity, settings.npmRegistry, effectiveProxy);
|
|
885
890
|
if (npmName !== null) {
|
|
886
891
|
target = npmName;
|
|
887
892
|
attempts.push(`npm registry search: \`npm search repository:${repoIdentity}\` → found \`${npmName}\``);
|
|
@@ -931,9 +936,6 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
931
936
|
// 插件侧问题引导去作者仓库提 Issue。
|
|
932
937
|
// 代理口径与诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
933
938
|
if (already === null) {
|
|
934
|
-
const effectiveProxy = settings.proxy !== ''
|
|
935
|
-
? settings.proxy
|
|
936
|
-
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
937
939
|
const isGitChannel = release !== null || (repoTarget !== null && target === repoTarget);
|
|
938
940
|
// release 包是 https://github.com/ 直链下载(非 git 克隆),探针按 HTTP 打 github.com 主页;
|
|
939
941
|
// npm 通道探 registry 根。两者都是通道的稳定入口,不探具体包/仓库(404 会被误判成网络故障)。
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { get } from 'node:https';
|
|
15
15
|
import { githubRepoOf } from '../profile/profile.js';
|
|
16
|
+
import { finalUrl } from '../probe.js';
|
|
16
17
|
/** 反查缓存:repo 小写 → npm 包名(null 表示已确认无对应 npm 包;网络异常不缓存)。 */
|
|
17
18
|
const cache = new Map();
|
|
18
19
|
/** 单次 registry 查询超时(ms):慢网络下失败返回 null,不阻塞安装流程。 */
|
|
@@ -27,18 +28,24 @@ const SEARCH_RESULT_LIMIT = 250;
|
|
|
27
28
|
* 安装仍会重试反查,避免一次瞬时故障把整个会话锁死在 git 通道。
|
|
28
29
|
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
29
30
|
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
31
|
+
* proxy 参数:GitHub 改名跳转探测走的代理(与安装/诊断同一口径)。
|
|
30
32
|
* 慢网络下失败不阻塞安装。
|
|
31
33
|
*/
|
|
32
|
-
export function resolveNpmPackage(repo, registry = '') {
|
|
34
|
+
export async function resolveNpmPackage(repo, registry = '', proxy = '') {
|
|
33
35
|
const key = repo.toLowerCase();
|
|
34
36
|
if (cache.has(key))
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
return cache.get(key) ?? null;
|
|
38
|
+
let found = await searchRepo(repo, registry);
|
|
39
|
+
// 仓库改名后 npm 包的 repository 已指向新名,按旧名反查不到:跟随一次 GitHub 的
|
|
40
|
+
// 改名跳转拿到规范名再查一遍(旧地址本身仍可用,只是仓库名变了)。
|
|
41
|
+
if (found === null) {
|
|
42
|
+
const canonical = await canonicalRepo(repo, proxy);
|
|
43
|
+
if (canonical !== '' && canonical.toLowerCase() !== key)
|
|
44
|
+
found = await searchRepo(canonical, registry);
|
|
45
|
+
}
|
|
46
|
+
if (found !== undefined)
|
|
47
|
+
cache.set(key, found);
|
|
48
|
+
return found ?? null;
|
|
42
49
|
}
|
|
43
50
|
/**
|
|
44
51
|
* Pick the installable npm package for one GitHub repository. npm search is a
|
|
@@ -65,7 +72,7 @@ export function isDshPackageMetadataForRepo(pkg, repo) {
|
|
|
65
72
|
const manifestMarked = pkg.dsh !== null && typeof pkg.dsh === 'object';
|
|
66
73
|
return keywordMarked || manifestMarked;
|
|
67
74
|
}
|
|
68
|
-
export function isDshNpmPackageForRepo(packageName, repo, registry = '') {
|
|
75
|
+
export function isDshNpmPackageForRepo(packageName, repo, registry = '', proxy = '') {
|
|
69
76
|
if (packageName === '' || repo === '')
|
|
70
77
|
return Promise.resolve(false);
|
|
71
78
|
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '');
|
|
@@ -80,18 +87,40 @@ export function isDshNpmPackageForRepo(packageName, repo, registry = '') {
|
|
|
80
87
|
let body = '';
|
|
81
88
|
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
82
89
|
res.on('end', () => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
void (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const pkg = JSON.parse(body);
|
|
93
|
+
if (isDshPackageMetadataForRepo(pkg, repo)) {
|
|
94
|
+
resolve(true);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// 仓库改名后包元数据指向新名:按原名不匹配时,用 GitHub 跳转后的规范名复核一次
|
|
98
|
+
const canonical = await canonicalRepo(repo, proxy);
|
|
99
|
+
resolve(canonical !== '' && isDshPackageMetadataForRepo(pkg, canonical));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
resolve(false);
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
89
105
|
});
|
|
90
106
|
});
|
|
91
107
|
req.on('timeout', () => req.destroy());
|
|
92
108
|
req.on('error', () => resolve(false));
|
|
93
109
|
});
|
|
94
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* 解析仓库改名后的规范 `owner/repo`:GitHub 对已改名仓库返回 301,跟随跳转取最终地址。
|
|
113
|
+
* 与其它 GitHub 访问同口径走 curl + 代理 env(Node 内置 https 读不到系统代理)。
|
|
114
|
+
* 未改名、仓库不存在、网络失败或 curl 不可用都返回 '',调用方按原名继续。
|
|
115
|
+
*/
|
|
116
|
+
async function canonicalRepo(repo, proxy) {
|
|
117
|
+
if (githubRepoOf(repo) === null)
|
|
118
|
+
return '';
|
|
119
|
+
const key = repo.toLowerCase();
|
|
120
|
+
const final = await finalUrl(`https://github.com/${repo}`, proxy, REQUEST_TIMEOUT_MS);
|
|
121
|
+
const canonical = final === null ? null : githubRepoOf(final);
|
|
122
|
+
return canonical !== null && canonical.toLowerCase() !== key ? canonical : '';
|
|
123
|
+
}
|
|
95
124
|
function matchingPackagesForRepo(objects, repo) {
|
|
96
125
|
const matches = [];
|
|
97
126
|
for (const obj of objects) {
|
package/lib/services/probe.js
CHANGED
|
@@ -235,6 +235,23 @@ export function gitLsRemote(url, proxy, timeoutMs) {
|
|
|
235
235
|
const timer = setTimeout(() => { child.kill(); finish(probeFail()); }, timeoutMs + 1000);
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
|
|
240
|
+
* 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
|
|
241
|
+
* 即规范地址。与其它 GitHub 访问同口径 —— curl 子进程注入代理 env(Node 内置
|
|
242
|
+
* http(s) 读不到系统代理,直连在部分网络下不可达)。
|
|
243
|
+
* 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
|
|
244
|
+
*/
|
|
245
|
+
export async function finalUrl(url, proxy, timeoutMs) {
|
|
246
|
+
const discard = process.platform === 'win32' ? 'NUL' : '/dev/null';
|
|
247
|
+
const r = await curlWithTlsFallback([
|
|
248
|
+
'-sS', '-I', '-L', '-o', discard, '-w', '%{url_effective}',
|
|
249
|
+
'--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))),
|
|
250
|
+
url,
|
|
251
|
+
], proxyEnv(proxy), timeoutMs);
|
|
252
|
+
const final = r.stdout.trim();
|
|
253
|
+
return r.code === 0 && final !== '' ? final : null;
|
|
254
|
+
}
|
|
238
255
|
/**
|
|
239
256
|
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数兜底)。
|
|
240
257
|
* 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* 安装仍会重试反查,避免一次瞬时故障把整个会话锁死在 git 通道。
|
|
7
7
|
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
8
8
|
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
9
|
+
* proxy 参数:GitHub 改名跳转探测走的代理(与安装/诊断同一口径)。
|
|
9
10
|
* 慢网络下失败不阻塞安装。
|
|
10
11
|
*/
|
|
11
|
-
export declare function resolveNpmPackage(repo: string, registry?: string): Promise<string | null>;
|
|
12
|
+
export declare function resolveNpmPackage(repo: string, registry?: string, proxy?: string): Promise<string | null>;
|
|
12
13
|
type NpmSearchPackage = {
|
|
13
14
|
name?: unknown;
|
|
14
15
|
keywords?: unknown;
|
|
@@ -22,6 +23,6 @@ type NpmSearchObject = {
|
|
|
22
23
|
package?: NpmSearchPackage;
|
|
23
24
|
};
|
|
24
25
|
export declare function isDshPackageMetadataForRepo(pkg: NpmSearchPackage, repo: string): boolean;
|
|
25
|
-
export declare function isDshNpmPackageForRepo(packageName: string, repo: string, registry?: string): Promise<boolean>;
|
|
26
|
+
export declare function isDshNpmPackageForRepo(packageName: string, repo: string, registry?: string, proxy?: string): Promise<boolean>;
|
|
26
27
|
export declare function selectNpmPackageForRepo(objects: NpmSearchObject[], repo: string): string | null;
|
|
27
28
|
export {};
|
|
@@ -59,6 +59,14 @@ export declare function probeUrl(url: string, proxy: string, timeoutMs: number):
|
|
|
59
59
|
* 不打 17MB 的 dsh-plugin-hub 主页)。
|
|
60
60
|
*/
|
|
61
61
|
export declare function gitLsRemote(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult>;
|
|
62
|
+
/**
|
|
63
|
+
* 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
|
|
64
|
+
* 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
|
|
65
|
+
* 即规范地址。与其它 GitHub 访问同口径 —— curl 子进程注入代理 env(Node 内置
|
|
66
|
+
* http(s) 读不到系统代理,直连在部分网络下不可达)。
|
|
67
|
+
* 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
|
|
68
|
+
*/
|
|
69
|
+
export declare function finalUrl(url: string, proxy: string, timeoutMs: number): Promise<string | null>;
|
|
62
70
|
/**
|
|
63
71
|
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数兜底)。
|
|
64
72
|
* 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
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",
|
|
@@ -533,12 +533,14 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
533
533
|
onConsumedOpenSection: () => setSettingsSection(null),
|
|
534
534
|
}),
|
|
535
535
|
// Hub 版本信息弹窗:版本号/徽标点击打开 —— 有更新显示新版本 + 变更记录 + 「直接更新」,
|
|
536
|
-
//
|
|
536
|
+
// 暂缓提示显示同一标题但只给「稍后再说」,无更新显示当前版本 + 记录 + 「已是最新」。
|
|
537
|
+
// 版本号与本机版本一律由 hubUpdateState / hubCurrentVersion 决定,不用远端版号冒充本机。
|
|
537
538
|
showHubUpdate && h(HubUpdateModal, {
|
|
538
539
|
info: catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
|
|
540
|
+
state: catalog.hubUpdateState,
|
|
541
|
+
currentVersion: catalog.hubCurrentVersion,
|
|
539
542
|
lang,
|
|
540
543
|
t,
|
|
541
|
-
hasUpdate: catalog.hubHasUpdate,
|
|
542
544
|
onClose: () => setShowHubUpdate(false),
|
|
543
545
|
onProceed: () => {
|
|
544
546
|
setShowHubUpdate(false)
|
|
@@ -4,30 +4,39 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* Hub 版本信息 / 自我更新说明弹窗:
|
|
7
|
-
* - 有更新(
|
|
7
|
+
* - 有更新(state='available'):标题「有新版本」,展示新版本号、发布时间与 Worker 下发的
|
|
8
8
|
* Markdown 变更记录(renderMarkdown 渲染,双语言按界面语言取 {zh,en} 对象),
|
|
9
9
|
* 按钮「稍后再说 / 直接更新」,确认后进入安装弹窗执行覆盖重装。
|
|
10
|
-
* -
|
|
10
|
+
* - 暂缓提示(state='pending'):远端确有新版本,但发布未满供应链安全门槛,pnpm 此时会
|
|
11
|
+
* 静默回退到旧版本 —— 仍按「有新版本」展示,只给「稍后再说」,并说明稍后会自动提示。
|
|
12
|
+
* - 无更新 / 信息缺失(state='latest' / 'none'):标题「当前版本」,版本号与本机版本取自
|
|
13
|
+
* currentVersion(构建注入的 PLUGIN_VERSION),**不得**用远端最新版号冒充本机版本。
|
|
11
14
|
* 点击头部版本号(或「可更新」徽标)打开 —— 无论有无更新都能看到更新内容。
|
|
12
15
|
*/
|
|
13
16
|
import { createElement as h } from 'react'
|
|
14
17
|
import type { MouseEvent } from 'react'
|
|
15
18
|
import styles from '../../styles/Modal.module.css'
|
|
16
|
-
import type { HubUpdateInfo } from '../../types.ts'
|
|
19
|
+
import type { HubUpdateInfo, HubUpdateState } from '../../types.ts'
|
|
17
20
|
import type { LocaleId, Translate } from '../../types.ts'
|
|
18
21
|
import { renderMarkdown } from '../../logic/renderMarkdown.ts'
|
|
19
22
|
import { CloseIcon } from '../ui/icons.tsx'
|
|
20
23
|
|
|
21
|
-
export function HubUpdateModal({ info, lang, t,
|
|
24
|
+
export function HubUpdateModal({ info, state, currentVersion, lang, t, onProceed, onClose }: {
|
|
22
25
|
info: HubUpdateInfo
|
|
23
26
|
lang: LocaleId
|
|
24
27
|
t: Translate
|
|
25
|
-
/**
|
|
26
|
-
|
|
28
|
+
/** Hub 自我更新状态(见 types.ts):决定标题 / 正文 / meta 版本号 / 按钮 */
|
|
29
|
+
state: HubUpdateState
|
|
30
|
+
/** 本机实际运行的版本 —— 「当前版本」与「暂缓提示」的版本号一律用它 */
|
|
31
|
+
currentVersion: string
|
|
27
32
|
/** 点「直接更新」:关闭本弹窗,进入安装弹窗的更新流程 */
|
|
28
33
|
onProceed: () => void
|
|
29
34
|
onClose: () => void
|
|
30
35
|
}) {
|
|
36
|
+
const hasUpdate = state === 'available'
|
|
37
|
+
const pending = state === 'pending'
|
|
38
|
+
// 展示的版本号:提示更新/暂缓提示时是远端新版本;否则是本机当前版本
|
|
39
|
+
const shownVersion = hasUpdate || pending ? info.version : currentVersion || info.version
|
|
31
40
|
// 变更记录:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则不出记录区
|
|
32
41
|
const notesRaw = typeof info.notes === 'string'
|
|
33
42
|
? info.notes
|
|
@@ -54,7 +63,7 @@ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }:
|
|
|
54
63
|
},
|
|
55
64
|
h('div', { className: `${styles.modal} ${styles.hubUpdateModal}`, role: 'dialog', 'aria-modal': 'true' },
|
|
56
65
|
h('div', { className: styles.modalHead },
|
|
57
|
-
h('div', { className: styles.modalTitle }, t(hasUpdate ? 'hubUpdateTitle' : 'hubCurrentTitle')),
|
|
66
|
+
h('div', { className: styles.modalTitle }, t(hasUpdate || pending ? 'hubUpdateTitle' : 'hubCurrentTitle')),
|
|
58
67
|
h('button', {
|
|
59
68
|
className: styles.modalClose,
|
|
60
69
|
'aria-label': t('confirmCancel'),
|
|
@@ -62,9 +71,9 @@ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }:
|
|
|
62
71
|
}, h(CloseIcon)),
|
|
63
72
|
),
|
|
64
73
|
h('div', { className: styles.modalDesc },
|
|
65
|
-
t(hasUpdate ? 'hubUpdateDesc' : 'hubCurrentDesc', { version:
|
|
74
|
+
t(hasUpdate ? 'hubUpdateDesc' : pending ? 'hubPendingDesc' : 'hubCurrentDesc', { version: shownVersion })),
|
|
66
75
|
h('div', { className: styles.hubUpdateMeta },
|
|
67
|
-
h('span', { className: styles.hubUpdateMetaItem }, `${t('version')} ${
|
|
76
|
+
h('span', { className: styles.hubUpdateMetaItem }, `${t('version')} ${shownVersion}`),
|
|
68
77
|
published
|
|
69
78
|
? h('span', { className: styles.hubUpdateMetaItem }, `${t('hubUpdatePublished')} ${published}`)
|
|
70
79
|
: null,
|
|
@@ -81,7 +90,7 @@ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }:
|
|
|
81
90
|
h('button', { className: styles.restartLater, onClick: onClose }, t('hubUpdateLater')),
|
|
82
91
|
h('button', { className: styles.modalInstall, onClick: onProceed }, t('updateNow')),
|
|
83
92
|
]
|
|
84
|
-
: h('button', { className: styles.modalInstall, onClick: onClose }, t('hubUpToDate')),
|
|
93
|
+
: h('button', { className: styles.modalInstall, onClick: onClose }, t(pending ? 'hubUpdateLater' : 'hubUpToDate')),
|
|
85
94
|
),
|
|
86
95
|
),
|
|
87
96
|
)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
13
13
|
import type { HubPlugin, LocaleId } from '../types.ts'
|
|
14
14
|
import { HUB_REPO, PLUGIN_VERSION } from '../logic/constants.ts'
|
|
15
|
-
import type { HubAboutInfo, HubUpdateInfo } from '../types.ts'
|
|
15
|
+
import type { HubAboutInfo, HubUpdateInfo, HubUpdateState } from '../types.ts'
|
|
16
16
|
import type { SortKey } from '../logic/constants.ts'
|
|
17
17
|
import { fetchCatalog, fetchStats } from '../data/catalog.ts'
|
|
18
18
|
import { fetchHubAbout, fetchHubUpdate } from '../data/hub.ts'
|
|
@@ -196,30 +196,32 @@ export function useCatalog(lang: LocaleId) {
|
|
|
196
196
|
)
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
|
-
* Hub
|
|
199
|
+
* Hub 自身更新状态:Hub 就是当前运行的应用(始终已安装,无需 installedName 命中)。
|
|
200
200
|
* 以 Worker 版本控制中心返回的「最新版本」为准,与当前运行的版本号比对——
|
|
201
201
|
* 构建时注入的 PLUGIN_VERSION 是运行 bundle 的真实版本;测试/异常场景缺失时
|
|
202
202
|
* 回退到安装时记录的版本。版本号不等即新版(版本号只在发版时变更,绝无降级场景)。
|
|
203
203
|
*
|
|
204
204
|
* 另加 pnpm 供应链安全门槛:新版本发布还不满 24 小时(+30 分钟缓冲)时,pnpm 会静默
|
|
205
205
|
* 回退到旧版本,此时若提示「可更新」用户点了也更新不到。publishedAt 由接口中心按 npm
|
|
206
|
-
* registry
|
|
206
|
+
* registry 真实发布时间下发(同源),据此判断最准。门槛命中要单列成 `pending` 而不是
|
|
207
|
+
* 落回「无更新」——回落会让弹窗在本机版本落后时谎称「已是最新」(issue #145)。
|
|
207
208
|
*/
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
209
|
+
const hubCurrentVersion = PLUGIN_VERSION || versions[HUB_REPO]?.version || ''
|
|
210
|
+
const hubUpdateState: HubUpdateState = (() => {
|
|
211
|
+
if (!hubUpdateInfo) return 'none'
|
|
212
|
+
if (hubCurrentVersion === '' || hubUpdateInfo.version === hubCurrentVersion) return 'latest'
|
|
212
213
|
const publishedAt = hubUpdateInfo.publishedAt
|
|
213
214
|
if (publishedAt) {
|
|
214
215
|
const publishedMs = Date.parse(publishedAt)
|
|
215
216
|
if (!Number.isNaN(publishedMs)) {
|
|
216
217
|
// 24 小时 + 30 分钟缓冲(防网络/CDN 边缘时间偏差)
|
|
217
218
|
const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
|
|
218
|
-
if (Date.now() - publishedMs < MIN_RELEASE_AGE_MS) return
|
|
219
|
+
if (Date.now() - publishedMs < MIN_RELEASE_AGE_MS) return 'pending'
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
|
-
return
|
|
222
|
+
return 'available'
|
|
222
223
|
})()
|
|
224
|
+
const hubHasUpdate = hubUpdateState === 'available'
|
|
223
225
|
|
|
224
226
|
/** 当前分类下的插件(「全部」时为整个目录)。 */
|
|
225
227
|
const categoryPlugins = useMemo(() => {
|
|
@@ -271,6 +273,8 @@ export function useCatalog(lang: LocaleId) {
|
|
|
271
273
|
installedVersion,
|
|
272
274
|
hasUpdate,
|
|
273
275
|
hubHasUpdate,
|
|
276
|
+
hubUpdateState,
|
|
277
|
+
hubCurrentVersion,
|
|
274
278
|
hubUpdateInfo,
|
|
275
279
|
hubAboutInfo,
|
|
276
280
|
refreshHub,
|
package/src/client/locales.ts
CHANGED
|
@@ -312,7 +312,7 @@ export const zh = {
|
|
|
312
312
|
// prepare 构建脚本实际执行失败(git 分发缺子模块/构建产物):插件打包分发问题,应提 Issue
|
|
313
313
|
failPrepareHint: '该插件安装时的构建脚本执行失败(git 分发常缺失子模块或构建产物),属插件打包分发问题。请向作者仓库提交 Issue 反馈。',
|
|
314
314
|
// 安装前预检 / 装后校验拦截([packaging]):git 分发缺构建产物、入口文件不存在 = 插件未适配官方默认安装方式
|
|
315
|
-
failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少构建产物(package.json 声明的入口文件在仓库中不存在),说明作者未适配官方安装流程。请到作者仓库提交 Issue
|
|
315
|
+
failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少构建产物(package.json 声明的入口文件在仓库中不存在),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物、改用预构建 release `.tgz` 直链(`releases/download/<tag>/<asset>.tgz`),或发布完整的 npm 版。',
|
|
316
316
|
// 原生依赖构建脚本被 pnpm 默认拦截(如 node-pty 的 ERR_PNPM_IGNORED_BUILDS):只影响带原生模块的插件,属插件依赖打包问题,应提 Issue
|
|
317
317
|
failIgnoredBuild: '该插件或其依赖需要执行构建脚本,被 pnpm 的安全白名单(allowBuilds)默认拦截导致安装失败(如原生模块 node-pty、或 git 安装时插件的 prepare 脚本)—— 其他插件不受影响,属该插件的依赖/打包问题。建议向作者仓库一键反馈,请作者改用预编译版本(如 node-pty-prebuilt-multiarch)或移除安装期构建。',
|
|
318
318
|
// npm arborist 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题,升级 npm 即可
|
|
@@ -380,6 +380,8 @@ export const zh = {
|
|
|
380
380
|
versionHint: '查看版本信息与更新记录',
|
|
381
381
|
hubCurrentTitle: '当前版本',
|
|
382
382
|
hubCurrentDesc: '你正在使用 v{version},已是最新版本。',
|
|
383
|
+
// 暂缓提示:远端确有新版本,但发布未满 24 小时(pnpm 供应链安全门槛),与「已是最新」严格区分
|
|
384
|
+
hubPendingDesc: '最新版 v{version} 已发布,但发布尚未满 24 小时:此时更新会被 pnpm 静默回退到旧版本,因此暂不提示。稍后这里会自动出现更新入口。',
|
|
383
385
|
hubUpToDate: '已是最新版本',
|
|
384
386
|
// 头部「关注我们」弹窗:平台介绍 + 用户反馈群二维码(内容由 Worker /about Markdown 推送,非写死)
|
|
385
387
|
followUs: '关注我们',
|
|
@@ -700,7 +702,7 @@ export const en = {
|
|
|
700
702
|
// prepare script actually failed (git tarball misses submodules/build output): a packaging issue of the plugin itself — file an issue
|
|
701
703
|
failPrepareHint: 'This plugin failed while running its build scripts during install (git tarballs often miss submodules or build output). This is a packaging issue of the plugin itself — please report it to the author repository.',
|
|
702
704
|
// Pre-install / post-install guard hit ([packaging]): git distribution lacks build output, entry file missing = not adapted to the official default install method
|
|
703
|
-
failPackagingHint: 'This plugin does not support the official default install method: its git distribution lacks the build output (the entry file declared in package.json is not in the repository), so it has not been adapted to the official install flow. Please file an Issue on the author\'s repo asking for committed build output or
|
|
705
|
+
failPackagingHint: 'This plugin does not support the official default install method: its git distribution lacks the build output (the entry file declared in package.json is not in the repository), so it has not been adapted to the official install flow. Please file an Issue on the author\'s repo asking for committed build output, a prebuilt release `.tgz` (`releases/download/<tag>/<asset>.tgz`), or a complete npm release.',
|
|
704
706
|
// native-module build scripts ignored by pnpm by default (e.g. node-pty, ERR_PNPM_IGNORED_BUILDS): only affects plugins with native deps — a dependency/packaging issue of the plugin itself, file an issue
|
|
705
707
|
failIgnoredBuild: 'This plugin (or its dependencies) needs to run build scripts, which pnpm blocks by default via its allowBuilds allowlist, so the install fails (e.g. native modules like node-pty, or the plugin\'s own prepare script on git installs) — other plugins are unaffected. This is a dependency/packaging issue of the plugin itself. Please report it to the author repository and ask the author to ship a prebuilt variant (e.g. node-pty-prebuilt-multiarch) or drop install-time builds.',
|
|
706
708
|
failNpmTooLow: 'Your local npm may be too old: npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
@@ -769,6 +771,8 @@ export const en = {
|
|
|
769
771
|
versionHint: 'View version info and changelog',
|
|
770
772
|
hubCurrentTitle: 'Current version',
|
|
771
773
|
hubCurrentDesc: 'You are on v{version} — this is the latest version.',
|
|
774
|
+
// Deferred notice: a newer release exists but is still inside pnpm's 24h supply-chain window
|
|
775
|
+
hubPendingDesc: 'v{version} has been released, but it is still within the 24-hour window where pnpm silently falls back to the older version, so the update is not offered yet. The update entry will appear here shortly.',
|
|
772
776
|
hubUpToDate: 'Up to date',
|
|
773
777
|
// Header "Follow us" modal: platform intro + user feedback group QR (content pushed via Worker /about Markdown)
|
|
774
778
|
followUs: 'Follow us',
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -124,12 +124,12 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
124
124
|
...(attempts && attempts.length > 0
|
|
125
125
|
? ['', '## Attempted install channels(已尝试的安装方式)', ...attempts.map((a) => `- ${a}`)]
|
|
126
126
|
: []),
|
|
127
|
-
// [packaging]
|
|
127
|
+
// [packaging] 场景:说明三种官方安装通道(npm 包 / git 直装 / 预构建 release .tgz),当前插件在对应通道下分发不完整
|
|
128
128
|
...(/\[packaging\]/i.test(message)
|
|
129
129
|
? [
|
|
130
130
|
'',
|
|
131
131
|
'## 安装方式说明',
|
|
132
|
-
'DSH
|
|
132
|
+
'DSH 插件支持三种官方安装通道:`dsh plugin add <npm-package>`(npm 分发,需发布完整构建产物)、`dsh plugin add git+https://github.com/owner/repo.git`(Git 直装,仓库需提交构建产物),以及预构建 release 包 `dsh plugin add https://github.com/owner/repo/releases/download/<tag>/<asset>.tgz`(仓库不便提交构建产物时的推荐方式)。注意:pnpm ≥10 默认拦截依赖构建脚本,Git 直装不会执行包内 `prepare`,请勿依赖它生成产物。当前插件的分发物缺少 package.json 声明的入口文件,请提交构建产物、改用 release `.tgz` 直链,或发布完整的 npm 包。',
|
|
133
133
|
]
|
|
134
134
|
: []),
|
|
135
135
|
'',
|
package/src/client/types.ts
CHANGED
|
@@ -109,6 +109,17 @@ export interface HubUpdateInfo {
|
|
|
109
109
|
notes?: string | Record<string, string> | null
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Hub 自我更新状态 —— 供版本弹窗选文案与按钮,四态互斥:
|
|
114
|
+
* - `none` 未取到接口中心信息(拉取失败)→ 只说当前版本,不提示更新
|
|
115
|
+
* - `available` 远端有更新且已过供应链安全门槛 → 提示更新
|
|
116
|
+
* - `pending` 远端有更新但发布未满门槛(见 useCatalog)→ 暂缓提示并说明原因
|
|
117
|
+
* - `latest` 与远端最新版一致 → 确已最新
|
|
118
|
+
* 「有新版但暂缓提示」必须与「确已最新」分开:两者若共用文案,弹窗会在本机版本落后时
|
|
119
|
+
* 谎称「你正在使用 v{远端最新版},已是最新」(见 issue #145)。
|
|
120
|
+
*/
|
|
121
|
+
export type HubUpdateState = 'none' | 'available' | 'pending' | 'latest'
|
|
122
|
+
|
|
112
123
|
/** Worker 返回的「关注我们」内容(content 为 Markdown,可字符串或 {zh,en} 双语言对象)。 */
|
|
113
124
|
export interface HubAboutInfo {
|
|
114
125
|
/** Markdown 平台介绍 + 反馈群二维码;可传字符串,或 { zh, en } 双语言对象按界面语言取 */
|
|
@@ -834,6 +834,11 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
834
834
|
return
|
|
835
835
|
}
|
|
836
836
|
const attempts: string[] = []
|
|
837
|
+
// 代理口径与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
838
|
+
// 供 npm 反查时跟随 GitHub 改名跳转用(Node https 读不到系统代理,交给 curl)。
|
|
839
|
+
const effectiveProxy = settings.proxy !== ''
|
|
840
|
+
? settings.proxy
|
|
841
|
+
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
837
842
|
// Catalog metadata may outlive a packaging split: one repository can publish both a
|
|
838
843
|
// CLI/helper package and a dedicated DSH plugin package. Keep the catalog target when
|
|
839
844
|
// registry metadata confirms it is a DSH package for this repo. Otherwise inspect the
|
|
@@ -843,7 +848,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
843
848
|
// correct package even with a large result window.
|
|
844
849
|
const catalogRepo = source === 'catalog' ? githubRepoOf(displayRepo) : null
|
|
845
850
|
if (catalogRepo !== null && release === null && repoTarget === null && validPackageName(rawRepo)) {
|
|
846
|
-
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry)
|
|
851
|
+
const catalogTargetIsDsh = await isDshNpmPackageForRepo(rawRepo, catalogRepo, settings.npmRegistry, effectiveProxy)
|
|
847
852
|
if (catalogTargetIsDsh) {
|
|
848
853
|
recordResolvedNpmPackage(profile, catalogRepo, rawRepo)
|
|
849
854
|
} else {
|
|
@@ -852,7 +857,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
852
857
|
const root = await preflightTarget(catalogGitTarget)
|
|
853
858
|
const rootName = root.ok && root.name !== null && validPackageName(root.name) ? root.name : null
|
|
854
859
|
if (rootName !== null && rootName !== rawRepo
|
|
855
|
-
&& await isDshNpmPackageForRepo(rootName, catalogRepo, settings.npmRegistry)) {
|
|
860
|
+
&& await isDshNpmPackageForRepo(rootName, catalogRepo, settings.npmRegistry, effectiveProxy)) {
|
|
856
861
|
attempts.push(`catalog npm target: \`${target}\` → repository root DSH package \`${rootName}\``)
|
|
857
862
|
target = rootName
|
|
858
863
|
recordResolvedNpmPackage(profile, catalogRepo, rootName)
|
|
@@ -871,7 +876,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
871
876
|
if (repoTarget !== null && pinnedRef === null) {
|
|
872
877
|
// 反查/记录统一用归一化后的 owner/repo(gitRepo),保证「输入完整链接」也走同一套 npm 反查
|
|
873
878
|
const repoIdentity = gitRepo ?? rawRepo
|
|
874
|
-
const npmName = await resolveNpmPackage(repoIdentity, settings.npmRegistry)
|
|
879
|
+
const npmName = await resolveNpmPackage(repoIdentity, settings.npmRegistry, effectiveProxy)
|
|
875
880
|
if (npmName !== null) {
|
|
876
881
|
target = npmName
|
|
877
882
|
attempts.push(`npm registry search: \`npm search repository:${repoIdentity}\` → found \`${npmName}\``)
|
|
@@ -920,9 +925,6 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
920
925
|
// 插件侧问题引导去作者仓库提 Issue。
|
|
921
926
|
// 代理口径与诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
922
927
|
if (already === null) {
|
|
923
|
-
const effectiveProxy = settings.proxy !== ''
|
|
924
|
-
? settings.proxy
|
|
925
|
-
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
926
928
|
const isGitChannel = release !== null || (repoTarget !== null && target === repoTarget)
|
|
927
929
|
// release 包是 https://github.com/ 直链下载(非 git 克隆),探针按 HTTP 打 github.com 主页;
|
|
928
930
|
// npm 通道探 registry 根。两者都是通道的稳定入口,不探具体包/仓库(404 会被误判成网络故障)。
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { get } from 'node:https'
|
|
15
15
|
import { githubRepoOf } from '../profile/profile.ts'
|
|
16
|
+
import { finalUrl } from '../probe.ts'
|
|
16
17
|
|
|
17
18
|
/** 反查缓存:repo 小写 → npm 包名(null 表示已确认无对应 npm 包;网络异常不缓存)。 */
|
|
18
19
|
const cache = new Map<string, string | null>()
|
|
@@ -30,16 +31,21 @@ const SEARCH_RESULT_LIMIT = 250
|
|
|
30
31
|
* 安装仍会重试反查,避免一次瞬时故障把整个会话锁死在 git 通道。
|
|
31
32
|
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
32
33
|
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
34
|
+
* proxy 参数:GitHub 改名跳转探测走的代理(与安装/诊断同一口径)。
|
|
33
35
|
* 慢网络下失败不阻塞安装。
|
|
34
36
|
*/
|
|
35
|
-
export function resolveNpmPackage(repo: string, registry = ''): Promise<string | null> {
|
|
37
|
+
export async function resolveNpmPackage(repo: string, registry = '', proxy = ''): Promise<string | null> {
|
|
36
38
|
const key = repo.toLowerCase()
|
|
37
|
-
if (cache.has(key)) return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
if (cache.has(key)) return cache.get(key) ?? null
|
|
40
|
+
let found = await searchRepo(repo, registry)
|
|
41
|
+
// 仓库改名后 npm 包的 repository 已指向新名,按旧名反查不到:跟随一次 GitHub 的
|
|
42
|
+
// 改名跳转拿到规范名再查一遍(旧地址本身仍可用,只是仓库名变了)。
|
|
43
|
+
if (found === null) {
|
|
44
|
+
const canonical = await canonicalRepo(repo, proxy)
|
|
45
|
+
if (canonical !== '' && canonical.toLowerCase() !== key) found = await searchRepo(canonical, registry)
|
|
46
|
+
}
|
|
47
|
+
if (found !== undefined) cache.set(key, found)
|
|
48
|
+
return found ?? null
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
type NpmSearchPackage = {
|
|
@@ -77,7 +83,7 @@ export function isDshPackageMetadataForRepo(pkg: NpmSearchPackage, repo: string)
|
|
|
77
83
|
return keywordMarked || manifestMarked
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
export function isDshNpmPackageForRepo(packageName: string, repo: string, registry = ''): Promise<boolean> {
|
|
86
|
+
export function isDshNpmPackageForRepo(packageName: string, repo: string, registry = '', proxy = ''): Promise<boolean> {
|
|
81
87
|
if (packageName === '' || repo === '') return Promise.resolve(false)
|
|
82
88
|
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '')
|
|
83
89
|
const url = `${base}/${encodeURIComponent(packageName)}/latest`
|
|
@@ -91,11 +97,20 @@ export function isDshNpmPackageForRepo(packageName: string, repo: string, regist
|
|
|
91
97
|
let body = ''
|
|
92
98
|
res.on('data', (chunk: Buffer) => { body += chunk.toString() })
|
|
93
99
|
res.on('end', () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
void (async () => {
|
|
101
|
+
try {
|
|
102
|
+
const pkg = JSON.parse(body) as NpmSearchPackage
|
|
103
|
+
if (isDshPackageMetadataForRepo(pkg, repo)) {
|
|
104
|
+
resolve(true)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
// 仓库改名后包元数据指向新名:按原名不匹配时,用 GitHub 跳转后的规范名复核一次
|
|
108
|
+
const canonical = await canonicalRepo(repo, proxy)
|
|
109
|
+
resolve(canonical !== '' && isDshPackageMetadataForRepo(pkg, canonical))
|
|
110
|
+
} catch {
|
|
111
|
+
resolve(false)
|
|
112
|
+
}
|
|
113
|
+
})()
|
|
99
114
|
})
|
|
100
115
|
})
|
|
101
116
|
req.on('timeout', () => req.destroy())
|
|
@@ -103,6 +118,19 @@ export function isDshNpmPackageForRepo(packageName: string, repo: string, regist
|
|
|
103
118
|
})
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 解析仓库改名后的规范 `owner/repo`:GitHub 对已改名仓库返回 301,跟随跳转取最终地址。
|
|
123
|
+
* 与其它 GitHub 访问同口径走 curl + 代理 env(Node 内置 https 读不到系统代理)。
|
|
124
|
+
* 未改名、仓库不存在、网络失败或 curl 不可用都返回 '',调用方按原名继续。
|
|
125
|
+
*/
|
|
126
|
+
async function canonicalRepo(repo: string, proxy: string): Promise<string> {
|
|
127
|
+
if (githubRepoOf(repo) === null) return ''
|
|
128
|
+
const key = repo.toLowerCase()
|
|
129
|
+
const final = await finalUrl(`https://github.com/${repo}`, proxy, REQUEST_TIMEOUT_MS)
|
|
130
|
+
const canonical = final === null ? null : githubRepoOf(final)
|
|
131
|
+
return canonical !== null && canonical.toLowerCase() !== key ? canonical : ''
|
|
132
|
+
}
|
|
133
|
+
|
|
106
134
|
function matchingPackagesForRepo(objects: NpmSearchObject[], repo: string): Array<{ name: string; isDshPlugin: boolean }> {
|
|
107
135
|
const matches: Array<{ name: string; isDshPlugin: boolean }> = []
|
|
108
136
|
|
|
@@ -256,6 +256,24 @@ export function gitLsRemote(url: string, proxy: string, timeoutMs: number): Prom
|
|
|
256
256
|
})
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
|
|
261
|
+
* 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
|
|
262
|
+
* 即规范地址。与其它 GitHub 访问同口径 —— curl 子进程注入代理 env(Node 内置
|
|
263
|
+
* http(s) 读不到系统代理,直连在部分网络下不可达)。
|
|
264
|
+
* 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
|
|
265
|
+
*/
|
|
266
|
+
export async function finalUrl(url: string, proxy: string, timeoutMs: number): Promise<string | null> {
|
|
267
|
+
const discard = process.platform === 'win32' ? 'NUL' : '/dev/null'
|
|
268
|
+
const r = await curlWithTlsFallback([
|
|
269
|
+
'-sS', '-I', '-L', '-o', discard, '-w', '%{url_effective}',
|
|
270
|
+
'--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))),
|
|
271
|
+
url,
|
|
272
|
+
], proxyEnv(proxy), timeoutMs)
|
|
273
|
+
const final = r.stdout.trim()
|
|
274
|
+
return r.code === 0 && final !== '' ? final : null
|
|
275
|
+
}
|
|
276
|
+
|
|
259
277
|
/**
|
|
260
278
|
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数兜底)。
|
|
261
279
|
* 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|