@noob-stupid/dsh-plugin-console 0.3.46 → 0.3.48
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 +314 -319
- package/README.zh.md +9 -13
- package/lib/client.js +4 -4
- package/lib/index.js +278 -101
- package/package.json +44 -44
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 的根因),返回清理数量。 */
|
|
@@ -1705,11 +1696,12 @@ const API_MIRROR_PREFIXES = [
|
|
|
1705
1696
|
'https://ghfast.top/https://api.github.com',
|
|
1706
1697
|
]
|
|
1707
1698
|
|
|
1708
|
-
/** raw.githubusercontent.com 的镜像竞速前缀(与浏览器端四通道一致)。
|
|
1699
|
+
/** raw.githubusercontent.com 的镜像竞速前缀(与浏览器端四通道一致)。
|
|
1700
|
+
* 2026-09-19 移除 mirror.ghproxy.com:该域名早已失效(实测连接超时;失效域名被停放页接管时
|
|
1701
|
+
* 会回 2xx HTML —— 正是"普通插件被误判成 submodule 套装"的假阳性来源之一)。 */
|
|
1709
1702
|
const RAW_MIRROR_PREFIXES = [
|
|
1710
1703
|
'https://ghproxy.net/https://raw.githubusercontent.com',
|
|
1711
1704
|
'https://ghfast.top/https://raw.githubusercontent.com',
|
|
1712
|
-
'https://mirror.ghproxy.com/https://raw.githubusercontent.com',
|
|
1713
1705
|
]
|
|
1714
1706
|
|
|
1715
1707
|
/** 并行竞速:任一分支拿到 2xx 即胜出;非 2xx 与错误都视为失败,全部失败则拒绝。 */
|
|
@@ -1868,9 +1860,12 @@ async function githubText(url, signal, token = null) {
|
|
|
1868
1860
|
}
|
|
1869
1861
|
|
|
1870
1862
|
/** 用 curl 子进程请求 JSON(绕过本机中间设备对 node:https TLS 指纹的拦截;curl 走系统网络栈)。 */
|
|
1871
|
-
async function curlJson(url, timeout = 15000, headers = {}) {
|
|
1863
|
+
async function curlJson(url, timeout = 15000, headers = {}, { ipv4 = false } = {}) {
|
|
1872
1864
|
const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
|
1873
1865
|
const args = ['-s', '-m', String(Math.max(5, Math.ceil(timeout / 1000))), '-H', 'accept: application/json', '-w', '\n__HTTP__%{http_code}']
|
|
1866
|
+
// ipv4:只给 GitHub 域名用——「解析出 IPv6 但没有 IPv6 路由」的环境里 curl 默认要先空等 ~5.2s 才回退
|
|
1867
|
+
// IPv4(2026-09-20 另一位用户实测:默认 5473ms vs `-4` 的 461ms);双栈正常的环境无副作用。
|
|
1868
|
+
if (ipv4) args.push('-4')
|
|
1874
1869
|
for (const [key, value] of Object.entries(headers)) args.push('-H', `${key}: ${value}`)
|
|
1875
1870
|
args.push(url)
|
|
1876
1871
|
const { stdout } = await execFileAsync(bin, args, { timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 })
|
|
@@ -1881,12 +1876,17 @@ async function curlJson(url, timeout = 15000, headers = {}) {
|
|
|
1881
1876
|
return JSON.parse(body)
|
|
1882
1877
|
}
|
|
1883
1878
|
|
|
1884
|
-
/** 用 curl 拉取文本(raw 文件用:本机 curl 直连 raw.githubusercontent.com 秒回,绕开 node:https 黑洞)。
|
|
1885
|
-
|
|
1879
|
+
/** 用 curl 拉取文本(raw 文件用:本机 curl 直连 raw.githubusercontent.com 秒回,绕开 node:https 黑洞)。
|
|
1880
|
+
* 导出供 test-suite-detect.mjs 断言传输层口径(200 空 body → 空串,404 → 抛 HTTP 404)。 */
|
|
1881
|
+
export async function curlText(url, timeout = 10000, { ipv4 = false } = {}) {
|
|
1886
1882
|
const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
|
1883
|
+
const args = ['-s', '-m', String(Math.max(3, Math.ceil(timeout / 1000))), '-H', 'accept: text/plain', '-w', '\n__HTTP__%{http_code}']
|
|
1884
|
+
// 见 curlJson 注释:GitHub 域名走 IPv4 快通道(IPv6 无路由的环境下默认要白等 ~5.2s)
|
|
1885
|
+
if (ipv4) args.push('-4')
|
|
1886
|
+
args.push(url)
|
|
1887
1887
|
const { stdout } = await execFileAsync(
|
|
1888
1888
|
bin,
|
|
1889
|
-
|
|
1889
|
+
args,
|
|
1890
1890
|
{ timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
|
|
1891
1891
|
)
|
|
1892
1892
|
const m = String(stdout).match(/\n__HTTP__(\d+)\s*$/u)
|
|
@@ -2049,7 +2049,9 @@ async function enrichItemOne(item) {
|
|
|
2049
2049
|
detectSkillRepo(item.fullName, branch),
|
|
2050
2050
|
rawTextWithFallback(item.fullName, branch, '.gitmodules'),
|
|
2051
2051
|
])
|
|
2052
|
-
|
|
2052
|
+
// 套装判定必须过内容校验:代理/CDN 对不存在的 .gitmodules 也可能回 2xx 空 body,
|
|
2053
|
+
// 只判"探测非 null"会把普通插件标成套装置仓库(2026-09-19 事故)。
|
|
2054
|
+
if (suiteResult.status === 'fulfilled' && looksLikeGitmodules(suiteResult.value)) {
|
|
2053
2055
|
hasSuite = true
|
|
2054
2056
|
}
|
|
2055
2057
|
if (pkgResult.status === 'fulfilled') {
|
|
@@ -2149,11 +2151,53 @@ function githubRepoInfo(repo) {
|
|
|
2149
2151
|
return `${match[1]}/${match[2]}`
|
|
2150
2152
|
}
|
|
2151
2153
|
|
|
2154
|
+
/** 「真的读到了文件内容」判定:null / 空串 / 纯空白都不算。导出供 test-suite-detect.mjs 断言。
|
|
2155
|
+
* 真实事故(2026-09-19,用户反馈装 dsh-whale-widget 报「未找到 .gitmodules」):
|
|
2156
|
+
* 本机加速器/代理(Watt Toolkit 之类,劫持 raw.githubusercontent.com 到 127.0.0.1)
|
|
2157
|
+
* 会对**不存在的文件**回 2xx + 空 body;旧代码只判 `body === null`,
|
|
2158
|
+
* 于是空串被当成"文件存在" → 普通插件被判定成 submodule 套装置仓库 → 安装必然失败。 */
|
|
2159
|
+
export function readBodyOrNull(body) {
|
|
2160
|
+
return typeof body === 'string' && body.trim() !== '' ? body : null
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/** .gitmodules 内容校验:必须是真 gitmodules(含 [submodule "x"] 段)才算套装仓库。导出供测试。
|
|
2164
|
+
* 只判"探测非 null"会把代理/CDN 的垃圾响应也算成套装;判定权交给内容,不交给状态码。 */
|
|
2165
|
+
export function looksLikeGitmodules(text) {
|
|
2166
|
+
return typeof text === 'string' && /^\s*\[submodule\s+"/mu.test(text)
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/** raw 抓取外层预算(毫秒)。事件(2026-09-20,另一位用户:Android + proot Ubuntu 容器,域名解析出
|
|
2170
|
+
* IPv6 但容器无 IPv6 路由):4 条通道最快也要 5.4s(curl 直连被 -m 6 掐死、node:https 5435ms、
|
|
2171
|
+
* jsDelivr 5564ms),旧的 5000ms 预算**必然先超时** → 抓取失败被当成"文件不存在",
|
|
2172
|
+
* 报成「仓库没有 package.json」。放宽到 10s:真 404 仍是毫秒级返回,只有全网慢时才多等。 */
|
|
2173
|
+
export const FETCH_BUDGET_MS = 10000
|
|
2174
|
+
/** 仓库元数据(默认分支)探测预算:旧值 3s 在同一环境下同样必输 → branch 恒为 'main',
|
|
2175
|
+
* 默认分支为 dev 的仓库会取错分支。 */
|
|
2176
|
+
export const META_BUDGET_MS = 8000
|
|
2177
|
+
export const FETCH_OK = 'ok'
|
|
2178
|
+
export const FETCH_NOT_FOUND = 'not-found'
|
|
2179
|
+
export const FETCH_UNREACHABLE = 'unreachable'
|
|
2180
|
+
|
|
2181
|
+
/** 竞速组合(纯逻辑,导出供单测):通道结果 + 预算 → { state, body }。
|
|
2182
|
+
* · anyPromise 以 null 成功 = 某通道确定性 404 → not-found
|
|
2183
|
+
* · anyPromise 以字符串成功 = 读到内容 → ok
|
|
2184
|
+
* · anyPromise 全部失败(AggregateError)或超出预算 → unreachable(**不再与 404 混为一谈**) */
|
|
2185
|
+
export function raceFetchOutcome(anyPromise, budgetMs) {
|
|
2186
|
+
const settled = Promise.resolve(anyPromise)
|
|
2187
|
+
.then((body) => (body === null ? { state: FETCH_NOT_FOUND, body: null } : { state: FETCH_OK, body }))
|
|
2188
|
+
.catch(() => ({ state: FETCH_UNREACHABLE, body: null }))
|
|
2189
|
+
return Promise.race([
|
|
2190
|
+
settled,
|
|
2191
|
+
new Promise((resolve) => setTimeout(() => resolve({ state: FETCH_UNREACHABLE, body: null }), budgetMs)),
|
|
2192
|
+
])
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2152
2195
|
/** raw 文件读取:https 主通道(含镜像)、gh CLI、curl 直连、curl jsDelivr CDN **四通道并行竞速**。
|
|
2153
2196
|
* 黑洞期 https/ghproxy 全挂、gh 可能不在服务进程 PATH——jsDelivr CDN 走系统网络且国内可达,是最后兜底。
|
|
2154
2197
|
* 关键语义:**404 是确定性结果**(文件不存在,探测 .gitmodules/SKILL.md 等时的正常答案),
|
|
2155
|
-
* 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。
|
|
2156
|
-
|
|
2198
|
+
* 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。
|
|
2199
|
+
* 返回值区分「确定性 404」与「超时/通道全灭」——二者都以"没内容"结束,但用户文案必须不同。 */
|
|
2200
|
+
async function rawTextFetch(repo, branch, path, budgetMs = FETCH_BUDGET_MS) {
|
|
2157
2201
|
const url = `${GITHUB_RAW}/${repo}/${encodeURIComponent(branch)}/${path}`
|
|
2158
2202
|
// 404/4xx:确定性"不存在",立即以 null 胜出(不等慢分支)
|
|
2159
2203
|
const notFound = (promise) => promise
|
|
@@ -2161,48 +2205,60 @@ async function rawTextWithFallback(repo, branch, path) {
|
|
|
2161
2205
|
if (error instanceof Error && /HTTP 404|HTTP 400|Not Found|not found|非 2xx|HTTP 非 2xx/u.test(error.message)) return null
|
|
2162
2206
|
throw error
|
|
2163
2207
|
})
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2208
|
+
// 各通道统一口径:状态 2xx **且** body 非空白,才算读到文件(否则抛"非 2xx" → 归一为 null)
|
|
2209
|
+
const real = (promise) => promise.then((body) => {
|
|
2210
|
+
const text = readBodyOrNull(body)
|
|
2211
|
+
if (text === null) throw new Error('HTTP 非 2xx')
|
|
2212
|
+
return text
|
|
2213
|
+
})
|
|
2214
|
+
const https = notFound(real(githubText(url)))
|
|
2169
2215
|
const gh = notFound(githubViaGh(`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`)
|
|
2170
2216
|
.then((data) => {
|
|
2171
2217
|
if (data && typeof data.content === 'string') return Buffer.from(data.content, 'base64').toString('utf8')
|
|
2172
2218
|
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')
|
|
2219
|
+
})
|
|
2220
|
+
.then((text) => {
|
|
2221
|
+
const body = readBodyOrNull(text)
|
|
2222
|
+
if (body === null) throw new Error('contents 无内容')
|
|
2182
2223
|
return body
|
|
2183
2224
|
}))
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
return await Promise.race([
|
|
2188
|
-
Promise.any([https, gh, curl, cdn]),
|
|
2189
|
-
new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
|
|
2190
|
-
])
|
|
2191
|
-
} catch {
|
|
2192
|
-
return null
|
|
2193
|
-
}
|
|
2225
|
+
const curl = notFound(real(curlText(url, 9000, { ipv4: true })))
|
|
2226
|
+
const cdn = notFound(real(curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 9000, { ipv4: true })))
|
|
2227
|
+
return await raceFetchOutcome(Promise.any([https, gh, curl, cdn]), budgetMs)
|
|
2194
2228
|
}
|
|
2195
2229
|
|
|
2196
|
-
|
|
2230
|
+
/** 兼容入口:调用方只关心"有没有内容"(null = 没有)。需要区分超时/404 的场景用 rawTextFetch。 */
|
|
2231
|
+
async function rawTextWithFallback(repo, branch, path) {
|
|
2232
|
+
const { state, body } = await rawTextFetch(repo, branch, path)
|
|
2233
|
+
return state === FETCH_OK ? body : null
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/** 包探测(带失败原因):reason ∈ ok / not-found / unreachable / invalid。导出供测试。
|
|
2237
|
+
* 事故(2026-09-20,另一位用户:Android + proot Ubuntu,容器无 IPv6 路由):
|
|
2238
|
+
* 抓取超时与真 404 都让上层拿到同一个 null,于是「网络太慢」被报成「仓库没有 package.json」。 */
|
|
2239
|
+
async function fetchRepoPackageEx(repo, branch) {
|
|
2240
|
+
const { state, body } = await rawTextFetch(repo, branch, 'package.json')
|
|
2241
|
+
if (state !== FETCH_OK) return { pkg: null, reason: state }
|
|
2197
2242
|
try {
|
|
2198
|
-
const body = await rawTextWithFallback(repo, branch, 'package.json')
|
|
2199
|
-
if (body === null) return null
|
|
2200
2243
|
const pkg = JSON.parse(body)
|
|
2201
|
-
if (pkg
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2244
|
+
if (pkg !== null && typeof pkg === 'object' && typeof pkg.name === 'string') return { pkg, reason: 'ok' }
|
|
2245
|
+
} catch {}
|
|
2246
|
+
return { pkg: null, reason: 'invalid' }
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
async function fetchRepoPackage(repo, branch) {
|
|
2250
|
+
return (await fetchRepoPackageEx(repo, branch)).pkg
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/** 探测失败时的用户可读文案:「抓取超时/不可达」与「真的没有」必须区分开。导出供测试。 */
|
|
2254
|
+
export function packageProbeErrorText(repo, branch, reason) {
|
|
2255
|
+
if (reason === FETCH_UNREACHABLE) {
|
|
2256
|
+
return `抓取超时/网络不可达:没能读到 ${repo}(${branch} 分支)的 package.json —— 通常是网络到 GitHub 太慢(例如解析出 IPv6 却无 IPv6 路由)。请重试;若持续失败,可先用「仓库落地」克隆到本地目录。`
|
|
2205
2257
|
}
|
|
2258
|
+
if (reason === 'invalid') {
|
|
2259
|
+
return `仓库 ${repo} 的 package.json 不是合法的包描述(缺少 name 字段),无法作为插件安装——可改用「仓库落地」克隆到本地目录。`
|
|
2260
|
+
}
|
|
2261
|
+
return `仓库 ${repo} 没有 package.json(也不是技能仓库),无法作为插件安装——可改用「仓库落地」克隆到本地目录。`
|
|
2206
2262
|
}
|
|
2207
2263
|
|
|
2208
2264
|
/**
|
|
@@ -2211,7 +2267,7 @@ async function fetchRepoPackage(repo, branch) {
|
|
|
2211
2267
|
* (曾导致入口链接丢失、DSH 启动崩溃)。registry 走国内镜像。
|
|
2212
2268
|
*/
|
|
2213
2269
|
async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmirror.com', timeout = 90000, signal = null) {
|
|
2214
|
-
const args = ['
|
|
2270
|
+
const args = ['add', spec, '--registry', registry]
|
|
2215
2271
|
const opts = {
|
|
2216
2272
|
cwd: profileDir,
|
|
2217
2273
|
timeout,
|
|
@@ -2225,19 +2281,9 @@ async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmir
|
|
|
2225
2281
|
GCM_INTERACTIVE: 'never',
|
|
2226
2282
|
},
|
|
2227
2283
|
}
|
|
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)
|
|
2284
|
+
// 跨平台定位 corepack/pnpm(Windows 布局 / Linux npm 全局布局 / PATH 兜底);
|
|
2285
|
+
// 旧代码只认 Windows 布局,Linux 上会生成 MODULE_NOT_FOUND 的命令(2026-09-20 事故)
|
|
2286
|
+
await runPnpmWithFallback(args, { execOpts: opts })
|
|
2241
2287
|
}
|
|
2242
2288
|
|
|
2243
2289
|
/**
|
|
@@ -2783,6 +2829,64 @@ function packageEntryExists(dir, pkg) {
|
|
|
2783
2829
|
return candidates.some((c) => existsSync(join(dir, c)))
|
|
2784
2830
|
}
|
|
2785
2831
|
|
|
2832
|
+
/** git 可执行名(跨平台)。事故(2026-09-20,另一位用户:Android + proot Ubuntu):
|
|
2833
|
+
* 「仓库落地」硬编码 `git.exe` → 非 Windows 环境 spawn git.exe ENOENT,克隆必然失败。导出供测试。 */
|
|
2834
|
+
export function gitBin() {
|
|
2835
|
+
return process.platform === 'win32' ? 'git.exe' : 'git'
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
/** pnpm 执行方式定位(跨平台,纯函数便于单测 → 按优先级返回列表,逐个尝试)。导出供测试。
|
|
2839
|
+
* 事故(2026-09-20,同一位用户,node v24 + Linux):只按 Windows 布局找 corepack.js
|
|
2840
|
+
* (`<node bin>/node_modules/corepack/dist/corepack.js`),而 Linux 的 npm 全局布局在
|
|
2841
|
+
* `<prefix>/lib/node_modules/corepack/...` → AI 赋能的 install-npm 生成
|
|
2842
|
+
* `node /usr/local/bin/node_modules/corepack/dist/corepack.js pnpm add …` → MODULE_NOT_FOUND。 */
|
|
2843
|
+
export function resolvePnpmRunners({ platform = process.platform, execPath = process.execPath, comspec = process.env.ComSpec ?? 'cmd.exe', exists = existsSync } = {}) {
|
|
2844
|
+
const binDir = dirname(execPath)
|
|
2845
|
+
const corepackCandidates = [
|
|
2846
|
+
join(binDir, 'node_modules', 'corepack', 'dist', 'corepack.js'), // Windows 官方安装器 / nvm-windows
|
|
2847
|
+
join(binDir, '..', 'lib', 'node_modules', 'corepack', 'dist', 'corepack.js'), // Linux/macOS npm 全局
|
|
2848
|
+
join(binDir, '..', 'libexec', 'lib', 'node_modules', 'corepack', 'dist', 'corepack.js'), // brew / 自编译布局
|
|
2849
|
+
]
|
|
2850
|
+
const runners = []
|
|
2851
|
+
for (const js of corepackCandidates) {
|
|
2852
|
+
if (exists(js)) {
|
|
2853
|
+
runners.push({ kind: 'node-corepack', note: `node ${js} pnpm`, run: (args) => ({ bin: execPath, argv: [js, 'pnpm', ...args] }) })
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
if (platform === 'win32') {
|
|
2857
|
+
// .cmd 批处理不能直接 execFile(EINVAL)→ 经 cmd.exe 调用(整条命令作为一个参数)
|
|
2858
|
+
runners.push({
|
|
2859
|
+
kind: 'cmd-corepack',
|
|
2860
|
+
note: 'cmd /c corepack pnpm',
|
|
2861
|
+
run: (args) => ({ bin: comspec, argv: ['/d', '/s', '/c', ['corepack', 'pnpm', ...args].map((a) => JSON.stringify(a)).join(' ')] }),
|
|
2862
|
+
})
|
|
2863
|
+
} else {
|
|
2864
|
+
runners.push({ kind: 'corepack', note: 'corepack pnpm', run: (args) => ({ bin: 'corepack', argv: ['pnpm', ...args] }) })
|
|
2865
|
+
runners.push({ kind: 'pnpm', note: 'pnpm', run: (args) => ({ bin: 'pnpm', argv: args }) })
|
|
2866
|
+
}
|
|
2867
|
+
return runners
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
/** 依次尝试各执行方式;只有"执行方式本身不可用"(ENOENT / MODULE_NOT_FOUND)才换下一个,
|
|
2871
|
+
* 真正的安装失败(网络、依赖冲突等)立即抛出,并附上已尝试的清单便于排查。 */
|
|
2872
|
+
async function runPnpmWithFallback(args, { execOpts = {}, runners = resolvePnpmRunners() } = {}) {
|
|
2873
|
+
let lastError = null
|
|
2874
|
+
for (const runner of runners) {
|
|
2875
|
+
const { bin, argv } = runner.run(args)
|
|
2876
|
+
try {
|
|
2877
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2878
|
+
await execFileAsync(bin, argv, execOpts)
|
|
2879
|
+
return { runner }
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
lastError = error
|
|
2882
|
+
const message = String(error?.message ?? '')
|
|
2883
|
+
if (!/ENOENT|Cannot find module/u.test(message)) throw error
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const tried = runners.map((r) => r.note).join(' → ')
|
|
2887
|
+
throw new Error(`${lastError?.message ?? 'pnpm 执行失败'}(已尝试:${tried})`)
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2786
2890
|
/** git 非交互环境:禁止任何登录/凭据窗口弹出(私有仓库或不可达源直接失败,不做交互式重试)。 */
|
|
2787
2891
|
function gitEnv() {
|
|
2788
2892
|
return {
|
|
@@ -2865,6 +2969,24 @@ async function installBundleFromRelease(subRepo, pkgName, target) {
|
|
|
2865
2969
|
}
|
|
2866
2970
|
}
|
|
2867
2971
|
|
|
2972
|
+
/** 套装探测(唯一入口):main → master,且**必须内容像 .gitmodules**(含 [submodule "x"] 段)才算套装。
|
|
2973
|
+
* 判据是内容、不是"探测非 null":2026-09-19 用户反馈装 dsh-whale-widget 被判成 submodule 套装置仓库、
|
|
2974
|
+
* clone 后报「未找到 .gitmodules」——根因就是代理/CDN 对**不存在的文件**回 2xx(空 body 也算"读到")。 */
|
|
2975
|
+
async function probeGitmodules(repo) {
|
|
2976
|
+
const main = await rawTextWithFallback(repo, 'main', '.gitmodules')
|
|
2977
|
+
if (looksLikeGitmodules(main)) return main
|
|
2978
|
+
const master = await rawTextWithFallback(repo, 'master', '.gitmodules')
|
|
2979
|
+
return looksLikeGitmodules(master) ? master : null
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
/** 安装类型决策(纯函数,导出供 test-suite-detect.mjs 断言):`.gitmodules` 的**内容**说了算——
|
|
2983
|
+
* 显式 suite 请求若探测内容不像 .gitmodules 也回落普通插件安装(前端标记可能来自 24h 缓存的误判,
|
|
2984
|
+
* 不能当判据);技能请求不受影响。 */
|
|
2985
|
+
export function resolveInstallKind(requestKind, probeText) {
|
|
2986
|
+
if (requestKind === 'skill') return 'skill'
|
|
2987
|
+
return looksLikeGitmodules(probeText) ? 'suite' : 'plugin'
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2868
2990
|
/** 套装安装(submodule 聚合仓库):照仓库 install.ps1/README 语义——
|
|
2869
2991
|
* clone 套装 → 手动镜像拉取子模块 → 按类型装配(bundle 插件含 Release tgz 兜底 / 普通插件 / 技能 / agent 预设)。
|
|
2870
2992
|
* 不执行第三方脚本本体(安全护栏:脚本型只读语义不运行)。 */
|
|
@@ -2876,7 +2998,12 @@ async function runSuiteInstallJob(job, ctx) {
|
|
|
2876
2998
|
mkdirSync(tmpDir, { recursive: true })
|
|
2877
2999
|
await gitCloneRepo(job.repo, tmpDir, job.source)
|
|
2878
3000
|
const subs = readGitmodules(tmpDir)
|
|
2879
|
-
if (subs.length === 0)
|
|
3001
|
+
if (subs.length === 0) {
|
|
3002
|
+
// 探测与仓库实际内容不符(假阳性 / 仓库已重构):**不报失败**,把决定权交回调用方
|
|
3003
|
+
// 回落普通插件安装(npm → Release → git 规格),用户不该因为一次误判装不上插件。
|
|
3004
|
+
job.stage = 'detecting'
|
|
3005
|
+
return { notASuite: true }
|
|
3006
|
+
}
|
|
2880
3007
|
job.stage = 'detecting'
|
|
2881
3008
|
const patchPath = findPatchPath(ctx)
|
|
2882
3009
|
const profileDir = dirname(patchPath)
|
|
@@ -3185,7 +3312,9 @@ async function aiRepair(job, ctx, profileDir, candidates, lastError) {
|
|
|
3185
3312
|
job.error = `本地 AI 修复通道没有可用的子代理提供方:${String(error)};请手动执行:dsh plugin --profile web add <包名>`
|
|
3186
3313
|
return
|
|
3187
3314
|
}
|
|
3188
|
-
|
|
3315
|
+
// 跨平台提示:corepack 的位置随安装方式/系统而异(Windows 官方安装器在 <node bin>/node_modules,
|
|
3316
|
+
// Linux 的 npm 全局在 <prefix>/lib/node_modules),所以给子代理的提示不再写死路径
|
|
3317
|
+
const corepackRunner = resolvePnpmRunners()[0]
|
|
3189
3318
|
const prompt = [
|
|
3190
3319
|
'你是 DeepSeek Harness 的插件安装专家。用户通过插件管理面板安装插件失败,现在由你主动接管安装(不是被动"修复",而是像人工一样把它装好)。',
|
|
3191
3320
|
`目标仓库:${job.repo}`,
|
|
@@ -3194,7 +3323,7 @@ async function aiRepair(job, ctx, profileDir, candidates, lastError) {
|
|
|
3194
3323
|
`此前确定性通道的错误:${lastError ?? '未知'}`,
|
|
3195
3324
|
'接管步骤:',
|
|
3196
3325
|
`1) 先弄清楚装什么:若根包是 private 的聚合仓库(monorepo),用 gh api repos/${job.repo}/git/trees/main?recursive=1 或读 packages 目录下的 package.json 找出子包名;优先选聚合包(名字含 all,如 dsh-web-ui-all,一个包装全部);`,
|
|
3197
|
-
`2) 用 corepack
|
|
3326
|
+
`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
3327
|
'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
3328
|
`4) 安装成功后按官方 dsh plugin add 规则落配置:包声明 dsh.bundle 时把包名追加进 profile 目录 package.json 的 dsh.profile.bundles 数组;普通插件在 cordis.patch.yml 追加 insert 行(id 由包名去 scope、非字母数字转连字符生成,name 填包名);`,
|
|
3200
3329
|
'5) 装完自查:确认包已出现在 node_modules、配置已写入;必要时用 gh api 核对仓库信息;',
|
|
@@ -3773,10 +3902,13 @@ async function runAiStep(job, step, ctx, profileDir, signal) {
|
|
|
3773
3902
|
const pkg = String(step.package ?? '')
|
|
3774
3903
|
if (!/^(@[A-Za-z0-9_\-\.]+\/)?[A-Za-z0-9_\-\.]+(@[A-Za-z0-9_\-\.]+)?$/u.test(pkg)) throw new Error(`包名不合法:${pkg}`)
|
|
3775
3904
|
const dir = resolvePlaceholders(String(step.directory ?? profile), { profileDir: profile })
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3905
|
+
// 跨平台:不再拼 `node <node bin>/node_modules/corepack/dist/corepack.js`(Linux 上必 MODULE_NOT_FOUND,
|
|
3906
|
+
// 2026-09-20 另一位用户的 install-npm 失败现场就是这一行),改为按优先级尝试可用的执行方式
|
|
3907
|
+
const args = ['add', pkg, '--dir', dir, '--registry', 'https://registry.npmmirror.com']
|
|
3908
|
+
const { runner } = await runPnpmWithFallback(args, {
|
|
3909
|
+
execOpts: { timeout: 900000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, signal },
|
|
3910
|
+
})
|
|
3911
|
+
jobLog(job, ` 运行: ${runner.note} ${args.join(' ')}`)
|
|
3780
3912
|
return
|
|
3781
3913
|
}
|
|
3782
3914
|
case 'write-file': {
|
|
@@ -4499,25 +4631,30 @@ async function runInstallJob(job, ctx) {
|
|
|
4499
4631
|
let candidates = [job.packageName].filter((name) => typeof name === 'string' && name !== '')
|
|
4500
4632
|
let subpackageMode = false
|
|
4501
4633
|
if (candidates.length === 0) {
|
|
4502
|
-
// 套装兜底:submodule 聚合仓库(根 .gitmodules
|
|
4503
|
-
// 不依赖前端标记(搜索结果 enrich 是异步的、索引浏览条目无 enrich
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
if (
|
|
4634
|
+
// 套装兜底:submodule 聚合仓库(根 .gitmodules 内容校验通过)→ 自动转套装安装,
|
|
4635
|
+
// 不依赖前端标记(搜索结果 enrich 是异步的、索引浏览条目无 enrich)。
|
|
4636
|
+
// 判据是**内容**(resolveInstallKind),不是"探测非 null":后者会把代理/CDN 对不存在文件回的
|
|
4637
|
+
// 2xx 空 body、垃圾页当成套装(2026-09-19 用户反馈的「未找到 .gitmodules」事故根因)。
|
|
4638
|
+
if (resolveInstallKind(job.kind, await probeGitmodules(job.repo)) === 'suite') {
|
|
4507
4639
|
job.kind = 'suite'
|
|
4508
|
-
await runSuiteInstallJob(job, ctx)
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4640
|
+
const suiteResult = await runSuiteInstallJob(job, ctx)
|
|
4641
|
+
// clone 后才发现没有 .gitmodules(探测假阳性 / 仓库已重构)→ 回落普通安装,不给用户一个失败
|
|
4642
|
+
if (suiteResult?.notASuite !== true) return
|
|
4643
|
+
job.kind = 'plugin'
|
|
4644
|
+
job.suiteNote = '探测到的 .gitmodules 与仓库实际内容不符(不是 submodule 套装仓库),已自动回落普通插件安装'
|
|
4645
|
+
}
|
|
4646
|
+
// 兜底:宿主端自行拉取仓库元数据(githubJson 与 curl 竞速 + 8s 超时降级,黑洞期不卡 40s)。
|
|
4647
|
+
// 8s 而非旧值 3s:IPv6 无路由的环境里单条通道就要 5.4s,3s 预算必输 → branch 恒为 main,
|
|
4648
|
+
// 默认分支为 dev 的仓库会取错分支(2026-09-20 另一位用户实测)。
|
|
4512
4649
|
const meta = await Promise.race([
|
|
4513
4650
|
Promise.any([
|
|
4514
4651
|
githubJson(`${GITHUB_API}/repos/${job.repo}`),
|
|
4515
|
-
curlJson(`${GITHUB_API}/repos/${job.repo}`, 12000),
|
|
4652
|
+
curlJson(`${GITHUB_API}/repos/${job.repo}`, 12000, {}, { ipv4: true }),
|
|
4516
4653
|
]),
|
|
4517
|
-
new Promise((resolve) => setTimeout(() => resolve(null),
|
|
4654
|
+
new Promise((resolve) => setTimeout(() => resolve(null), META_BUDGET_MS)),
|
|
4518
4655
|
]).catch(() => null)
|
|
4519
4656
|
const branch = meta?.default_branch ?? 'main'
|
|
4520
|
-
const pkg = await
|
|
4657
|
+
const { pkg, reason } = await fetchRepoPackageEx(job.repo, branch)
|
|
4521
4658
|
if (pkg === null) {
|
|
4522
4659
|
// 无 package.json:先探测是否技能仓库(含 SKILL.md)→ 自动转技能安装;
|
|
4523
4660
|
// 否则标记 hint=repo-land,前端给出「仓库落地」一键入口(克隆到本地目录)。
|
|
@@ -4529,7 +4666,9 @@ async function runInstallJob(job, ctx) {
|
|
|
4529
4666
|
}
|
|
4530
4667
|
job.status = 'failed'
|
|
4531
4668
|
job.hint = 'repo-land'
|
|
4532
|
-
|
|
4669
|
+
// 文案区分「抓取超时/不可达」与「真的没有」——旧代码两者共用一个出口,报错永远说"文件不存在"
|
|
4670
|
+
job.error = packageProbeErrorText(job.repo, branch, reason)
|
|
4671
|
+
job.probeReason = reason
|
|
4533
4672
|
return
|
|
4534
4673
|
}
|
|
4535
4674
|
if (pkg.private === true) {
|
|
@@ -5492,24 +5631,26 @@ async function handle(ctx, req, res) {
|
|
|
5492
5631
|
if (pathname === `${ROUTE_PREFIX}/repo`) {
|
|
5493
5632
|
const repo = githubRepoInfo(typeof body.repo === 'string' ? body.repo : '')
|
|
5494
5633
|
const auth = readGithubAuth()
|
|
5495
|
-
// meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,
|
|
5634
|
+
// meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,8 秒超时即降级——
|
|
5496
5635
|
// Promise.any 全失败时要等最慢分支(黑洞期 https 41.5s),加 race 超时避免拖累整体。
|
|
5636
|
+
// 8s 而非旧值 3s:IPv6 无路由的环境里单条通道就要 5.4s(2026-09-20 实测),3s 必输 → branch 取错。
|
|
5497
5637
|
let meta = null
|
|
5498
5638
|
try {
|
|
5499
5639
|
meta = await Promise.race([
|
|
5500
5640
|
Promise.any([
|
|
5501
5641
|
githubJson(`${GITHUB_API}/repos/${repo}`, req.signal, auth.token),
|
|
5502
|
-
curlJson(`${GITHUB_API}/repos/${repo}`, 12000),
|
|
5642
|
+
curlJson(`${GITHUB_API}/repos/${repo}`, 12000, {}, { ipv4: true }),
|
|
5503
5643
|
]),
|
|
5504
|
-
new Promise((resolve) => setTimeout(() => resolve(null),
|
|
5644
|
+
new Promise((resolve) => setTimeout(() => resolve(null), META_BUDGET_MS)),
|
|
5505
5645
|
])
|
|
5506
5646
|
} catch {}
|
|
5507
5647
|
const branch = meta?.default_branch ?? 'main'
|
|
5508
5648
|
const pkg = await fetchRepoPackage(repo, branch)
|
|
5509
5649
|
const skill = await detectSkillRepo(repo, branch)
|
|
5510
5650
|
const skillMeta = skill.hasSkill ? await fetchSkillMeta(repo, branch, skill.skillDir) : null
|
|
5511
|
-
// 套装识别:根 .gitmodules
|
|
5512
|
-
|
|
5651
|
+
// 套装识别:根 .gitmodules 存在**且内容真的是 gitmodules**(只判非 null 会被代理/CDN 对不存在
|
|
5652
|
+
// 文件回的 2xx 空 body 骗到,把普通插件标成套装——2026-09-19 用户反馈事故)
|
|
5653
|
+
const hasSuite = looksLikeGitmodules(await rawTextWithFallback(repo, branch, '.gitmodules'))
|
|
5513
5654
|
// 官方安装方式(详情面板展示 + 一键复制,供用户手动安装):
|
|
5514
5655
|
// 套装 → 仓库 install.ps1/README 的官方步骤;普通/聚合 → dsh plugin add 官方命令
|
|
5515
5656
|
let installCommand = null
|
|
@@ -6085,14 +6226,22 @@ async function handle(ctx, req, res) {
|
|
|
6085
6226
|
const name = meta?.moduleName
|
|
6086
6227
|
if (typeof name === 'string' && name !== '' && (name === root || name.startsWith(root + '/'))) bundleRowIds.add(rowId)
|
|
6087
6228
|
}
|
|
6088
|
-
const
|
|
6089
|
-
if ((p.status ?? 'pending') !== 'pending') return false
|
|
6229
|
+
const inFamily = (p) => {
|
|
6090
6230
|
if (typeof p.moduleName === 'string' && p.moduleName !== '' && (p.moduleName === root || p.moduleName.startsWith(root + '/'))) return true
|
|
6091
6231
|
return bundleRowIds.has(p.rowId) || info.get(p.rowId)?.moduleName === root
|
|
6232
|
+
}
|
|
6233
|
+
// 目标 = ① 待适配(pending)② **已记已适配、但从没通过源码扫描**(check !== 'pass')。
|
|
6234
|
+
// ② 这类正是"账面上已适配、补丁里却还禁着"的行 —— 老逻辑只挑 pending,于是用户点「一键启用已适配」
|
|
6235
|
+
// 得到"没有待适配行、无需操作",可它们其实一行都没启用(实测就是这个)。
|
|
6236
|
+
const targets = (pending?.pending ?? []).filter((p) => {
|
|
6237
|
+
if (!inFamily(p)) return false
|
|
6238
|
+
const status = p.status ?? 'pending'
|
|
6239
|
+
if (status === 'pending') return true
|
|
6240
|
+
return status === 'adopted' && p.check !== 'pass'
|
|
6092
6241
|
})
|
|
6093
6242
|
if (targets.length === 0) {
|
|
6094
6243
|
// 无待适配行=全部已适配/已解锁,属正常状态:返回友好提示而非错误(点击「一键启用已适配」不应报"操作失败")
|
|
6095
|
-
sendJson(res, 200, { ok: true, unlocked: [], kept: [], note: '
|
|
6244
|
+
sendJson(res, 200, { ok: true, unlocked: [], kept: [], note: '该全家桶内没有待扫描/待解锁的行(全部已通过源码扫描或已解锁)' })
|
|
6096
6245
|
return
|
|
6097
6246
|
}
|
|
6098
6247
|
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : '?'
|
|
@@ -6125,7 +6274,17 @@ async function handle(ctx, req, res) {
|
|
|
6125
6274
|
unlocked.push(p.rowId)
|
|
6126
6275
|
}
|
|
6127
6276
|
writeCompatPending(pending)
|
|
6128
|
-
|
|
6277
|
+
// kept = 扫描未通过(保持禁用,并带上原因);unlocked = 本次真解锁的行
|
|
6278
|
+
sendJson(res, 200, {
|
|
6279
|
+
ok: true,
|
|
6280
|
+
root,
|
|
6281
|
+
unlocked,
|
|
6282
|
+
kept,
|
|
6283
|
+
scanned: targets.length,
|
|
6284
|
+
note: kept.length === 0
|
|
6285
|
+
? `已重跑源码扫描并解锁 ${unlocked.length} 行`
|
|
6286
|
+
: `扫描 ${targets.length} 行:解锁 ${unlocked.length} 行,${kept.length} 行未通过(保持禁用,原因见下)`,
|
|
6287
|
+
})
|
|
6129
6288
|
return
|
|
6130
6289
|
}
|
|
6131
6290
|
|
|
@@ -7076,9 +7235,9 @@ async function handle(ctx, req, res) {
|
|
|
7076
7235
|
sendError(res, 400, 'deepseek-harness 是 DSH 框架本体,不是插件——无需安装(升级请用官方 dsh 升级方式)')
|
|
7077
7236
|
return
|
|
7078
7237
|
}
|
|
7079
|
-
const
|
|
7080
|
-
if ((
|
|
7081
|
-
sendError(res, 400, `${
|
|
7238
|
+
const requestedKind = body.kind === 'skill' ? 'skill' : body.kind === 'suite' ? 'suite' : 'plugin'
|
|
7239
|
+
if ((requestedKind === 'skill' || requestedKind === 'suite') && repo === '') {
|
|
7240
|
+
sendError(res, 400, `${requestedKind === 'skill' ? '技能' : '套装'}安装必须提供仓库(owner/name)`)
|
|
7082
7241
|
return
|
|
7083
7242
|
}
|
|
7084
7243
|
const source = body.source === 'gitee' ? body.source : 'github'
|
|
@@ -7095,6 +7254,14 @@ async function handle(ctx, req, res) {
|
|
|
7095
7254
|
sendError(res, 409, '该插件正在安装/更新中,请等待当前任务完成后再试')
|
|
7096
7255
|
return
|
|
7097
7256
|
}
|
|
7257
|
+
// 套装请求必须复检内容(2026-09-19 事故):前端标记可能来自 24h enrich 缓存的误判,
|
|
7258
|
+
// 探测内容不像 .gitmodules 就按普通插件安装——别把用户送进注定失败的套装通道。
|
|
7259
|
+
let kind = requestedKind
|
|
7260
|
+
let suiteNote = null
|
|
7261
|
+
if (requestedKind === 'suite') {
|
|
7262
|
+
kind = resolveInstallKind('suite', await probeGitmodules(repo))
|
|
7263
|
+
if (kind !== 'suite') suiteNote = '探测未发现有效的 .gitmodules(不是 submodule 套装仓库),已按普通插件安装'
|
|
7264
|
+
}
|
|
7098
7265
|
const job = {
|
|
7099
7266
|
id: `job-${installJobSeq += 1}`,
|
|
7100
7267
|
repo,
|
|
@@ -7114,10 +7281,20 @@ async function handle(ctx, req, res) {
|
|
|
7114
7281
|
update: body.update === true,
|
|
7115
7282
|
kind,
|
|
7116
7283
|
}
|
|
7284
|
+
if (suiteNote !== null) job.suiteNote = suiteNote
|
|
7117
7285
|
installJobs.set(job.id, job)
|
|
7286
|
+
// 套装通道兜底:探测说有、clone 下来却没有 .gitmodules(假阳性 / 仓库已重构)时
|
|
7287
|
+
// 回落普通插件安装,而不是给用户一个「未找到 .gitmodules」的失败。
|
|
7288
|
+
const runSuiteThenFallback = async () => {
|
|
7289
|
+
const result = await runSuiteInstallJob(job, ctx)
|
|
7290
|
+
if (result?.notASuite !== true) return
|
|
7291
|
+
job.kind = 'plugin'
|
|
7292
|
+
job.suiteNote = '仓库实际内容与探测不符(没有 .gitmodules),已自动回落普通插件安装'
|
|
7293
|
+
await runInstallJob(job, ctx)
|
|
7294
|
+
}
|
|
7118
7295
|
// 后台执行:请求立即返回,安装不受客户端断开/离开面板影响
|
|
7119
|
-
void (kind === 'skill' ? runSkillInstallJob(job) : kind === 'suite' ?
|
|
7120
|
-
sendJson(res, 200, { ok: true, jobId: job.id, status: 'installing', kind })
|
|
7296
|
+
void (kind === 'skill' ? runSkillInstallJob(job) : kind === 'suite' ? runSuiteThenFallback() : runInstallJob(job, ctx))
|
|
7297
|
+
sendJson(res, 200, { ok: true, jobId: job.id, status: 'installing', kind, ...(suiteNote !== null ? { suiteNote } : {}) })
|
|
7121
7298
|
return
|
|
7122
7299
|
}
|
|
7123
7300
|
|
|
@@ -7365,7 +7542,7 @@ async function handle(ctx, req, res) {
|
|
|
7365
7542
|
let usedUrl = ''
|
|
7366
7543
|
for (const url of urls) {
|
|
7367
7544
|
try {
|
|
7368
|
-
await execFileAsync(
|
|
7545
|
+
await execFileAsync(gitBin(), ['clone', '--depth', '1', url, target], { cwd: root, timeout: 600000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, env: gitEnv() })
|
|
7369
7546
|
cloned = true
|
|
7370
7547
|
usedUrl = url
|
|
7371
7548
|
break
|