@noob-stupid/dsh-plugin-console 0.3.47 → 0.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/README.zh.md +282 -281
- package/lib/client.js +80 -8
- package/lib/index.js +485 -140
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -594,7 +594,7 @@ async function removeBundleFromManifest(profileDir, bundlePkg) {
|
|
|
594
594
|
/** 用 corepack pnpm 移除包(与安装同一管理器与凭据抑制环境)。
|
|
595
595
|
* 注意:pnpm remove 不支持 --registry 选项,去掉避免静默失败。 */
|
|
596
596
|
async function pnpmRemove(profileDir, packageName) {
|
|
597
|
-
const args = ['
|
|
597
|
+
const args = ['remove', packageName]
|
|
598
598
|
const opts = {
|
|
599
599
|
cwd: profileDir,
|
|
600
600
|
timeout: 120000,
|
|
@@ -602,17 +602,8 @@ async function pnpmRemove(profileDir, packageName) {
|
|
|
602
602
|
maxBuffer: 4 * 1024 * 1024,
|
|
603
603
|
env: { ...process.env, COREPACK_NPM_REGISTRY: 'https://registry.npmmirror.com', GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' },
|
|
604
604
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (existsSync(corepackJs)) {
|
|
608
|
-
await execFileAsync(process.execPath, [corepackJs, ...args], opts)
|
|
609
|
-
return
|
|
610
|
-
}
|
|
611
|
-
const cmd = `${JSON.stringify('corepack')} ${args.map((arg) => JSON.stringify(arg)).join(' ')}`
|
|
612
|
-
await execFileAsync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', cmd], opts)
|
|
613
|
-
return
|
|
614
|
-
}
|
|
615
|
-
await execFileAsync('corepack', args, opts)
|
|
605
|
+
// 跨平台定位 corepack/pnpm(Linux 的 corepack 不在 <node bin>/node_modules 下,2026-09-20 事故)
|
|
606
|
+
await runPnpmWithFallback(args, { execOpts: opts })
|
|
616
607
|
}
|
|
617
608
|
|
|
618
609
|
/** 清理陈旧包目录与 pnpm _tmp_ 残留(Windows 原子替换 EPERM 的根因),返回清理数量。 */
|
|
@@ -645,7 +636,7 @@ function cleanupStalePackageDir(profileDir, packageName) {
|
|
|
645
636
|
const SOURCES_FILE = join(dshHome(), 'plugin-console-sources.json')
|
|
646
637
|
/** 敏感凭据单独落盘(避免与可分享配置混存):自定义搜索源 headers + Gitee secret/token。 */
|
|
647
638
|
const SOURCES_SECRETS_FILE = join(dshHome(), 'plugin-console-sources.secrets.json')
|
|
648
|
-
const DEFAULT_SOURCES = {
|
|
639
|
+
export const DEFAULT_SOURCES = {
|
|
649
640
|
registries: [
|
|
650
641
|
{ id: 'npmmirror', name: 'npmmirror(国内镜像)', url: 'https://registry.npmmirror.com', primary: true },
|
|
651
642
|
{ id: 'npmjs', name: 'npmjs(官方源)', url: 'https://registry.npmjs.org', primary: false },
|
|
@@ -655,9 +646,16 @@ const DEFAULT_SOURCES = {
|
|
|
655
646
|
{ id: 'gitee', name: 'Gitee', type: 'builtin' },
|
|
656
647
|
],
|
|
657
648
|
// 市场静态索引源(按主→备依次尝试;内网可整体替换为自建镜像,实现完全离线的市场浏览)
|
|
649
|
+
// 2026-09-20 扩容 2 → 5:只有 jsDelivr + ghproxy 两个源时,两者同时不可达就会让整个市场退化成
|
|
650
|
+
// 「只能搜 GitHub 实时结果」——收录清单(含 ★7800 全家桶)与本地索引模糊匹配一起失效
|
|
651
|
+
// (另一位用户实测报「市场索引加载失败:网络不可达(2 个索引源全部失败):GitHub 请求超时」)。
|
|
652
|
+
// jsDelivr 官方多入口互为主备:cdn(主)/ gcore / fastly;再加 ghproxy 与 raw 直连兜底。
|
|
658
653
|
indexSources: [
|
|
659
654
|
{ id: 'jsdelivr', name: 'jsDelivr CDN', url: 'https://cdn.jsdelivr.net/gh/Noob-stupid/dsh-plugin-hub@main/marketplace/index.json', primary: true },
|
|
655
|
+
{ id: 'jsdelivr-gcore', name: 'jsDelivr (gcore)', url: 'https://gcore.jsdelivr.net/gh/Noob-stupid/dsh-plugin-hub@main/marketplace/index.json', primary: false },
|
|
656
|
+
{ id: 'jsdelivr-fastly', name: 'jsDelivr (fastly)', url: 'https://fastly.jsdelivr.net/gh/Noob-stupid/dsh-plugin-hub@main/marketplace/index.json', primary: false },
|
|
660
657
|
{ id: 'ghproxy', name: 'ghproxy 镜像', url: 'https://ghproxy.net/https://raw.githubusercontent.com/Noob-stupid/dsh-plugin-hub/main/marketplace/index.json', primary: false },
|
|
658
|
+
{ id: 'raw', name: 'GitHub raw 直连', url: 'https://raw.githubusercontent.com/Noob-stupid/dsh-plugin-hub/main/marketplace/index.json', primary: false },
|
|
661
659
|
],
|
|
662
660
|
// Git 克隆源({owner}/{repo} 占位符;按主→备依次尝试)。
|
|
663
661
|
// 可替换为 Gitee / GitLab / 自建 Gitea / 任意镜像代理,实现「换一个网站下载仓库内容」。
|
|
@@ -1705,11 +1703,12 @@ const API_MIRROR_PREFIXES = [
|
|
|
1705
1703
|
'https://ghfast.top/https://api.github.com',
|
|
1706
1704
|
]
|
|
1707
1705
|
|
|
1708
|
-
/** raw.githubusercontent.com 的镜像竞速前缀(与浏览器端四通道一致)。
|
|
1706
|
+
/** raw.githubusercontent.com 的镜像竞速前缀(与浏览器端四通道一致)。
|
|
1707
|
+
* 2026-09-19 移除 mirror.ghproxy.com:该域名早已失效(实测连接超时;失效域名被停放页接管时
|
|
1708
|
+
* 会回 2xx HTML —— 正是"普通插件被误判成 submodule 套装"的假阳性来源之一)。 */
|
|
1709
1709
|
const RAW_MIRROR_PREFIXES = [
|
|
1710
1710
|
'https://ghproxy.net/https://raw.githubusercontent.com',
|
|
1711
1711
|
'https://ghfast.top/https://raw.githubusercontent.com',
|
|
1712
|
-
'https://mirror.ghproxy.com/https://raw.githubusercontent.com',
|
|
1713
1712
|
]
|
|
1714
1713
|
|
|
1715
1714
|
/** 并行竞速:任一分支拿到 2xx 即胜出;非 2xx 与错误都视为失败,全部失败则拒绝。 */
|
|
@@ -1868,9 +1867,12 @@ async function githubText(url, signal, token = null) {
|
|
|
1868
1867
|
}
|
|
1869
1868
|
|
|
1870
1869
|
/** 用 curl 子进程请求 JSON(绕过本机中间设备对 node:https TLS 指纹的拦截;curl 走系统网络栈)。 */
|
|
1871
|
-
async function curlJson(url, timeout = 15000, headers = {}) {
|
|
1870
|
+
async function curlJson(url, timeout = 15000, headers = {}, { ipv4 = false } = {}) {
|
|
1872
1871
|
const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
|
1873
1872
|
const args = ['-s', '-m', String(Math.max(5, Math.ceil(timeout / 1000))), '-H', 'accept: application/json', '-w', '\n__HTTP__%{http_code}']
|
|
1873
|
+
// ipv4:只给 GitHub 域名用——「解析出 IPv6 但没有 IPv6 路由」的环境里 curl 默认要先空等 ~5.2s 才回退
|
|
1874
|
+
// IPv4(2026-09-20 另一位用户实测:默认 5473ms vs `-4` 的 461ms);双栈正常的环境无副作用。
|
|
1875
|
+
if (ipv4) args.push('-4')
|
|
1874
1876
|
for (const [key, value] of Object.entries(headers)) args.push('-H', `${key}: ${value}`)
|
|
1875
1877
|
args.push(url)
|
|
1876
1878
|
const { stdout } = await execFileAsync(bin, args, { timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 })
|
|
@@ -1881,12 +1883,17 @@ async function curlJson(url, timeout = 15000, headers = {}) {
|
|
|
1881
1883
|
return JSON.parse(body)
|
|
1882
1884
|
}
|
|
1883
1885
|
|
|
1884
|
-
/** 用 curl 拉取文本(raw 文件用:本机 curl 直连 raw.githubusercontent.com 秒回,绕开 node:https 黑洞)。
|
|
1885
|
-
|
|
1886
|
+
/** 用 curl 拉取文本(raw 文件用:本机 curl 直连 raw.githubusercontent.com 秒回,绕开 node:https 黑洞)。
|
|
1887
|
+
* 导出供 test-suite-detect.mjs 断言传输层口径(200 空 body → 空串,404 → 抛 HTTP 404)。 */
|
|
1888
|
+
export async function curlText(url, timeout = 10000, { ipv4 = false } = {}) {
|
|
1886
1889
|
const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
|
1890
|
+
const args = ['-s', '-m', String(Math.max(3, Math.ceil(timeout / 1000))), '-H', 'accept: text/plain', '-w', '\n__HTTP__%{http_code}']
|
|
1891
|
+
// 见 curlJson 注释:GitHub 域名走 IPv4 快通道(IPv6 无路由的环境下默认要白等 ~5.2s)
|
|
1892
|
+
if (ipv4) args.push('-4')
|
|
1893
|
+
args.push(url)
|
|
1887
1894
|
const { stdout } = await execFileAsync(
|
|
1888
1895
|
bin,
|
|
1889
|
-
|
|
1896
|
+
args,
|
|
1890
1897
|
{ timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
|
|
1891
1898
|
)
|
|
1892
1899
|
const m = String(stdout).match(/\n__HTTP__(\d+)\s*$/u)
|
|
@@ -2049,7 +2056,9 @@ async function enrichItemOne(item) {
|
|
|
2049
2056
|
detectSkillRepo(item.fullName, branch),
|
|
2050
2057
|
rawTextWithFallback(item.fullName, branch, '.gitmodules'),
|
|
2051
2058
|
])
|
|
2052
|
-
|
|
2059
|
+
// 套装判定必须过内容校验:代理/CDN 对不存在的 .gitmodules 也可能回 2xx 空 body,
|
|
2060
|
+
// 只判"探测非 null"会把普通插件标成套装置仓库(2026-09-19 事故)。
|
|
2061
|
+
if (suiteResult.status === 'fulfilled' && looksLikeGitmodules(suiteResult.value)) {
|
|
2053
2062
|
hasSuite = true
|
|
2054
2063
|
}
|
|
2055
2064
|
if (pkgResult.status === 'fulfilled') {
|
|
@@ -2149,11 +2158,53 @@ function githubRepoInfo(repo) {
|
|
|
2149
2158
|
return `${match[1]}/${match[2]}`
|
|
2150
2159
|
}
|
|
2151
2160
|
|
|
2161
|
+
/** 「真的读到了文件内容」判定:null / 空串 / 纯空白都不算。导出供 test-suite-detect.mjs 断言。
|
|
2162
|
+
* 真实事故(2026-09-19,用户反馈装 dsh-whale-widget 报「未找到 .gitmodules」):
|
|
2163
|
+
* 本机加速器/代理(Watt Toolkit 之类,劫持 raw.githubusercontent.com 到 127.0.0.1)
|
|
2164
|
+
* 会对**不存在的文件**回 2xx + 空 body;旧代码只判 `body === null`,
|
|
2165
|
+
* 于是空串被当成"文件存在" → 普通插件被判定成 submodule 套装置仓库 → 安装必然失败。 */
|
|
2166
|
+
export function readBodyOrNull(body) {
|
|
2167
|
+
return typeof body === 'string' && body.trim() !== '' ? body : null
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/** .gitmodules 内容校验:必须是真 gitmodules(含 [submodule "x"] 段)才算套装仓库。导出供测试。
|
|
2171
|
+
* 只判"探测非 null"会把代理/CDN 的垃圾响应也算成套装;判定权交给内容,不交给状态码。 */
|
|
2172
|
+
export function looksLikeGitmodules(text) {
|
|
2173
|
+
return typeof text === 'string' && /^\s*\[submodule\s+"/mu.test(text)
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/** raw 抓取外层预算(毫秒)。事件(2026-09-20,另一位用户:Android + proot Ubuntu 容器,域名解析出
|
|
2177
|
+
* IPv6 但容器无 IPv6 路由):4 条通道最快也要 5.4s(curl 直连被 -m 6 掐死、node:https 5435ms、
|
|
2178
|
+
* jsDelivr 5564ms),旧的 5000ms 预算**必然先超时** → 抓取失败被当成"文件不存在",
|
|
2179
|
+
* 报成「仓库没有 package.json」。放宽到 10s:真 404 仍是毫秒级返回,只有全网慢时才多等。 */
|
|
2180
|
+
export const FETCH_BUDGET_MS = 10000
|
|
2181
|
+
/** 仓库元数据(默认分支)探测预算:旧值 3s 在同一环境下同样必输 → branch 恒为 'main',
|
|
2182
|
+
* 默认分支为 dev 的仓库会取错分支。 */
|
|
2183
|
+
export const META_BUDGET_MS = 8000
|
|
2184
|
+
export const FETCH_OK = 'ok'
|
|
2185
|
+
export const FETCH_NOT_FOUND = 'not-found'
|
|
2186
|
+
export const FETCH_UNREACHABLE = 'unreachable'
|
|
2187
|
+
|
|
2188
|
+
/** 竞速组合(纯逻辑,导出供单测):通道结果 + 预算 → { state, body }。
|
|
2189
|
+
* · anyPromise 以 null 成功 = 某通道确定性 404 → not-found
|
|
2190
|
+
* · anyPromise 以字符串成功 = 读到内容 → ok
|
|
2191
|
+
* · anyPromise 全部失败(AggregateError)或超出预算 → unreachable(**不再与 404 混为一谈**) */
|
|
2192
|
+
export function raceFetchOutcome(anyPromise, budgetMs) {
|
|
2193
|
+
const settled = Promise.resolve(anyPromise)
|
|
2194
|
+
.then((body) => (body === null ? { state: FETCH_NOT_FOUND, body: null } : { state: FETCH_OK, body }))
|
|
2195
|
+
.catch(() => ({ state: FETCH_UNREACHABLE, body: null }))
|
|
2196
|
+
return Promise.race([
|
|
2197
|
+
settled,
|
|
2198
|
+
new Promise((resolve) => setTimeout(() => resolve({ state: FETCH_UNREACHABLE, body: null }), budgetMs)),
|
|
2199
|
+
])
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2152
2202
|
/** raw 文件读取:https 主通道(含镜像)、gh CLI、curl 直连、curl jsDelivr CDN **四通道并行竞速**。
|
|
2153
2203
|
* 黑洞期 https/ghproxy 全挂、gh 可能不在服务进程 PATH——jsDelivr CDN 走系统网络且国内可达,是最后兜底。
|
|
2154
2204
|
* 关键语义:**404 是确定性结果**(文件不存在,探测 .gitmodules/SKILL.md 等时的正常答案),
|
|
2155
|
-
* 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。
|
|
2156
|
-
|
|
2205
|
+
* 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。
|
|
2206
|
+
* 返回值区分「确定性 404」与「超时/通道全灭」——二者都以"没内容"结束,但用户文案必须不同。 */
|
|
2207
|
+
async function rawTextFetch(repo, branch, path, budgetMs = FETCH_BUDGET_MS) {
|
|
2157
2208
|
const url = `${GITHUB_RAW}/${repo}/${encodeURIComponent(branch)}/${path}`
|
|
2158
2209
|
// 404/4xx:确定性"不存在",立即以 null 胜出(不等慢分支)
|
|
2159
2210
|
const notFound = (promise) => promise
|
|
@@ -2161,48 +2212,184 @@ async function rawTextWithFallback(repo, branch, path) {
|
|
|
2161
2212
|
if (error instanceof Error && /HTTP 404|HTTP 400|Not Found|not found|非 2xx|HTTP 非 2xx/u.test(error.message)) return null
|
|
2162
2213
|
throw error
|
|
2163
2214
|
})
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2215
|
+
// 各通道统一口径:状态 2xx **且** body 非空白,才算读到文件(否则抛"非 2xx" → 归一为 null)
|
|
2216
|
+
const real = (promise) => promise.then((body) => {
|
|
2217
|
+
const text = readBodyOrNull(body)
|
|
2218
|
+
if (text === null) throw new Error('HTTP 非 2xx')
|
|
2219
|
+
return text
|
|
2220
|
+
})
|
|
2221
|
+
const https = notFound(real(githubText(url)))
|
|
2169
2222
|
const gh = notFound(githubViaGh(`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`)
|
|
2170
2223
|
.then((data) => {
|
|
2171
2224
|
if (data && typeof data.content === 'string') return Buffer.from(data.content, 'base64').toString('utf8')
|
|
2172
2225
|
throw new Error('contents 无内容')
|
|
2173
|
-
})
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
if (body === null) throw new Error('
|
|
2177
|
-
return body
|
|
2178
|
-
}))
|
|
2179
|
-
const cdn = notFound(curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 6000)
|
|
2180
|
-
.then((body) => {
|
|
2181
|
-
if (body === null) throw new Error('HTTP 非 2xx')
|
|
2226
|
+
})
|
|
2227
|
+
.then((text) => {
|
|
2228
|
+
const body = readBodyOrNull(text)
|
|
2229
|
+
if (body === null) throw new Error('contents 无内容')
|
|
2182
2230
|
return body
|
|
2183
2231
|
}))
|
|
2232
|
+
const curl = notFound(real(curlText(url, 9000, { ipv4: true })))
|
|
2233
|
+
const cdn = notFound(real(curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 9000, { ipv4: true })))
|
|
2234
|
+
return await raceFetchOutcome(Promise.any([https, gh, curl, cdn]), budgetMs)
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
/** 兼容入口:调用方只关心"有没有内容"(null = 没有)。需要区分超时/404 的场景用 rawTextFetch。 */
|
|
2238
|
+
async function rawTextWithFallback(repo, branch, path) {
|
|
2239
|
+
const { state, body } = await rawTextFetch(repo, branch, path)
|
|
2240
|
+
return state === FETCH_OK ? body : null
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
/** 包探测(带失败原因):reason ∈ ok / not-found / unreachable / invalid。导出供测试。
|
|
2244
|
+
* 事故(2026-09-20,另一位用户:Android + proot Ubuntu,容器无 IPv6 路由):
|
|
2245
|
+
* 抓取超时与真 404 都让上层拿到同一个 null,于是「网络太慢」被报成「仓库没有 package.json」。 */
|
|
2246
|
+
async function fetchRepoPackageEx(repo, branch) {
|
|
2247
|
+
const { state, body } = await rawTextFetch(repo, branch, 'package.json')
|
|
2248
|
+
if (state !== FETCH_OK) return { pkg: null, reason: state }
|
|
2184
2249
|
try {
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
|
|
2190
|
-
])
|
|
2191
|
-
} catch {
|
|
2192
|
-
return null
|
|
2193
|
-
}
|
|
2250
|
+
const pkg = JSON.parse(body)
|
|
2251
|
+
if (pkg !== null && typeof pkg === 'object' && typeof pkg.name === 'string') return { pkg, reason: 'ok' }
|
|
2252
|
+
} catch {}
|
|
2253
|
+
return { pkg: null, reason: 'invalid' }
|
|
2194
2254
|
}
|
|
2195
2255
|
|
|
2196
2256
|
async function fetchRepoPackage(repo, branch) {
|
|
2257
|
+
return (await fetchRepoPackageEx(repo, branch)).pkg
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/** 从 npm 的 repository 字段解析出 GitHub/Gitee 仓库标识(纯函数,单测覆盖)。
|
|
2261
|
+
* 支持 `https://github.com/o/r.git`、`git+https://…`、`git://…`、带 `#path` 的 monorepo 写法。 */
|
|
2262
|
+
export function parseRepoFromUrl(url) {
|
|
2263
|
+
const raw = String(url ?? '').trim()
|
|
2264
|
+
// npm 老式简写:`github:owner/repo` / `gitee:owner/repo`
|
|
2265
|
+
const shorthand = raw.match(/^(?:github|gitee):([^/\s]+)\/([^/\s#?]+?)(?:\.git)?(?:[#?].*)?$/u)
|
|
2266
|
+
if (shorthand !== null) return `${shorthand[1]}/${shorthand[2]}`
|
|
2267
|
+
const m = raw.match(/(?:github\.com|gitee\.com)[/:]([^/\s]+)\/([^/\s#?]+?)(?:\.git)?(?:[#?].*)?$/u)
|
|
2268
|
+
if (m === null) return null
|
|
2269
|
+
return `${m[1]}/${m[2]}`
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/** 结果里是否已有"名字逐词命中查询词"的条目——决定要不要再加 `in:readme` 重查一次。
|
|
2273
|
+
* (仓库搜索的检索面只有 名字/描述/topics;README 里的词必须显式 in:readme 才查得到) */
|
|
2274
|
+
export function hasDirectNameHit(items, query) {
|
|
2275
|
+
const tokens = String(query ?? '').toLowerCase().split(/[\s\-_/.]+/u).filter((tk) => tk.length >= 3)
|
|
2276
|
+
if (tokens.length === 0) return true // 查询太短/太泛:不做二次查询,避免把结果冲稀
|
|
2277
|
+
return items.some((it) => {
|
|
2278
|
+
const name = String(it?.fullName ?? '').toLowerCase()
|
|
2279
|
+
return tokens.every((tk) => name.includes(tk))
|
|
2280
|
+
})
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/** npm 包名搜索:registry 搜索接口 → 候选包 → 读 packument 的 repository.url → 映射回 GitHub 仓库。
|
|
2284
|
+
* 背景(2026-09-20):用户搜 `web-all`(= npm 包 `@linxin666/dsh-web-all`)搜不到,因为 `web-all`
|
|
2285
|
+
* 只存在于 npm 包名、仓库文件与 README 里,而 GitHub 仓库搜索的检索面只有 名字/描述/topics。
|
|
2286
|
+
* 这条通道不依赖静态索引、也不依赖 GitHub 登录,且命中后可按包名直接安装。 */
|
|
2287
|
+
export async function searchNpmPackages(query, registries, limit = 3, token = null) {
|
|
2288
|
+
const q = String(query ?? '').trim().toLowerCase()
|
|
2289
|
+
if (q.length < 2) return []
|
|
2290
|
+
let hits = null
|
|
2291
|
+
let registry = null
|
|
2292
|
+
for (const reg of (registries ?? []).slice(0, 3)) {
|
|
2293
|
+
try {
|
|
2294
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2295
|
+
const data = await fetchJsonUrl(`${reg}/-/v1/search?text=${encodeURIComponent(q)}&size=10`, 8000)
|
|
2296
|
+
if (data && Array.isArray(data.objects)) { hits = data.objects; registry = reg; break }
|
|
2297
|
+
} catch {}
|
|
2298
|
+
}
|
|
2299
|
+
if (hits === null) return []
|
|
2300
|
+
const candidates = hits
|
|
2301
|
+
.map((o) => o?.package)
|
|
2302
|
+
.filter((p) => p && typeof p.name === 'string' && p.name.toLowerCase().includes(q))
|
|
2303
|
+
.slice(0, limit)
|
|
2304
|
+
const out = []
|
|
2305
|
+
for (const cand of candidates) {
|
|
2306
|
+
try {
|
|
2307
|
+
const encoded = cand.name.startsWith('@')
|
|
2308
|
+
? `@${encodeURIComponent(cand.name.slice(1).split('/')[0])}%2f${encodeURIComponent(cand.name.split('/').slice(1).join('/'))}`
|
|
2309
|
+
: encodeURIComponent(cand.name)
|
|
2310
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2311
|
+
const meta = await fetchJsonUrl(`${registry}/${encoded}`, 8000)
|
|
2312
|
+
const repo = parseRepoFromUrl(meta?.repository?.url ?? meta?.repository ?? cand.links?.repository ?? '')
|
|
2313
|
+
if (repo === null) continue
|
|
2314
|
+
// 顺带补仓库真实元数据(星数/描述/默认分支)——npm 里的信息不足,且默认分支可能是 dev
|
|
2315
|
+
let info = null
|
|
2316
|
+
try {
|
|
2317
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2318
|
+
info = await githubJson(`${GITHUB_API}/repos/${repo}`, null, token)
|
|
2319
|
+
} catch {}
|
|
2320
|
+
out.push({
|
|
2321
|
+
fullName: repo,
|
|
2322
|
+
description: `${info?.description ?? cand.description ?? ''}(npm 包:${cand.name}@${cand.version ?? '?'})`.trim(),
|
|
2323
|
+
htmlUrl: info?.html_url ?? `https://github.com/${repo}`,
|
|
2324
|
+
stars: typeof info?.stargazers_count === 'number' ? info.stargazers_count : 0,
|
|
2325
|
+
updatedAt: info?.updated_at ?? '',
|
|
2326
|
+
defaultBranch: info?.default_branch ?? null,
|
|
2327
|
+
topics: Array.isArray(info?.topics) ? info.topics : [],
|
|
2328
|
+
source: 'npm',
|
|
2329
|
+
sourceName: 'npm',
|
|
2330
|
+
packageName: cand.name,
|
|
2331
|
+
npmVersion: cand.version ?? null,
|
|
2332
|
+
npmPackage: true,
|
|
2333
|
+
})
|
|
2334
|
+
} catch {}
|
|
2335
|
+
}
|
|
2336
|
+
return out
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/** monorepo 子包增强:GitHub 代码搜索(`<词> filename:package.json`)→ 命中 `packages/<包>/package.json`
|
|
2340
|
+
* → 读该 package.json 取真实包名 → 作为子包条目返回(这样 `dsh-web-all`、OpenViking 这类
|
|
2341
|
+
* 「只存在于仓库文件里的包名」也能被搜到,且带 packageName 可直接按包名安装)。
|
|
2342
|
+
* ⚠️ GitHub **代码搜索 API 强制要求登录**(未登录实测 401 Requires authentication),未登录时返回空。 */
|
|
2343
|
+
export async function searchSubpackageItems(query, token = null, signal = null) {
|
|
2344
|
+
const subItems = []
|
|
2197
2345
|
try {
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2346
|
+
const codeData = await githubJson(
|
|
2347
|
+
`${GITHUB_API}/search/code?q=${encodeURIComponent(`${query} filename:package.json`)}`,
|
|
2348
|
+
signal,
|
|
2349
|
+
token,
|
|
2350
|
+
)
|
|
2351
|
+
for (const hit of (codeData.items ?? []).slice(0, 10)) {
|
|
2352
|
+
const hitPath = typeof hit.path === 'string' ? hit.path : ''
|
|
2353
|
+
if (!/^(?:packages|examples|plugins|skills|apps|extensions|src|lib)\/[^/]+\/package\.json$/u.test(hitPath)) continue
|
|
2354
|
+
const repoName = hit.repository?.full_name ?? ''
|
|
2355
|
+
if (!repoName) continue
|
|
2356
|
+
const dir = hitPath.split('/').slice(0, -1).join('/')
|
|
2357
|
+
let packageName = dir.split('/').slice(-1)[0]
|
|
2358
|
+
try {
|
|
2359
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2360
|
+
const pkgText = await rawTextWithFallback(repoName, 'main', hitPath)
|
|
2361
|
+
if (pkgText !== null) {
|
|
2362
|
+
const pkg = JSON.parse(pkgText)
|
|
2363
|
+
if (pkg && typeof pkg.name === 'string') packageName = pkg.name
|
|
2364
|
+
}
|
|
2365
|
+
} catch {}
|
|
2366
|
+
subItems.push({
|
|
2367
|
+
fullName: repoName,
|
|
2368
|
+
description: `子包:${dir}`,
|
|
2369
|
+
htmlUrl: `https://github.com/${repoName}/tree/main/${dir}`,
|
|
2370
|
+
stars: 0,
|
|
2371
|
+
updatedAt: '',
|
|
2372
|
+
defaultBranch: 'main',
|
|
2373
|
+
topics: [],
|
|
2374
|
+
source: 'github',
|
|
2375
|
+
subpackagePath: dir,
|
|
2376
|
+
packageName,
|
|
2377
|
+
})
|
|
2378
|
+
if (subItems.length >= 5) break
|
|
2379
|
+
}
|
|
2380
|
+
} catch {}
|
|
2381
|
+
return subItems
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/** 探测失败时的用户可读文案:「抓取超时/不可达」与「真的没有」必须区分开。导出供测试。 */
|
|
2385
|
+
export function packageProbeErrorText(repo, branch, reason) {
|
|
2386
|
+
if (reason === FETCH_UNREACHABLE) {
|
|
2387
|
+
return `抓取超时/网络不可达:没能读到 ${repo}(${branch} 分支)的 package.json —— 通常是网络到 GitHub 太慢(例如解析出 IPv6 却无 IPv6 路由)。请重试;若持续失败,可先用「仓库落地」克隆到本地目录。`
|
|
2388
|
+
}
|
|
2389
|
+
if (reason === 'invalid') {
|
|
2390
|
+
return `仓库 ${repo} 的 package.json 不是合法的包描述(缺少 name 字段),无法作为插件安装——可改用「仓库落地」克隆到本地目录。`
|
|
2205
2391
|
}
|
|
2392
|
+
return `仓库 ${repo} 没有 package.json(也不是技能仓库),无法作为插件安装——可改用「仓库落地」克隆到本地目录。`
|
|
2206
2393
|
}
|
|
2207
2394
|
|
|
2208
2395
|
/**
|
|
@@ -2211,7 +2398,7 @@ async function fetchRepoPackage(repo, branch) {
|
|
|
2211
2398
|
* (曾导致入口链接丢失、DSH 启动崩溃)。registry 走国内镜像。
|
|
2212
2399
|
*/
|
|
2213
2400
|
async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmirror.com', timeout = 90000, signal = null) {
|
|
2214
|
-
const args = ['
|
|
2401
|
+
const args = ['add', spec, '--registry', registry]
|
|
2215
2402
|
const opts = {
|
|
2216
2403
|
cwd: profileDir,
|
|
2217
2404
|
timeout,
|
|
@@ -2225,19 +2412,9 @@ async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmir
|
|
|
2225
2412
|
GCM_INTERACTIVE: 'never',
|
|
2226
2413
|
},
|
|
2227
2414
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
if (existsSync(corepackJs)) {
|
|
2232
|
-
await execFileAsync(process.execPath, [corepackJs, ...args], opts)
|
|
2233
|
-
return
|
|
2234
|
-
}
|
|
2235
|
-
// 兜底:经 cmd.exe 运行 corepack
|
|
2236
|
-
const cmd = `${JSON.stringify('corepack')} ${args.map((arg) => JSON.stringify(arg)).join(' ')}`
|
|
2237
|
-
await execFileAsync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', cmd], opts)
|
|
2238
|
-
return
|
|
2239
|
-
}
|
|
2240
|
-
await execFileAsync('corepack', args, opts)
|
|
2415
|
+
// 跨平台定位 corepack/pnpm(Windows 布局 / Linux npm 全局布局 / PATH 兜底);
|
|
2416
|
+
// 旧代码只认 Windows 布局,Linux 上会生成 MODULE_NOT_FOUND 的命令(2026-09-20 事故)
|
|
2417
|
+
await runPnpmWithFallback(args, { execOpts: opts })
|
|
2241
2418
|
}
|
|
2242
2419
|
|
|
2243
2420
|
/**
|
|
@@ -2783,6 +2960,64 @@ function packageEntryExists(dir, pkg) {
|
|
|
2783
2960
|
return candidates.some((c) => existsSync(join(dir, c)))
|
|
2784
2961
|
}
|
|
2785
2962
|
|
|
2963
|
+
/** git 可执行名(跨平台)。事故(2026-09-20,另一位用户:Android + proot Ubuntu):
|
|
2964
|
+
* 「仓库落地」硬编码 `git.exe` → 非 Windows 环境 spawn git.exe ENOENT,克隆必然失败。导出供测试。 */
|
|
2965
|
+
export function gitBin() {
|
|
2966
|
+
return process.platform === 'win32' ? 'git.exe' : 'git'
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
/** pnpm 执行方式定位(跨平台,纯函数便于单测 → 按优先级返回列表,逐个尝试)。导出供测试。
|
|
2970
|
+
* 事故(2026-09-20,同一位用户,node v24 + Linux):只按 Windows 布局找 corepack.js
|
|
2971
|
+
* (`<node bin>/node_modules/corepack/dist/corepack.js`),而 Linux 的 npm 全局布局在
|
|
2972
|
+
* `<prefix>/lib/node_modules/corepack/...` → AI 赋能的 install-npm 生成
|
|
2973
|
+
* `node /usr/local/bin/node_modules/corepack/dist/corepack.js pnpm add …` → MODULE_NOT_FOUND。 */
|
|
2974
|
+
export function resolvePnpmRunners({ platform = process.platform, execPath = process.execPath, comspec = process.env.ComSpec ?? 'cmd.exe', exists = existsSync } = {}) {
|
|
2975
|
+
const binDir = dirname(execPath)
|
|
2976
|
+
const corepackCandidates = [
|
|
2977
|
+
join(binDir, 'node_modules', 'corepack', 'dist', 'corepack.js'), // Windows 官方安装器 / nvm-windows
|
|
2978
|
+
join(binDir, '..', 'lib', 'node_modules', 'corepack', 'dist', 'corepack.js'), // Linux/macOS npm 全局
|
|
2979
|
+
join(binDir, '..', 'libexec', 'lib', 'node_modules', 'corepack', 'dist', 'corepack.js'), // brew / 自编译布局
|
|
2980
|
+
]
|
|
2981
|
+
const runners = []
|
|
2982
|
+
for (const js of corepackCandidates) {
|
|
2983
|
+
if (exists(js)) {
|
|
2984
|
+
runners.push({ kind: 'node-corepack', note: `node ${js} pnpm`, run: (args) => ({ bin: execPath, argv: [js, 'pnpm', ...args] }) })
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
if (platform === 'win32') {
|
|
2988
|
+
// .cmd 批处理不能直接 execFile(EINVAL)→ 经 cmd.exe 调用(整条命令作为一个参数)
|
|
2989
|
+
runners.push({
|
|
2990
|
+
kind: 'cmd-corepack',
|
|
2991
|
+
note: 'cmd /c corepack pnpm',
|
|
2992
|
+
run: (args) => ({ bin: comspec, argv: ['/d', '/s', '/c', ['corepack', 'pnpm', ...args].map((a) => JSON.stringify(a)).join(' ')] }),
|
|
2993
|
+
})
|
|
2994
|
+
} else {
|
|
2995
|
+
runners.push({ kind: 'corepack', note: 'corepack pnpm', run: (args) => ({ bin: 'corepack', argv: ['pnpm', ...args] }) })
|
|
2996
|
+
runners.push({ kind: 'pnpm', note: 'pnpm', run: (args) => ({ bin: 'pnpm', argv: args }) })
|
|
2997
|
+
}
|
|
2998
|
+
return runners
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
/** 依次尝试各执行方式;只有"执行方式本身不可用"(ENOENT / MODULE_NOT_FOUND)才换下一个,
|
|
3002
|
+
* 真正的安装失败(网络、依赖冲突等)立即抛出,并附上已尝试的清单便于排查。 */
|
|
3003
|
+
async function runPnpmWithFallback(args, { execOpts = {}, runners = resolvePnpmRunners() } = {}) {
|
|
3004
|
+
let lastError = null
|
|
3005
|
+
for (const runner of runners) {
|
|
3006
|
+
const { bin, argv } = runner.run(args)
|
|
3007
|
+
try {
|
|
3008
|
+
// eslint-disable-next-line no-await-in-loop
|
|
3009
|
+
await execFileAsync(bin, argv, execOpts)
|
|
3010
|
+
return { runner }
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
lastError = error
|
|
3013
|
+
const message = String(error?.message ?? '')
|
|
3014
|
+
if (!/ENOENT|Cannot find module/u.test(message)) throw error
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
const tried = runners.map((r) => r.note).join(' → ')
|
|
3018
|
+
throw new Error(`${lastError?.message ?? 'pnpm 执行失败'}(已尝试:${tried})`)
|
|
3019
|
+
}
|
|
3020
|
+
|
|
2786
3021
|
/** git 非交互环境:禁止任何登录/凭据窗口弹出(私有仓库或不可达源直接失败,不做交互式重试)。 */
|
|
2787
3022
|
function gitEnv() {
|
|
2788
3023
|
return {
|
|
@@ -2812,23 +3047,38 @@ function gitCloneUrls(repoFullName, source = 'github') {
|
|
|
2812
3047
|
return urls.length > 0 ? urls : [`https://github.com/${repoFullName}.git`]
|
|
2813
3048
|
}
|
|
2814
3049
|
|
|
2815
|
-
/**
|
|
3050
|
+
/** 逐条尝试的错误汇总(纯函数,单测覆盖):报**第一个**错误(真实原因)+ 尝试清单。
|
|
3051
|
+
* 为什么要这样:多源重试时若第一次失败留下半成品目录,第二次会以
|
|
3052
|
+
* `fatal: destination path '…' already exists and is not an empty directory` 失败,
|
|
3053
|
+
* 旧代码把它当 lastError 抛出去 → 用户只看到"目录非空",真实原因(镜像/网络不可达)被完全掩盖。
|
|
3054
|
+
* 2026-09-20 另一位用户实测报的就是这句。 */
|
|
3055
|
+
export function summarizeCloneErrors(errors) {
|
|
3056
|
+
const first = errors[0]
|
|
3057
|
+
const tried = errors.map((e) => (/already exists and is not an empty directory/u.test(e.message) ? `${e.url}(目录非空)` : e.url)).join(';')
|
|
3058
|
+
return `git clone 失败(首个错误:${first?.message ?? '未知'});已尝试 ${errors.length} 个源:${tried}`
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
/** git clone(镜像→直连;gitee 直连),返回 { url } 或抛错。 */
|
|
2816
3062
|
async function gitCloneRepo(repo, dest, source = 'github', timeout = 180000) {
|
|
2817
3063
|
const urls = gitCloneUrls(repo, source)
|
|
2818
|
-
|
|
2819
|
-
for (const url of urls) {
|
|
3064
|
+
const errors = []
|
|
3065
|
+
for (const [attempt, url] of urls.entries()) {
|
|
3066
|
+
// 每次尝试前都清掉目标目录:上一次可能留下半成品(git 会先建目录再传输),
|
|
3067
|
+
// 不清就会让第二次以"目录非空"失败并掩盖真实原因(见 summarizeCloneErrors 注释)。
|
|
3068
|
+
try { rmSync(dest, { recursive: true, force: true }) } catch {}
|
|
2820
3069
|
try {
|
|
3070
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2821
3071
|
await execFileAsync('git', ['clone', '--depth', '1', '--quiet', url, dest], {
|
|
2822
3072
|
timeout,
|
|
2823
3073
|
windowsHide: true,
|
|
2824
3074
|
env: gitEnv(),
|
|
2825
3075
|
})
|
|
2826
|
-
return
|
|
3076
|
+
return { url, attempt: attempt + 1 }
|
|
2827
3077
|
} catch (error) {
|
|
2828
|
-
|
|
3078
|
+
errors.push({ url, message: error?.message ?? String(error) })
|
|
2829
3079
|
}
|
|
2830
3080
|
}
|
|
2831
|
-
throw new Error(
|
|
3081
|
+
throw new Error(summarizeCloneErrors(errors))
|
|
2832
3082
|
}
|
|
2833
3083
|
|
|
2834
3084
|
/** 从 GitHub Release 下载预构建 tgz 装配到 node_modules/<pkgName>(gh CLI 通道,含绝对路径候选)。 */
|
|
@@ -2865,6 +3115,24 @@ async function installBundleFromRelease(subRepo, pkgName, target) {
|
|
|
2865
3115
|
}
|
|
2866
3116
|
}
|
|
2867
3117
|
|
|
3118
|
+
/** 套装探测(唯一入口):main → master,且**必须内容像 .gitmodules**(含 [submodule "x"] 段)才算套装。
|
|
3119
|
+
* 判据是内容、不是"探测非 null":2026-09-19 用户反馈装 dsh-whale-widget 被判成 submodule 套装置仓库、
|
|
3120
|
+
* clone 后报「未找到 .gitmodules」——根因就是代理/CDN 对**不存在的文件**回 2xx(空 body 也算"读到")。 */
|
|
3121
|
+
async function probeGitmodules(repo) {
|
|
3122
|
+
const main = await rawTextWithFallback(repo, 'main', '.gitmodules')
|
|
3123
|
+
if (looksLikeGitmodules(main)) return main
|
|
3124
|
+
const master = await rawTextWithFallback(repo, 'master', '.gitmodules')
|
|
3125
|
+
return looksLikeGitmodules(master) ? master : null
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
/** 安装类型决策(纯函数,导出供 test-suite-detect.mjs 断言):`.gitmodules` 的**内容**说了算——
|
|
3129
|
+
* 显式 suite 请求若探测内容不像 .gitmodules 也回落普通插件安装(前端标记可能来自 24h 缓存的误判,
|
|
3130
|
+
* 不能当判据);技能请求不受影响。 */
|
|
3131
|
+
export function resolveInstallKind(requestKind, probeText) {
|
|
3132
|
+
if (requestKind === 'skill') return 'skill'
|
|
3133
|
+
return looksLikeGitmodules(probeText) ? 'suite' : 'plugin'
|
|
3134
|
+
}
|
|
3135
|
+
|
|
2868
3136
|
/** 套装安装(submodule 聚合仓库):照仓库 install.ps1/README 语义——
|
|
2869
3137
|
* clone 套装 → 手动镜像拉取子模块 → 按类型装配(bundle 插件含 Release tgz 兜底 / 普通插件 / 技能 / agent 预设)。
|
|
2870
3138
|
* 不执行第三方脚本本体(安全护栏:脚本型只读语义不运行)。 */
|
|
@@ -2876,7 +3144,12 @@ async function runSuiteInstallJob(job, ctx) {
|
|
|
2876
3144
|
mkdirSync(tmpDir, { recursive: true })
|
|
2877
3145
|
await gitCloneRepo(job.repo, tmpDir, job.source)
|
|
2878
3146
|
const subs = readGitmodules(tmpDir)
|
|
2879
|
-
if (subs.length === 0)
|
|
3147
|
+
if (subs.length === 0) {
|
|
3148
|
+
// 探测与仓库实际内容不符(假阳性 / 仓库已重构):**不报失败**,把决定权交回调用方
|
|
3149
|
+
// 回落普通插件安装(npm → Release → git 规格),用户不该因为一次误判装不上插件。
|
|
3150
|
+
job.stage = 'detecting'
|
|
3151
|
+
return { notASuite: true }
|
|
3152
|
+
}
|
|
2880
3153
|
job.stage = 'detecting'
|
|
2881
3154
|
const patchPath = findPatchPath(ctx)
|
|
2882
3155
|
const profileDir = dirname(patchPath)
|
|
@@ -3185,7 +3458,9 @@ async function aiRepair(job, ctx, profileDir, candidates, lastError) {
|
|
|
3185
3458
|
job.error = `本地 AI 修复通道没有可用的子代理提供方:${String(error)};请手动执行:dsh plugin --profile web add <包名>`
|
|
3186
3459
|
return
|
|
3187
3460
|
}
|
|
3188
|
-
|
|
3461
|
+
// 跨平台提示:corepack 的位置随安装方式/系统而异(Windows 官方安装器在 <node bin>/node_modules,
|
|
3462
|
+
// Linux 的 npm 全局在 <prefix>/lib/node_modules),所以给子代理的提示不再写死路径
|
|
3463
|
+
const corepackRunner = resolvePnpmRunners()[0]
|
|
3189
3464
|
const prompt = [
|
|
3190
3465
|
'你是 DeepSeek Harness 的插件安装专家。用户通过插件管理面板安装插件失败,现在由你主动接管安装(不是被动"修复",而是像人工一样把它装好)。',
|
|
3191
3466
|
`目标仓库:${job.repo}`,
|
|
@@ -3194,7 +3469,7 @@ async function aiRepair(job, ctx, profileDir, candidates, lastError) {
|
|
|
3194
3469
|
`此前确定性通道的错误:${lastError ?? '未知'}`,
|
|
3195
3470
|
'接管步骤:',
|
|
3196
3471
|
`1) 先弄清楚装什么:若根包是 private 的聚合仓库(monorepo),用 gh api repos/${job.repo}/git/trees/main?recursive=1 或读 packages 目录下的 package.json 找出子包名;优先选聚合包(名字含 all,如 dsh-web-ui-all,一个包装全部);`,
|
|
3197
|
-
`2) 用 corepack
|
|
3472
|
+
`2) 用 corepack/pnpm 安装(本机可用的执行方式:${corepackRunner?.note ?? 'corepack pnpm 或 pnpm'},即在该方式后接 add <包名> --registry https://registry.npmmirror.com,工作目录 ${profileDir};Windows 上是 node <corepack.js> pnpm,Linux 上通常是 corepack pnpm 或直接 pnpm);镜像 404 时改 --registry https://registry.npmjs.org;都不行再用 git 通道;`,
|
|
3198
3473
|
'3) **node 网络黑洞判定**:pnpm 长时间无下载进度(Progress 停在 downloaded 0)、或报 socket hang up / EPERM / TIMEOUT、或 node 脚本连 127.0.0.1 都超时,说明本机拦截 node 进程网络——此时 curl 与系统 git 通常仍可用,立即改用 curl 手动安装:先 `curl -s https://registry.npmmirror.com/<包名>` 取最新版本号,再 `curl -sL -o <临时目录>/pkg.tgz https://registry.npmmirror.com/<包名>/-/<包名>-<版本>.tgz`,解压 tar -xzf 后把 package 内容放入 profile/node_modules/<包名>(先备份旧目录);包有 dependencies 时用同样方式逐个 curl 拉取补齐到 node_modules;零依赖包(如 dsh-skin)一条龙即可完成;',
|
|
3199
3474
|
`4) 安装成功后按官方 dsh plugin add 规则落配置:包声明 dsh.bundle 时把包名追加进 profile 目录 package.json 的 dsh.profile.bundles 数组;普通插件在 cordis.patch.yml 追加 insert 行(id 由包名去 scope、非字母数字转连字符生成,name 填包名);`,
|
|
3200
3475
|
'5) 装完自查:确认包已出现在 node_modules、配置已写入;必要时用 gh api 核对仓库信息;',
|
|
@@ -3773,10 +4048,13 @@ async function runAiStep(job, step, ctx, profileDir, signal) {
|
|
|
3773
4048
|
const pkg = String(step.package ?? '')
|
|
3774
4049
|
if (!/^(@[A-Za-z0-9_\-\.]+\/)?[A-Za-z0-9_\-\.]+(@[A-Za-z0-9_\-\.]+)?$/u.test(pkg)) throw new Error(`包名不合法:${pkg}`)
|
|
3775
4050
|
const dir = resolvePlaceholders(String(step.directory ?? profile), { profileDir: profile })
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
4051
|
+
// 跨平台:不再拼 `node <node bin>/node_modules/corepack/dist/corepack.js`(Linux 上必 MODULE_NOT_FOUND,
|
|
4052
|
+
// 2026-09-20 另一位用户的 install-npm 失败现场就是这一行),改为按优先级尝试可用的执行方式
|
|
4053
|
+
const args = ['add', pkg, '--dir', dir, '--registry', 'https://registry.npmmirror.com']
|
|
4054
|
+
const { runner } = await runPnpmWithFallback(args, {
|
|
4055
|
+
execOpts: { timeout: 900000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, signal },
|
|
4056
|
+
})
|
|
4057
|
+
jobLog(job, ` 运行: ${runner.note} ${args.join(' ')}`)
|
|
3780
4058
|
return
|
|
3781
4059
|
}
|
|
3782
4060
|
case 'write-file': {
|
|
@@ -4499,25 +4777,30 @@ async function runInstallJob(job, ctx) {
|
|
|
4499
4777
|
let candidates = [job.packageName].filter((name) => typeof name === 'string' && name !== '')
|
|
4500
4778
|
let subpackageMode = false
|
|
4501
4779
|
if (candidates.length === 0) {
|
|
4502
|
-
// 套装兜底:submodule 聚合仓库(根 .gitmodules
|
|
4503
|
-
// 不依赖前端标记(搜索结果 enrich 是异步的、索引浏览条目无 enrich
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
if (
|
|
4780
|
+
// 套装兜底:submodule 聚合仓库(根 .gitmodules 内容校验通过)→ 自动转套装安装,
|
|
4781
|
+
// 不依赖前端标记(搜索结果 enrich 是异步的、索引浏览条目无 enrich)。
|
|
4782
|
+
// 判据是**内容**(resolveInstallKind),不是"探测非 null":后者会把代理/CDN 对不存在文件回的
|
|
4783
|
+
// 2xx 空 body、垃圾页当成套装(2026-09-19 用户反馈的「未找到 .gitmodules」事故根因)。
|
|
4784
|
+
if (resolveInstallKind(job.kind, await probeGitmodules(job.repo)) === 'suite') {
|
|
4507
4785
|
job.kind = 'suite'
|
|
4508
|
-
await runSuiteInstallJob(job, ctx)
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4786
|
+
const suiteResult = await runSuiteInstallJob(job, ctx)
|
|
4787
|
+
// clone 后才发现没有 .gitmodules(探测假阳性 / 仓库已重构)→ 回落普通安装,不给用户一个失败
|
|
4788
|
+
if (suiteResult?.notASuite !== true) return
|
|
4789
|
+
job.kind = 'plugin'
|
|
4790
|
+
job.suiteNote = '探测到的 .gitmodules 与仓库实际内容不符(不是 submodule 套装仓库),已自动回落普通插件安装'
|
|
4791
|
+
}
|
|
4792
|
+
// 兜底:宿主端自行拉取仓库元数据(githubJson 与 curl 竞速 + 8s 超时降级,黑洞期不卡 40s)。
|
|
4793
|
+
// 8s 而非旧值 3s:IPv6 无路由的环境里单条通道就要 5.4s,3s 预算必输 → branch 恒为 main,
|
|
4794
|
+
// 默认分支为 dev 的仓库会取错分支(2026-09-20 另一位用户实测)。
|
|
4512
4795
|
const meta = await Promise.race([
|
|
4513
4796
|
Promise.any([
|
|
4514
4797
|
githubJson(`${GITHUB_API}/repos/${job.repo}`),
|
|
4515
|
-
curlJson(`${GITHUB_API}/repos/${job.repo}`, 12000),
|
|
4798
|
+
curlJson(`${GITHUB_API}/repos/${job.repo}`, 12000, {}, { ipv4: true }),
|
|
4516
4799
|
]),
|
|
4517
|
-
new Promise((resolve) => setTimeout(() => resolve(null),
|
|
4800
|
+
new Promise((resolve) => setTimeout(() => resolve(null), META_BUDGET_MS)),
|
|
4518
4801
|
]).catch(() => null)
|
|
4519
4802
|
const branch = meta?.default_branch ?? 'main'
|
|
4520
|
-
const pkg = await
|
|
4803
|
+
const { pkg, reason } = await fetchRepoPackageEx(job.repo, branch)
|
|
4521
4804
|
if (pkg === null) {
|
|
4522
4805
|
// 无 package.json:先探测是否技能仓库(含 SKILL.md)→ 自动转技能安装;
|
|
4523
4806
|
// 否则标记 hint=repo-land,前端给出「仓库落地」一键入口(克隆到本地目录)。
|
|
@@ -4529,7 +4812,9 @@ async function runInstallJob(job, ctx) {
|
|
|
4529
4812
|
}
|
|
4530
4813
|
job.status = 'failed'
|
|
4531
4814
|
job.hint = 'repo-land'
|
|
4532
|
-
|
|
4815
|
+
// 文案区分「抓取超时/不可达」与「真的没有」——旧代码两者共用一个出口,报错永远说"文件不存在"
|
|
4816
|
+
job.error = packageProbeErrorText(job.repo, branch, reason)
|
|
4817
|
+
job.probeReason = reason
|
|
4533
4818
|
return
|
|
4534
4819
|
}
|
|
4535
4820
|
if (pkg.private === true) {
|
|
@@ -5320,6 +5605,36 @@ async function handle(ctx, req, res) {
|
|
|
5320
5605
|
const all = body.all === true
|
|
5321
5606
|
const auth = readGithubAuth()
|
|
5322
5607
|
let items = []
|
|
5608
|
+
// 给前端的补充说明(如"代码搜索需登录")——放响应里,前端据此给提示,不改 items 结构
|
|
5609
|
+
const extraNotes = {}
|
|
5610
|
+
// 增量检索(前端浏览器直连成功时**并行**调用):只补「npm 包名映射 + in:readme 重查 + 代码搜索子包」,
|
|
5611
|
+
// 不重复仓库搜索。为什么需要它:直连成功就不会走本路由,而未登录用户恰恰只能走直连
|
|
5612
|
+
// (2026-09-20 事故:未登录 + 索引加载失败 → 三条检索路全断,搜 web-all 搜不到 dsh-web)。
|
|
5613
|
+
if (body.extras === true) {
|
|
5614
|
+
const extra = []
|
|
5615
|
+
if (raw !== '') {
|
|
5616
|
+
try { extra.push(...await searchNpmPackages(raw, orderedRegistries(readSources()), 3, auth.token)) } catch {}
|
|
5617
|
+
try {
|
|
5618
|
+
const again = await githubJson(
|
|
5619
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(`${raw} in:name,description,readme${all ? '' : ' topic:dsh-plugin'}`)}&sort=stars&order=desc&per_page=20&page=1`,
|
|
5620
|
+
req.signal,
|
|
5621
|
+
auth.token,
|
|
5622
|
+
)
|
|
5623
|
+
extra.push(...normalizePlatformItems(again.items ?? [], 'main').map((it) => ({ ...it, source: 'github', viaReadme: true })))
|
|
5624
|
+
} catch {}
|
|
5625
|
+
try { extra.push(...await searchSubpackageItems(raw, auth.token, req.signal)) } catch {}
|
|
5626
|
+
}
|
|
5627
|
+
sendJson(res, 200, {
|
|
5628
|
+
ok: true,
|
|
5629
|
+
query: raw,
|
|
5630
|
+
items: extra,
|
|
5631
|
+
extras: true,
|
|
5632
|
+
authenticated: auth.loggedIn,
|
|
5633
|
+
source: 'github',
|
|
5634
|
+
...(raw !== '' && !auth.loggedIn ? { codeSearchSkipped: true } : {}),
|
|
5635
|
+
})
|
|
5636
|
+
return
|
|
5637
|
+
}
|
|
5323
5638
|
if (body.multi === true) {
|
|
5324
5639
|
// 多源汇总:GitHub + 全部自定义搜索源并行检索,结果合并(每项带 source 标记)。
|
|
5325
5640
|
// Gitee 为直装模式(关键词搜索无意义),不参与多源汇总。
|
|
@@ -5426,6 +5741,11 @@ async function handle(ctx, req, res) {
|
|
|
5426
5741
|
const data = await fetchJsonUrl(url, 15000, custom.headers ?? {})
|
|
5427
5742
|
items = normalizePlatformItems(data, 'main').map((item) => ({ ...item, source, sourceName: custom.name }))
|
|
5428
5743
|
} else {
|
|
5744
|
+
// npm 包名搜索与 GitHub 仓库搜索**并行**:npm 侧要查 registry 搜索 + packument + 仓库元数据
|
|
5745
|
+
// (实测 4.5~9s),串行会把两段等待叠加到用户身上。
|
|
5746
|
+
const npmPromise = raw !== '' && !body.skills
|
|
5747
|
+
? searchNpmPackages(raw, orderedRegistries(readSources()), 3, auth.token).catch(() => [])
|
|
5748
|
+
: Promise.resolve([])
|
|
5429
5749
|
const data = await githubJson(
|
|
5430
5750
|
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(all ? query : `${query} topic:dsh-plugin`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
5431
5751
|
req.signal,
|
|
@@ -5433,51 +5753,50 @@ async function handle(ctx, req, res) {
|
|
|
5433
5753
|
)
|
|
5434
5754
|
items = normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github' }))
|
|
5435
5755
|
items = await enrichItems(items)
|
|
5436
|
-
// monorepo 子包增强(OpenViking/examples/dsh-memory-plugin
|
|
5756
|
+
// monorepo 子包增强(OpenViking/examples/dsh-memory-plugin 等可按子包名搜到;
|
|
5757
|
+
// 代码搜索需登录,未登录时该函数返回空数组,见 searchSubpackageItems 的说明)
|
|
5437
5758
|
if (raw !== '' && !body.skills) {
|
|
5759
|
+
for (const sub of await searchSubpackageItems(raw, auth.token, req.signal)) {
|
|
5760
|
+
if (!items.some((x) => x.fullName === sub.fullName)) items.push(sub)
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
// ── B′:README 重查(2026-09-20)────────────────────────────────────────
|
|
5764
|
+
// 仓库搜索只在「仓库名 + 描述 + topics」里找词,所以只写在 README 或仓库文件里的名字搜不到。
|
|
5765
|
+
// 典型:`web-all` 只是 npm 包名 + `packages/dsh-web-all/package.json` 的内容,
|
|
5766
|
+
// `q=web-all topic:dsh-plugin` 32 条里没有 dsh-web;而 `web-all in:readme` 第 9 条就是它。
|
|
5767
|
+
// 首屏没有"名字逐词命中"的条目时,用 in:name,description,readme 再查一次(未登录也能用)。
|
|
5768
|
+
if (raw !== '' && !body.skills && page === 1 && !hasDirectNameHit(items, raw)) {
|
|
5438
5769
|
try {
|
|
5439
|
-
const
|
|
5440
|
-
`${GITHUB_API}/search/
|
|
5770
|
+
const again = await githubJson(
|
|
5771
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(`${raw} in:name,description,readme${all ? '' : ' topic:dsh-plugin'}`)}&sort=stars&order=desc&per_page=20&page=1`,
|
|
5441
5772
|
req.signal,
|
|
5442
5773
|
auth.token,
|
|
5443
5774
|
)
|
|
5444
|
-
const
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
if (
|
|
5448
|
-
const repoName = hit.repository?.full_name ?? ''
|
|
5449
|
-
if (!repoName) continue
|
|
5450
|
-
const dir = hitPath.split('/').slice(0, -1).join('/')
|
|
5451
|
-
let packageName = dir.split('/').slice(-1)[0]
|
|
5452
|
-
try {
|
|
5453
|
-
const pkgText = await rawTextWithFallback(repoName, 'main', hitPath)
|
|
5454
|
-
if (pkgText !== null) {
|
|
5455
|
-
const pkg = JSON.parse(pkgText)
|
|
5456
|
-
if (pkg && typeof pkg.name === 'string') packageName = pkg.name
|
|
5457
|
-
}
|
|
5458
|
-
} catch {}
|
|
5459
|
-
subItems.push({
|
|
5460
|
-
fullName: repoName,
|
|
5461
|
-
description: `子包:${dir}`,
|
|
5462
|
-
htmlUrl: `https://github.com/${repoName}/tree/main/${dir}`,
|
|
5463
|
-
stars: 0,
|
|
5464
|
-
updatedAt: '',
|
|
5465
|
-
defaultBranch: 'main',
|
|
5466
|
-
topics: [],
|
|
5467
|
-
source: 'github',
|
|
5468
|
-
subpackagePath: dir,
|
|
5469
|
-
packageName,
|
|
5470
|
-
})
|
|
5471
|
-
if (subItems.length >= 5) break
|
|
5472
|
-
}
|
|
5473
|
-
for (const sub of subItems) {
|
|
5474
|
-
if (!items.some((x) => x.fullName === sub.fullName)) items.push(sub)
|
|
5775
|
+
for (const it of normalizePlatformItems(again.items ?? [], 'main')) {
|
|
5776
|
+
if (items.some((x) => x.fullName === it.fullName)) continue
|
|
5777
|
+
items.push({ ...it, source: 'github', viaReadme: true })
|
|
5778
|
+
if (items.length >= 40) break
|
|
5475
5779
|
}
|
|
5476
5780
|
} catch {}
|
|
5477
5781
|
}
|
|
5782
|
+
// ── A:npm 包名搜索(2026-09-20)────────────────────────────────────────
|
|
5783
|
+
// 用户输入常常是 npm 包名(`web-all`),而 GitHub 元数据里没有它 → 走 registry 搜索接口反查
|
|
5784
|
+
// 包 → repository.url → 仓库,命中**置顶**并带 npmPackage 标记(前端按包名安装)。
|
|
5785
|
+
// 不依赖静态索引、不依赖 GitHub 登录;registry 走配置的软件源(默认国内镜像)。
|
|
5786
|
+
// 已在列表里的同仓库条目(例如代码搜索加进来的"子包")合并 npm 信息后上移——精确命中不该排在第 21 位。
|
|
5787
|
+
if (raw !== '' && !body.skills) {
|
|
5788
|
+
for (const it of (await npmPromise).reverse()) {
|
|
5789
|
+
const existingIdx = items.findIndex((x) => x.fullName === it.fullName)
|
|
5790
|
+
const existing = existingIdx >= 0 ? items.splice(existingIdx, 1)[0] : null
|
|
5791
|
+
items.unshift({ ...(existing ?? {}), ...it })
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
// 代码搜索(monorepo 子包)需要 GitHub 登录:未登录时它拿不到结果,
|
|
5795
|
+
// 前端据此提示"登录后可按子包名搜索"(2026-09-20 事故复盘:未登录用户三条检索路径全断)
|
|
5796
|
+
if (raw !== '' && !body.skills && !auth.loggedIn) extraNotes.codeSearchSkipped = true
|
|
5478
5797
|
}
|
|
5479
5798
|
items = items.filter((item) => item.fullName !== '')
|
|
5480
|
-
sendJson(res, 200, { ok: true, query, items, authenticated: auth.loggedIn, source })
|
|
5799
|
+
sendJson(res, 200, { ok: true, query, items, authenticated: auth.loggedIn, source, ...(extraNotes) })
|
|
5481
5800
|
return
|
|
5482
5801
|
}
|
|
5483
5802
|
|
|
@@ -5492,24 +5811,26 @@ async function handle(ctx, req, res) {
|
|
|
5492
5811
|
if (pathname === `${ROUTE_PREFIX}/repo`) {
|
|
5493
5812
|
const repo = githubRepoInfo(typeof body.repo === 'string' ? body.repo : '')
|
|
5494
5813
|
const auth = readGithubAuth()
|
|
5495
|
-
// meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,
|
|
5814
|
+
// meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,8 秒超时即降级——
|
|
5496
5815
|
// Promise.any 全失败时要等最慢分支(黑洞期 https 41.5s),加 race 超时避免拖累整体。
|
|
5816
|
+
// 8s 而非旧值 3s:IPv6 无路由的环境里单条通道就要 5.4s(2026-09-20 实测),3s 必输 → branch 取错。
|
|
5497
5817
|
let meta = null
|
|
5498
5818
|
try {
|
|
5499
5819
|
meta = await Promise.race([
|
|
5500
5820
|
Promise.any([
|
|
5501
5821
|
githubJson(`${GITHUB_API}/repos/${repo}`, req.signal, auth.token),
|
|
5502
|
-
curlJson(`${GITHUB_API}/repos/${repo}`, 12000),
|
|
5822
|
+
curlJson(`${GITHUB_API}/repos/${repo}`, 12000, {}, { ipv4: true }),
|
|
5503
5823
|
]),
|
|
5504
|
-
new Promise((resolve) => setTimeout(() => resolve(null),
|
|
5824
|
+
new Promise((resolve) => setTimeout(() => resolve(null), META_BUDGET_MS)),
|
|
5505
5825
|
])
|
|
5506
5826
|
} catch {}
|
|
5507
5827
|
const branch = meta?.default_branch ?? 'main'
|
|
5508
5828
|
const pkg = await fetchRepoPackage(repo, branch)
|
|
5509
5829
|
const skill = await detectSkillRepo(repo, branch)
|
|
5510
5830
|
const skillMeta = skill.hasSkill ? await fetchSkillMeta(repo, branch, skill.skillDir) : null
|
|
5511
|
-
// 套装识别:根 .gitmodules
|
|
5512
|
-
|
|
5831
|
+
// 套装识别:根 .gitmodules 存在**且内容真的是 gitmodules**(只判非 null 会被代理/CDN 对不存在
|
|
5832
|
+
// 文件回的 2xx 空 body 骗到,把普通插件标成套装——2026-09-19 用户反馈事故)
|
|
5833
|
+
const hasSuite = looksLikeGitmodules(await rawTextWithFallback(repo, branch, '.gitmodules'))
|
|
5513
5834
|
// 官方安装方式(详情面板展示 + 一键复制,供用户手动安装):
|
|
5514
5835
|
// 套装 → 仓库 install.ps1/README 的官方步骤;普通/聚合 → dsh plugin add 官方命令
|
|
5515
5836
|
let installCommand = null
|
|
@@ -5949,9 +6270,13 @@ async function handle(ctx, req, res) {
|
|
|
5949
6270
|
}
|
|
5950
6271
|
let lastError = null
|
|
5951
6272
|
let formatError = null
|
|
6273
|
+
// 总预算:索引源扩容到 5 个后,逐个 15s 最坏要等 75s(用户只会看到"市场一直转圈")。
|
|
6274
|
+
// 单源 8s + 整体 20s 封顶,超预算立刻进入落盘缓存兜底。
|
|
6275
|
+
const deadline = Date.now() + 20000
|
|
5952
6276
|
for (const src of indexList) {
|
|
6277
|
+
if (Date.now() > deadline) break
|
|
5953
6278
|
try {
|
|
5954
|
-
const data = await fetchJsonUrl(src.url,
|
|
6279
|
+
const data = await fetchJsonUrl(src.url, 8000)
|
|
5955
6280
|
if (data && Array.isArray(data.items)) {
|
|
5956
6281
|
marketIndexCache = { at: Date.now(), data, sourceName: src.name }
|
|
5957
6282
|
try { await writeFile(MARKET_INDEX_CACHE_FILE, JSON.stringify({ at: Date.now(), data, sourceName: src.name }), 'utf8') } catch {}
|
|
@@ -5974,7 +6299,9 @@ async function handle(ctx, req, res) {
|
|
|
5974
6299
|
const reason = formatError !== null
|
|
5975
6300
|
? formatError
|
|
5976
6301
|
: `网络不可达(${indexList.length} 个索引源全部失败):${lastError?.message ?? '未知错误'}`
|
|
5977
|
-
|
|
6302
|
+
// 说清后果(2026-09-20 另一位用户实测的困惑):索引不在时市场只剩 GitHub 实时结果,
|
|
6303
|
+
// 收录条目与本地索引模糊匹配一起失效(他搜 web-all 搜不到 dsh-web 全家桶就是这个原因)
|
|
6304
|
+
sendError(res, 500, `索引加载失败:${reason}(此时市场只能搜 GitHub 实时结果,收录条目可能看不到;可在「软件源 → 索引源」增删/更换索引源后重试)`)
|
|
5978
6305
|
return
|
|
5979
6306
|
}
|
|
5980
6307
|
|
|
@@ -7094,9 +7421,9 @@ async function handle(ctx, req, res) {
|
|
|
7094
7421
|
sendError(res, 400, 'deepseek-harness 是 DSH 框架本体,不是插件——无需安装(升级请用官方 dsh 升级方式)')
|
|
7095
7422
|
return
|
|
7096
7423
|
}
|
|
7097
|
-
const
|
|
7098
|
-
if ((
|
|
7099
|
-
sendError(res, 400, `${
|
|
7424
|
+
const requestedKind = body.kind === 'skill' ? 'skill' : body.kind === 'suite' ? 'suite' : 'plugin'
|
|
7425
|
+
if ((requestedKind === 'skill' || requestedKind === 'suite') && repo === '') {
|
|
7426
|
+
sendError(res, 400, `${requestedKind === 'skill' ? '技能' : '套装'}安装必须提供仓库(owner/name)`)
|
|
7100
7427
|
return
|
|
7101
7428
|
}
|
|
7102
7429
|
const source = body.source === 'gitee' ? body.source : 'github'
|
|
@@ -7113,6 +7440,14 @@ async function handle(ctx, req, res) {
|
|
|
7113
7440
|
sendError(res, 409, '该插件正在安装/更新中,请等待当前任务完成后再试')
|
|
7114
7441
|
return
|
|
7115
7442
|
}
|
|
7443
|
+
// 套装请求必须复检内容(2026-09-19 事故):前端标记可能来自 24h enrich 缓存的误判,
|
|
7444
|
+
// 探测内容不像 .gitmodules 就按普通插件安装——别把用户送进注定失败的套装通道。
|
|
7445
|
+
let kind = requestedKind
|
|
7446
|
+
let suiteNote = null
|
|
7447
|
+
if (requestedKind === 'suite') {
|
|
7448
|
+
kind = resolveInstallKind('suite', await probeGitmodules(repo))
|
|
7449
|
+
if (kind !== 'suite') suiteNote = '探测未发现有效的 .gitmodules(不是 submodule 套装仓库),已按普通插件安装'
|
|
7450
|
+
}
|
|
7116
7451
|
const job = {
|
|
7117
7452
|
id: `job-${installJobSeq += 1}`,
|
|
7118
7453
|
repo,
|
|
@@ -7132,10 +7467,20 @@ async function handle(ctx, req, res) {
|
|
|
7132
7467
|
update: body.update === true,
|
|
7133
7468
|
kind,
|
|
7134
7469
|
}
|
|
7470
|
+
if (suiteNote !== null) job.suiteNote = suiteNote
|
|
7135
7471
|
installJobs.set(job.id, job)
|
|
7472
|
+
// 套装通道兜底:探测说有、clone 下来却没有 .gitmodules(假阳性 / 仓库已重构)时
|
|
7473
|
+
// 回落普通插件安装,而不是给用户一个「未找到 .gitmodules」的失败。
|
|
7474
|
+
const runSuiteThenFallback = async () => {
|
|
7475
|
+
const result = await runSuiteInstallJob(job, ctx)
|
|
7476
|
+
if (result?.notASuite !== true) return
|
|
7477
|
+
job.kind = 'plugin'
|
|
7478
|
+
job.suiteNote = '仓库实际内容与探测不符(没有 .gitmodules),已自动回落普通插件安装'
|
|
7479
|
+
await runInstallJob(job, ctx)
|
|
7480
|
+
}
|
|
7136
7481
|
// 后台执行:请求立即返回,安装不受客户端断开/离开面板影响
|
|
7137
|
-
void (kind === 'skill' ? runSkillInstallJob(job) : kind === 'suite' ?
|
|
7138
|
-
sendJson(res, 200, { ok: true, jobId: job.id, status: 'installing', kind })
|
|
7482
|
+
void (kind === 'skill' ? runSkillInstallJob(job) : kind === 'suite' ? runSuiteThenFallback() : runInstallJob(job, ctx))
|
|
7483
|
+
sendJson(res, 200, { ok: true, jobId: job.id, status: 'installing', kind, ...(suiteNote !== null ? { suiteNote } : {}) })
|
|
7139
7484
|
return
|
|
7140
7485
|
}
|
|
7141
7486
|
|
|
@@ -7383,7 +7728,7 @@ async function handle(ctx, req, res) {
|
|
|
7383
7728
|
let usedUrl = ''
|
|
7384
7729
|
for (const url of urls) {
|
|
7385
7730
|
try {
|
|
7386
|
-
await execFileAsync(
|
|
7731
|
+
await execFileAsync(gitBin(), ['clone', '--depth', '1', url, target], { cwd: root, timeout: 600000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, env: gitEnv() })
|
|
7387
7732
|
cloned = true
|
|
7388
7733
|
usedUrl = url
|
|
7389
7734
|
break
|