@noob-stupid/dsh-plugin-console 0.5.45 → 0.5.47
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/lib/client.js +145 -15
- package/lib/server/domain/bundle-refs.js +42 -2
- package/lib/server/domain/install-job.js +53 -52
- package/lib/server/domain/install-verify.js +59 -1
- package/lib/server/domain/install.js +8 -0
- package/lib/server/domain/landed-source.js +79 -0
- package/lib/server/domain/manifest-probe.js +68 -0
- package/lib/server/domain/market.js +117 -24
- package/lib/server/domain/presence-check.js +121 -0
- package/lib/server/domain/probe-outcome.js +58 -0
- package/lib/server/domain/registry-manifest.js +102 -0
- package/lib/server/domain/repo-package-match.js +135 -0
- package/lib/server/domain/repo-url.js +23 -0
- package/lib/server/domain/subdir-detect.js +181 -0
- package/lib/server/domain/subdir-install.js +368 -0
- package/lib/server/domain/suite.js +4 -1
- package/lib/server/infra/egress.js +208 -0
- package/lib/server/infra/http.js +263 -119
- package/lib/server/routes/components.js +262 -10
- package/lib/server/routes/install.js +27 -0
- package/lib/server/routes/state.js +7 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -458,6 +458,25 @@ window.__ModuleLoader__.load({
|
|
|
458
458
|
repoLandBusy: "克隆中…(大仓库可能较慢)",
|
|
459
459
|
repoLandDone: "已落地到:",
|
|
460
460
|
repoLandOpenFolder: "打开文件夹",
|
|
461
|
+
repoOpenDone: "已请求打开:",
|
|
462
|
+
repoOpenFailed: "打开文件夹失败:",
|
|
463
|
+
subpluginFound: "该仓库根目录没有清单,但插件在子目录 {dir}/ 下({pkg},标记:{markers})—— 它不是「不是插件」,只是清单不在仓库根",
|
|
464
|
+
subpluginCopyDir: "复制子目录路径",
|
|
465
|
+
subpluginCopyName: "复制包名",
|
|
466
|
+
// 子目录插件的一键安装(0.5.47 加法):识别出来之后**必须能装**,不能只给一段文字。
|
|
467
|
+
subpluginInstall: "从这个子目录安装",
|
|
468
|
+
subpluginInstallHint: "先按稀疏取源码把该子目录取到本地,再按既有本地目录来源(link:)装进 profile;清单校验与来源校验与其它安装路径同一套,装完照样收口(清单 + lock + 挂载行)",
|
|
469
|
+
// 「报失败前先核实已装」(0.5.47 改错):已装 ⇒ 这句话是主行,历史失败折叠成次要信息。
|
|
470
|
+
alreadyInstalled: "已安装 v{version}(无需重装)",
|
|
471
|
+
alreadyInstalledNoVersion: "已安装(无需重装;版本号读不到)",
|
|
472
|
+
installedBadge: "已安装",
|
|
473
|
+
alreadyInstalledWhere: "命中:{where}",
|
|
474
|
+
whereDependencies: "依赖清单 dependencies",
|
|
475
|
+
whereBundles: "dsh.profile.bundles",
|
|
476
|
+
whereNodeModules: "node_modules",
|
|
477
|
+
failureSecondary: "历史失败(次要信息,仅供排查;已装上的插件无需重装)",
|
|
478
|
+
failureSecondaryShow: "显示历史失败",
|
|
479
|
+
failureSecondaryHide: "收起历史失败",
|
|
461
480
|
repoLandExists: "该仓库已存在,无需重复落地",
|
|
462
481
|
repoLandDirLabel: "保存路径",
|
|
463
482
|
repoLandSaveDir: "保存路径",
|
|
@@ -1018,6 +1037,25 @@ window.__ModuleLoader__.load({
|
|
|
1018
1037
|
repoLandBusy: "Cloning… (large repos may take a while)",
|
|
1019
1038
|
repoLandDone: "Landed at: ",
|
|
1020
1039
|
repoLandOpenFolder: "Open folder",
|
|
1040
|
+
repoOpenDone: "Requested to open: ",
|
|
1041
|
+
repoOpenFailed: "Failed to open folder: ",
|
|
1042
|
+
subpluginFound: "This repository has no manifest at its root, but the plugin lives in the subdirectory {dir}/ ({pkg}, markers: {markers}) — it is not \"not a plugin\", the manifest is just not at the repository root",
|
|
1043
|
+
subpluginCopyDir: "Copy subdirectory path",
|
|
1044
|
+
subpluginCopyName: "Copy package name",
|
|
1045
|
+
// One-click install from a subdirectory (0.5.47): recognition must come with a way to install.
|
|
1046
|
+
subpluginInstall: "Install from this subdirectory",
|
|
1047
|
+
subpluginInstallHint: "Fetches the subdirectory source (sparse) into a local folder, then installs it through the existing local-directory source (link:). Manifest and source validation are the same as for any other install path, and finalization (manifest + lock + mount row) still applies",
|
|
1048
|
+
// "Verify it is actually installed before reporting failure" (0.5.47): this line is primary when it is.
|
|
1049
|
+
alreadyInstalled: "Already installed v{version} (no reinstall needed)",
|
|
1050
|
+
alreadyInstalledNoVersion: "Already installed (no reinstall needed; version unreadable)",
|
|
1051
|
+
installedBadge: "Installed",
|
|
1052
|
+
alreadyInstalledWhere: "Found in: {where}",
|
|
1053
|
+
whereDependencies: "manifest dependencies",
|
|
1054
|
+
whereBundles: "dsh.profile.bundles",
|
|
1055
|
+
whereNodeModules: "node_modules",
|
|
1056
|
+
failureSecondary: "Earlier failure (secondary, for diagnosis only; an installed plugin needs no reinstall)",
|
|
1057
|
+
failureSecondaryShow: "Show earlier failure",
|
|
1058
|
+
failureSecondaryHide: "Hide earlier failure",
|
|
1021
1059
|
repoLandExists: "Already exists — nothing to clone",
|
|
1022
1060
|
repoLandDirLabel: "Save path",
|
|
1023
1061
|
repoLandSaveDir: "Save path",
|
|
@@ -2229,7 +2267,10 @@ window.__ModuleLoader__.load({
|
|
|
2229
2267
|
if (!window.confirm(t("repoLandRemoveConfirm").replace("{repo}", item.repo))) return;
|
|
2230
2268
|
try {
|
|
2231
2269
|
await call("/plugin-console/repo-remove", { path: item.path });
|
|
2232
|
-
} catch {
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
// 2026-10-13 改错:删不掉时旧代码 `catch {}` 静默吞掉,用户看到的是"点了没反应、仓库还在"。
|
|
2272
|
+
setMessage(t("safetySwitchFailed") + ":" + (error && typeof error.message === "string" && error.message !== "" ? error.message : t("safetySwitchFailed")));
|
|
2273
|
+
}
|
|
2233
2274
|
repoLandRefresh();
|
|
2234
2275
|
}
|
|
2235
2276
|
async function repoLandStart() {
|
|
@@ -2245,9 +2286,22 @@ window.__ModuleLoader__.load({
|
|
|
2245
2286
|
}
|
|
2246
2287
|
setRepoLandBusy(false);
|
|
2247
2288
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2289
|
+
/** 打开仓库落地目录(2026-10-13 改错:**成功与失败都必须有结果**)。
|
|
2290
|
+
* 旧实现 `try { await call(...) } catch {}` —— 非 2xx(路径被守卫拒绝 / 目录不存在 /
|
|
2291
|
+
* 文件管理器起不来)全被吞掉,用户只看到"点了没反应"(真机实测报告)。
|
|
2292
|
+
* 现在:服务端回结构化状态 → 成功显示"已在文件管理器中打开:<path>"(服务端的 note 也带上),
|
|
2293
|
+
* 失败把**原因原样**显示(守卫拒绝的原因、目录不存在、spawn 失败的系统原话)。
|
|
2294
|
+
* 供「刚落地」按钮与「已落地列表」每行的按钮共用(两处行为必须一致,别再各写一遍)。 */
|
|
2295
|
+
async function repoLandOpenFolder(target) {
|
|
2296
|
+
const path = typeof target === "string" && target !== "" ? target : (repoLandResult && repoLandResult.path);
|
|
2297
|
+
if (!path) return;
|
|
2298
|
+
try {
|
|
2299
|
+
const data = await call("/plugin-console/repo-open", { path });
|
|
2300
|
+
const note = data && typeof data.note === "string" && data.note !== "" ? "(" + data.note + ")" : "";
|
|
2301
|
+
setMessage(t("repoOpenDone") + (data && data.path ? data.path : path) + note);
|
|
2302
|
+
} catch (error) {
|
|
2303
|
+
setMessage(t("repoOpenFailed") + (error && typeof error.message === "string" && error.message !== "" ? error.message : t("safetySwitchFailed")));
|
|
2304
|
+
}
|
|
2251
2305
|
}
|
|
2252
2306
|
const sectionRef = react.useRef(null);
|
|
2253
2307
|
const [compCardTop, setCompCardTop] = react.useState(null);
|
|
@@ -2407,6 +2461,8 @@ window.__ModuleLoader__.load({
|
|
|
2407
2461
|
}, [state.status]);
|
|
2408
2462
|
// 已关闭的失败提示(用户点 × 后不再显示,服务重启后重置)
|
|
2409
2463
|
const [dismissedFailures, setDismissedFailures] = react.useState({});
|
|
2464
|
+
// 「已装上的那次失败」里,历史失败正文是否展开(0.5.47 加法:默认收起 —— 次要信息不许喧宾夺主)
|
|
2465
|
+
const [failureDetailOpen, setFailureDetailOpen] = react.useState({});
|
|
2410
2466
|
// 软件源管理
|
|
2411
2467
|
const [sourcesOpen, setSourcesOpen] = react.useState(false);
|
|
2412
2468
|
const [sourcesData, setSourcesData] = react.useState(null);
|
|
@@ -3647,7 +3703,9 @@ window.__ModuleLoader__.load({
|
|
|
3647
3703
|
() => { setSearchSourceName(""); setSearchSourceUrl(""); setSearchSourceHeaders(""); },
|
|
3648
3704
|
);
|
|
3649
3705
|
};
|
|
3650
|
-
|
|
3706
|
+
// subdir 用于「从这个子目录安装」(0.5.47 加法):服务端按它去读 <subdir>/package.json、
|
|
3707
|
+
// 稀疏取该子目录源码、再按既有本地目录来源(link:)装。为空时请求体形状与改动前逐字相同。
|
|
3708
|
+
const startJob = (repo, packageName, source = "github", kind = "plugin", update = false, subdir = null) => {
|
|
3651
3709
|
// 防并发(与 addLocal 一致):已有安装/更新进行中时忽略新点击(更新按钮此前可绕过,
|
|
3652
3710
|
// 反复点击会产生几十个并发任务装同一插件)
|
|
3653
3711
|
if (installing !== null) {
|
|
@@ -3658,7 +3716,9 @@ window.__ModuleLoader__.load({
|
|
|
3658
3716
|
setReloadHint(false);
|
|
3659
3717
|
setRestartHint(false);
|
|
3660
3718
|
setMessage(null);
|
|
3661
|
-
|
|
3719
|
+
const body = { repo: repo ?? "", packageName, source, kind, update };
|
|
3720
|
+
if (subdir !== null && subdir !== undefined && String(subdir) !== "") body.subdir = String(subdir);
|
|
3721
|
+
call("/plugin-console/install", body).then(
|
|
3662
3722
|
(data) => {
|
|
3663
3723
|
if (data && data.jobId) {
|
|
3664
3724
|
setJobs((prev) => ({
|
|
@@ -4804,7 +4864,7 @@ window.__ModuleLoader__.load({
|
|
|
4804
4864
|
? (repoLandResult.ok === true
|
|
4805
4865
|
? el("div", { className: styles.rowTop },
|
|
4806
4866
|
el("p", { className: styles.message, style: { flex: 1, minWidth: 0, wordBreak: "break-all" } }, t("repoLandDone") + repoLandResult.path),
|
|
4807
|
-
el("button", { type: "button", className: styles.toggle, onClick: repoLandOpenFolder }, t("repoLandOpenFolder")))
|
|
4867
|
+
el("button", { type: "button", className: styles.toggle, onClick: () => repoLandOpenFolder(repoLandResult.path) }, t("repoLandOpenFolder")))
|
|
4808
4868
|
: el("p", { className: styles.message, "data-error": "true" }, t("safetySwitchFailed") + ":" + (repoLandResult.error ?? "?")))
|
|
4809
4869
|
: null,
|
|
4810
4870
|
el("strong", { className: styles.name }, t("repoLandList") + "(" + repoLanded.length + ")"),
|
|
@@ -4814,7 +4874,7 @@ window.__ModuleLoader__.load({
|
|
|
4814
4874
|
repoLanded.map((item) =>
|
|
4815
4875
|
el("div", { key: item.repo, className: styles.rowTop },
|
|
4816
4876
|
el("strong", { className: styles.name, style: { flex: "1 1 auto" } }, item.repo),
|
|
4817
|
-
el("button", { type: "button", className: styles.toggle, onClick: () =>
|
|
4877
|
+
el("button", { type: "button", className: styles.toggle, onClick: () => repoLandOpenFolder(item.path) }, t("repoLandOpenFolder")),
|
|
4818
4878
|
el("button", { type: "button", className: styles.trashBtn, title: t("repoLandRemoveConfirm"), "aria-label": t("repoLandRemoveConfirm"), onClick: () => repoLandRemove(item) }, "✕"))))
|
|
4819
4879
|
)))
|
|
4820
4880
|
: null,
|
|
@@ -5392,10 +5452,33 @@ window.__ModuleLoader__.load({
|
|
|
5392
5452
|
? el("span", { className: styles.tag, "data-user": "true" }, t("suiteComponentOk"))
|
|
5393
5453
|
: el("span", { className: styles.tag, "data-error": "true" }, t("suiteComponentFail"))),
|
|
5394
5454
|
r.note ? el("p", { className: styles.desc }, r.note) : null))))),
|
|
5395
|
-
(state.status === "ready" ? state.data.recentFailures ?? [] : []).filter((job) => !dismissedFailures[job.jobId]).map((job) =>
|
|
5396
|
-
|
|
5455
|
+
(state.status === "ready" ? state.data.recentFailures ?? [] : []).filter((job) => !dismissedFailures[job.jobId]).map((job) => {
|
|
5456
|
+
// ★ 0.5.47 改错(用户被误导过):服务端在下发**之前**已经查过三处(依赖清单 /
|
|
5457
|
+
// dsh.profile.bundles / node_modules)。已经装上时,「已安装 vX(无需重装)」是**主行**,
|
|
5458
|
+
// 历史失败降级成次要信息(默认收起)—— 绝不让用户以为没装上、再点一次重试。
|
|
5459
|
+
const evidence = job.installedEvidence !== null && job.installedEvidence !== undefined && job.installedEvidence.installed === true
|
|
5460
|
+
? job.installedEvidence
|
|
5461
|
+
: null;
|
|
5462
|
+
const whereText = evidence === null
|
|
5463
|
+
? ""
|
|
5464
|
+
: (Array.isArray(evidence.where) ? evidence.where : []).map((w) => w === "dependencies" ? t("whereDependencies") : w === "bundles" ? t("whereBundles") : w === "node_modules" ? t("whereNodeModules") : String(w)).join(" / ");
|
|
5465
|
+
const installedLine = evidence === null
|
|
5466
|
+
? null
|
|
5467
|
+
: (evidence.version === null || evidence.version === undefined
|
|
5468
|
+
? t("alreadyInstalledNoVersion")
|
|
5469
|
+
: t("alreadyInstalled").replace("{version}", String(evidence.version)))
|
|
5470
|
+
+ (whereText === "" ? "" : " · " + t("alreadyInstalledWhere").replace("{where}", whereText));
|
|
5471
|
+
const failureOpen = failureDetailOpen[job.jobId] === true;
|
|
5472
|
+
// 子目录插件的一键安装(0.5.47 加法):服务端给了结构化的可执行计划才渲染按钮
|
|
5473
|
+
// (`install.action === "install-subdir"`)—— 只在文案里写"可以装"不算数。
|
|
5474
|
+
const subInstall = job.subplugin !== null && job.subplugin !== undefined
|
|
5475
|
+
&& job.subplugin.install !== null && job.subplugin.install !== undefined
|
|
5476
|
+
&& job.subplugin.install.action === "install-subdir"
|
|
5477
|
+
? job.subplugin.install
|
|
5478
|
+
: null;
|
|
5479
|
+
return el("div", { key: "fail-" + job.jobId, className: styles.detail },
|
|
5397
5480
|
el("div", { className: styles.rowTop },
|
|
5398
|
-
el("span", { className: styles.phase, "data-phase": "failed" }, t("recentFailures")),
|
|
5481
|
+
el("span", { className: styles.phase, "data-phase": evidence === null ? "failed" : "active" }, evidence === null ? t("recentFailures") : t("installedBadge")),
|
|
5399
5482
|
el("strong", { className: styles.name }, job.packageName ?? job.repo),
|
|
5400
5483
|
el("button", {
|
|
5401
5484
|
type: "button",
|
|
@@ -5404,14 +5487,61 @@ window.__ModuleLoader__.load({
|
|
|
5404
5487
|
"aria-label": t("dismissFailure"),
|
|
5405
5488
|
onClick: () => setDismissedFailures((prev) => ({ ...prev, [job.jobId]: true })),
|
|
5406
5489
|
}, "✕")),
|
|
5407
|
-
|
|
5490
|
+
installedLine === null
|
|
5491
|
+
? el("p", { className: styles.message, "data-error": "true" }, job.error ?? "")
|
|
5492
|
+
: el("p", { className: styles.message, "data-warn": "true" }, installedLine),
|
|
5493
|
+
// 子目录插件(2026-10-13 加法,真机 Fei2411/DeepSeek-Bot):服务端已判定"插件在
|
|
5494
|
+
// <子目录>/ 下",这里把结论**单独显示**(不用用户从一整段报错里找路径),
|
|
5495
|
+
// 0.5.47 起再给**可点的一步**(不只是可复制的路径)。用 styles.message、不加新 class。
|
|
5496
|
+
job.subplugin !== null && job.subplugin !== undefined
|
|
5497
|
+
? el("p", { className: styles.message, "data-warn": "true" },
|
|
5498
|
+
t("subpluginFound")
|
|
5499
|
+
.replace("{dir}", String(job.subplugin.dir ?? "?"))
|
|
5500
|
+
.replace("{pkg}", String(job.subplugin.packageName ?? "?"))
|
|
5501
|
+
.replace("{markers}", Array.isArray(job.subplugin.markers) ? job.subplugin.markers.join(" / ") : "?"))
|
|
5502
|
+
: null,
|
|
5503
|
+
subInstall !== null
|
|
5504
|
+
? el("div", { className: styles.rowTop },
|
|
5505
|
+
el("button", {
|
|
5506
|
+
type: "button",
|
|
5507
|
+
className: styles.toggle,
|
|
5508
|
+
title: t("subpluginInstallHint"),
|
|
5509
|
+
onClick: () => startJob(job.repo, subInstall.packageName ?? job.packageName ?? null, job.source ?? "github", "plugin", false, subInstall.dir),
|
|
5510
|
+
}, t("subpluginInstall") + " · " + String(subInstall.dir ?? "") + "/"))
|
|
5511
|
+
: null,
|
|
5512
|
+
job.subplugin !== null && job.subplugin !== undefined && typeof job.subplugin.dir === "string" && job.subplugin.dir !== ""
|
|
5513
|
+
? el("div", { className: styles.rowTop },
|
|
5514
|
+
el("button", { type: "button", className: styles.toggle, onClick: () => copyInstallCommand(String(job.subplugin.dir)) }, t("subpluginCopyDir")),
|
|
5515
|
+
typeof job.subplugin.packageName === "string" && job.subplugin.packageName !== ""
|
|
5516
|
+
? el("button", { type: "button", className: styles.toggle, onClick: () => copyInstallCommand(String(job.subplugin.packageName)) }, t("subpluginCopyName"))
|
|
5517
|
+
: null)
|
|
5518
|
+
: null,
|
|
5408
5519
|
...diagLines(job).map(diagLineEl),
|
|
5409
5520
|
el("div", { className: styles.rowTop },
|
|
5410
5521
|
el("button", { type: "button", className: styles.toggle, onClick: () => startJob(job.repo, job.packageName, job.source ?? "github") }, t("retry")),
|
|
5411
|
-
|
|
5522
|
+
// 历史失败(0.5.47):已装上的插件这张卡里,失败正文与"重试"降级进可折叠的次要区
|
|
5523
|
+
// —— 默认收起,用户不会被一句红色失败误导成"没装上"。
|
|
5524
|
+
evidence !== null
|
|
5525
|
+
? el("button", {
|
|
5526
|
+
type: "button",
|
|
5527
|
+
className: styles.toggle,
|
|
5528
|
+
onClick: () => setFailureDetailOpen((prev) => ({ ...prev, [job.jobId]: failureOpen !== true })),
|
|
5529
|
+
}, failureOpen ? t("failureSecondaryHide") : t("failureSecondaryShow"))
|
|
5530
|
+
: null,
|
|
5531
|
+
// 「仓库落地」一键克隆:hint=repo-land(旧结论)与 hint=subdir-plugin(插件在子目录里)
|
|
5532
|
+
// 都要给 —— 后者的文案本身就写着"先按「仓库落地」克隆到本地查看该子目录",
|
|
5533
|
+
// 按钮若只在 repo-land 时渲染,那句话就指向一个不存在的入口。
|
|
5534
|
+
job.hint === "repo-land" || job.hint === "subdir-plugin"
|
|
5412
5535
|
? el("button", { type: "button", className: styles.toggle, title: repoLandTitleText(), onClick: () => { setRepoLandOpen(true); repoLandEntry(job.repo); } }, t("repoLandEntry"))
|
|
5413
|
-
: null)
|
|
5414
|
-
|
|
5536
|
+
: null),
|
|
5537
|
+
// 次要信息区(默认收起):历史失败正文 + 服务端那句核实note + 诊断行,只在用户展开时显示
|
|
5538
|
+
evidence !== null && failureOpen
|
|
5539
|
+
? el("div", { className: styles.detail },
|
|
5540
|
+
el("p", { className: styles.note }, t("failureSecondary")),
|
|
5541
|
+
el("p", { className: styles.message, "data-error": "true" }, job.error ?? ""),
|
|
5542
|
+
typeof evidence.note === "string" && evidence.note !== "" ? el("p", { className: styles.note }, evidence.note) : null)
|
|
5543
|
+
: null)
|
|
5544
|
+
}),
|
|
5415
5545
|
repoInfo !== null
|
|
5416
5546
|
? el("aside", { className: styles.floatPanel },
|
|
5417
5547
|
el("div", { className: styles.descTopbar },
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
// 注册前先按本模块读出的引用逐个 `resolvePackageJson` 核实,缺失即拒绝注册。
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync } from 'node:fs'
|
|
12
|
-
import {
|
|
12
|
+
import { readFile } from 'node:fs/promises'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { DEFAULT_BUNDLES } from './install.js'
|
|
15
|
+
import { resolvePackageJson } from '../infra/paths.js'
|
|
13
16
|
|
|
14
17
|
/** 读取聚合包 `cordis.patch.yml` 的 insert 行 name 列表(注册前校验用)。
|
|
15
18
|
* 解析形状与 install.js#ensureBundlePatchIntegrity 同构:`- insert:` 块下的
|
|
@@ -45,4 +48,41 @@ function readBundlePatchRefNames(profileDir, pkgName) {
|
|
|
45
48
|
} catch { return [] }
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
/** 读取用户额外 bundle(非官方模板)的补丁插入行 id 与包名,用于"额外插件"判定。
|
|
52
|
+
* 2026-10-13 从 install-job.js 原样搬出(只搬移未改逻辑;那边贴着 600 行硬顶,
|
|
53
|
+
* 而这块与上面的 insert 行解析是同一件事),调用方经 install-job.js 的 re-export 取用,
|
|
54
|
+
* 对外导出名与行为一字未变(tests/test-architecture-guard 的接线断言要求导出名仍在)。 */
|
|
55
|
+
async function readExtraBundleRows(profileDir) {
|
|
56
|
+
const rows = new Set()
|
|
57
|
+
try {
|
|
58
|
+
const manifest = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8'))
|
|
59
|
+
const bundles = manifest.dsh?.profile?.bundles ?? []
|
|
60
|
+
for (const pkg of bundles) {
|
|
61
|
+
if (DEFAULT_BUNDLES.includes(pkg)) continue
|
|
62
|
+
try {
|
|
63
|
+
const pk = resolvePackageJson(pkg, profileDir)
|
|
64
|
+
const dir = dirname(pk ?? join(profileDir, 'node_modules', ...String(pkg).split('/'), 'package.json'))
|
|
65
|
+
const text = await readFile(join(dir, 'cordis.patch.yml'), 'utf8')
|
|
66
|
+
const lines = text.split(/\r?\n/u)
|
|
67
|
+
let inInsert = false
|
|
68
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
69
|
+
const line = lines[index]
|
|
70
|
+
if (/^- insert:\s*$/u.test(line)) {
|
|
71
|
+
inInsert = true
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (/^- /u.test(line)) inInsert = false
|
|
75
|
+
if (!inInsert) continue
|
|
76
|
+
const idMatch = line.match(/^ {4}- id: ([A-Za-z0-9_.-]+)\s*$/u)
|
|
77
|
+
if (!idMatch) continue
|
|
78
|
+
rows.add(idMatch[1])
|
|
79
|
+
const nameMatch = (lines[index + 1] ?? '').match(/^ {6}name: ['"]([^'"]+)['"]\s*$/u)
|
|
80
|
+
if (nameMatch) rows.add(nameMatch[1])
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
return rows
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export { readBundlePatchRefNames, readExtraBundleRows }
|
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
// 分层 Step 8c-2 从 lib/index.js 搬出;形参由 ctx 收窄为 ports(调用方注入的窄接口),只搬移未改逻辑。
|
|
3
3
|
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
4
4
|
|
|
5
|
-
import { readFile } from 'node:fs/promises'
|
|
6
5
|
import { dirname, join, resolve } from 'node:path'
|
|
7
6
|
import { aiRepair } from './ai-run.js'
|
|
8
7
|
import { maybeAutoAdaptCompat } from './compat.js'
|
|
9
8
|
import { getReposDir } from './repoland.js'
|
|
10
9
|
import { aiConsentFailureText, cleanupAttemptedCandidates, pnpmRemove } from './install-cleanup.js'
|
|
11
10
|
import { GIT_CHANNEL_BUDGET_MS, GIT_MIN_ATTEMPT_MS, GIT_SPEC_TIMEOUT_MS, gitChannelBudgetMs, noteChannel, formatBudgetMs, tryGitChannel } from './git-channel.js'
|
|
12
|
-
import {
|
|
13
|
-
import { readBundlePatchRefNames } from './bundle-refs.js' // 0.5.
|
|
11
|
+
import { AI_CONSENT_TIMEOUT_MS, PNPM_INSTALL_TIMEOUT_MS, addBundleToManifest, backfillMissingDeps, curlManualInstall, detectBundleOnly, ensureBundlePatchIntegrity, githubReleaseInstall, pnpmInstall, raceInstallChannels, readGithubAuth, syncAggregateSubpackageVersions } from './install.js'
|
|
12
|
+
import { readBundlePatchRefNames, readExtraBundleRows } from './bundle-refs.js' // re-export(0.5.46 搬入;0.5.26 已搬 readBundlePatchRefNames)
|
|
14
13
|
import { classifyInstallFailure, retryPnpmOnce } from './install-diagnose.js'
|
|
15
14
|
import { jobLog } from './jobs.js'
|
|
16
15
|
import { gatePrivateRoot } from './repo-size.js'
|
|
17
16
|
import { tryPresetSourceChannel } from './preset-source.js'
|
|
18
17
|
import { settlePresetOutcome } from './preset-in-package.js'
|
|
19
18
|
import { alreadyInstalledTarget, verifyInstalledEntry } from './install-verify.js'
|
|
20
|
-
import { applyNpmNameHint, expandSubpackageCandidates, fetchRepoPackage, fetchRepoPackageEx, fetchSubpackageNames, npmNameHintForRepo, packageProbeErrorText, subpackageCandidates } from './market.js'
|
|
19
|
+
import { applyNpmNameHint, composeProbeFailureText, expandSubpackageCandidates, fetchRepoPackage, fetchRepoPackageEx, fetchSubdirPlugins, fetchSubpackageNames, npmNameHintForRepo, packageProbeErrorText, subpackageCandidates } from './market.js'
|
|
20
|
+
import { decideNoRootManifest } from './subdir-detect.js'
|
|
21
|
+
import { runSubdirInstallJob } from './subdir-install.js'
|
|
22
|
+
import { bindMarketDeps, probeRootManifest } from './manifest-probe.js' // 0.5.47 接线:抓清单的额外通道 + 通道身份
|
|
21
23
|
import { finalizeInstallPersistence } from './install-finalize.js'
|
|
22
24
|
import { readPatchState, syncNameFromNote } from './patch.js'
|
|
23
25
|
import { listEntries } from './runtime.js'
|
|
@@ -42,11 +44,9 @@ const RELEASE_CHANNEL_BUDGET = 3
|
|
|
42
44
|
* 否则替身与 cordis 语义不一致,这类缺陷还会再溜过去一次。 */
|
|
43
45
|
function channelImpls(ports) {
|
|
44
46
|
const real = { pnpmInstall, curlManualInstall, raceInstallChannels, githubReleaseInstall, backfillMissingDeps }
|
|
45
|
-
// 生产路径上 ports 就是 cordis 的 ctx 代理,属性式读取未 inject
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// ports.get('subagents') / ports.get('agents') 一致);普通对象(测试替身/窄接口)才回退属性访问。
|
|
49
|
-
// try/catch 保留 0.3.61 的兜底语义:万一还有别的 ctx 形状,宁可回落真实实现也不要炸掉安装。
|
|
47
|
+
// 生产路径上 ports 就是 cordis 的 ctx 代理,属性式读取未 inject 的名字会**同步抛**(0.3.59 每次安装
|
|
48
|
+
// 都在这里失败)⇒ 正规写法是 ctx.get('installChannels')(可选读取,未声明也不抛);普通对象(测试
|
|
49
|
+
// 替身/窄接口)才回退属性访问。try/catch 保留 0.3.61 的兜底语义:宁可回落真实实现也别炸掉安装。
|
|
50
50
|
let override = null
|
|
51
51
|
try {
|
|
52
52
|
override = typeof ports?.get === 'function' ? (ports.get('installChannels') ?? null) : (ports?.installChannels ?? null)
|
|
@@ -75,9 +75,8 @@ async function tryCandidateChannels({ job, ch, name, profileDir, registries, rep
|
|
|
75
75
|
const patchPath = join(profileDir, 'cordis.patch.yml')
|
|
76
76
|
let installedName = null
|
|
77
77
|
let lastError = null
|
|
78
|
-
// 0.5.41 接线:这次成功装上的**来源**(`{ spec, origin, kind }`)—— 取样通道(curl tarball /
|
|
79
|
-
//
|
|
80
|
-
// registry 通道不填(既有流程一字不改);git 通道也不填(它本来就是 pnpm 装的,清单里已是 pnpm 写的规格)。
|
|
78
|
+
// 0.5.41 接线:这次成功装上的**来源**(`{ spec, origin, kind }`)—— 取样通道(curl tarball / GitHub
|
|
79
|
+
// release 资产)知道真实下载地址,收口那一步据此走 `pnpm add <spec>` 真装;registry/git 通道都不填。
|
|
81
80
|
let source = null
|
|
82
81
|
// 加法优化:包已在 node_modules 且名字匹配时,不再重复安装/触发 EPERM,直接进入启用流程
|
|
83
82
|
// (2026-09-27 搬进 install-verify.js#alreadyInstalledTarget:本文件贴着 600 行架构硬顶)
|
|
@@ -222,8 +221,12 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
222
221
|
fetchRepoPackage,
|
|
223
222
|
fetchRepoPackageEx,
|
|
224
223
|
fetchSubpackageNames,
|
|
224
|
+
fetchSubdirPlugins,
|
|
225
225
|
subpackageCandidates,
|
|
226
226
|
npmNameHint: npmNameHintForRepo,
|
|
227
|
+
// 技能仓库探测 + 默认分支探测也进袋子(2026-10-13,**测试安全**):这两处原本是直接调用,离线测试
|
|
228
|
+
// 拦不住(本机实测会真的连 api.github.com/ghproxy/ghfast)。生产默认值与改动前逐字相同。
|
|
229
|
+
detectSkillRepo, fetchRepoMeta,
|
|
227
230
|
...(deps.marketProbes ?? {}),
|
|
228
231
|
}
|
|
229
232
|
// 展开实现必须跟随**注入的** fetchSubpackageNames(否则注入的探测桩会被绕过、悄悄去打真网络 ——
|
|
@@ -231,10 +234,22 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
231
234
|
if (deps.marketProbes?.expandSubpackages === undefined) {
|
|
232
235
|
probes.expandSubpackages = (opts = {}) => expandSubpackageCandidates({ ...opts, deps: { fetchSubpackageNames: probes.fetchSubpackageNames } })
|
|
233
236
|
}
|
|
237
|
+
// ★ 0.5.47:额外通道的 deps **绑在袋子上**(套装判定先探一次 2 参调用并缓存,只在调用点传 deps 会被绕过)
|
|
238
|
+
bindMarketDeps(probes, job, deps)
|
|
234
239
|
const jobBudgetMs = Number.isFinite(deps.jobBudgetMs) && deps.jobBudgetMs > 0 ? deps.jobBudgetMs : 8 * 60 * 1000
|
|
235
240
|
// 源码通道(体积门禁 + 稀疏取源码 + 预设装配)的注入缝:与 marketProbes 同一风格,生产调用方不传。
|
|
236
241
|
const sourceDeps = deps.sourceDeps ?? {}
|
|
237
242
|
const aiConsentTimeoutMs = Number.isFinite(deps.aiConsentTimeoutMs) && deps.aiConsentTimeoutMs > 0 ? deps.aiConsentTimeoutMs : AI_CONSENT_TIMEOUT_MS
|
|
243
|
+
// ★ 子目录插件(2026-10-13 加法,第二轮):用户点了「从这个子目录安装」⇒ 走专门的子目录通道
|
|
244
|
+
// (先落地该子目录,再按既有**本地目录来源** link: 装;判据与理由见 domain/subdir-install.js)。
|
|
245
|
+
// 位置刻意放在**任何仓库根探测之前**:这条路本来就不该按根包去探(真机 Fei2411/DeepSeek-Bot
|
|
246
|
+
// 根目录根本没有清单),否则会白跑一轮探测、还可能把结论带偏。
|
|
247
|
+
// `channels` 沿用本文件唯一那个通道注入缝(channelImpls),不在这里另立一个说法。
|
|
248
|
+
if (typeof job.subdir === 'string' && job.subdir.trim() !== '') {
|
|
249
|
+
await runSubdirInstallJob(job, ports, { ...deps, channels: channelImpls(ports) })
|
|
250
|
+
job.finishedAt = Date.now()
|
|
251
|
+
return
|
|
252
|
+
}
|
|
238
253
|
try {
|
|
239
254
|
job.stage = 'preparing'
|
|
240
255
|
// 2026-09-06 事故(室友机器):对 deepseek-ai/deepseek-harness(框架本体仓库)点安装会按 bundle 规则
|
|
@@ -283,21 +298,35 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
283
298
|
const meta = job.repoMeta !== undefined ? job.repoMeta : await fetchRepoMeta(job.repo)
|
|
284
299
|
const branch = meta?.default_branch ?? 'main'
|
|
285
300
|
job.defaultBranch = branch // 批次 B-⑥:后续读子包一律以它打头(默认分支是 dev 的仓库不再读不到子包)
|
|
286
|
-
const rootProbe = job
|
|
301
|
+
const rootProbe = await probeRootManifest({ job, probes, noteChannel, branch }) // 0.5.47 接线:额外通道 + 通道身份
|
|
287
302
|
const { pkg, reason } = rootProbe
|
|
288
303
|
if (pkg === null) {
|
|
289
304
|
// 无 package.json:先探测是否技能仓库(含 SKILL.md)→ 自动转技能安装;
|
|
290
|
-
//
|
|
291
|
-
|
|
305
|
+
// 2026-10-13 改错(用户实测 Fei2411/DeepSeek-Bot):**根目录没有清单 ≠ 这个仓库不是插件**。
|
|
306
|
+
// 真机该仓库根目录只有 docs/ + ds-bot/,而 ds-bot/package.json 同时带 dsh.client 与
|
|
307
|
+
// dsh.bundle.patch —— 旧代码在这里直接下"不是插件"的结论,结论错、也没给用户下一步。
|
|
308
|
+
// 现在先向下扫 1–2 层子目录(与 fetchSubpackageNames 的路径口径一致):
|
|
309
|
+
// · 扫到带 dsh 标记的清单 → 明说"插件在 <子目录>/ 下"并给出可复制的安装写法;
|
|
310
|
+
// · 扫完确实没有 → 保持旧结论,但文案补上"已检查根目录与 N 层子目录"(诚实)。
|
|
311
|
+
const skillProbe = await (probes.detectSkillRepo ?? detectSkillRepo)(job.repo, branch)
|
|
292
312
|
if (skillProbe.hasSkill) {
|
|
293
313
|
job.kind = 'skill'
|
|
294
314
|
await runSkillInstallJob(job, ports)
|
|
295
315
|
return
|
|
296
316
|
}
|
|
317
|
+
const decided = await decideNoRootManifest({
|
|
318
|
+
repo: job.repo,
|
|
319
|
+
branch,
|
|
320
|
+
reason,
|
|
321
|
+
probe: probes.fetchSubdirPlugins,
|
|
322
|
+
baseText: composeProbeFailureText(rootProbe, packageProbeErrorText(job.repo, branch, reason)),
|
|
323
|
+
source: job.source,
|
|
324
|
+
})
|
|
297
325
|
job.status = 'failed'
|
|
298
|
-
job.hint =
|
|
299
|
-
|
|
300
|
-
job.
|
|
326
|
+
job.hint = decided.hint
|
|
327
|
+
job.error = decided.error
|
|
328
|
+
job.subplugin = decided.subplugin
|
|
329
|
+
if (decided.hint === 'subdir-plugin') return
|
|
301
330
|
job.probeReason = reason
|
|
302
331
|
return
|
|
303
332
|
}
|
|
@@ -562,37 +591,9 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
562
591
|
job.finishedAt = Date.now()
|
|
563
592
|
}
|
|
564
593
|
}
|
|
565
|
-
/** 读取用户额外 bundle(非官方模板)的补丁插入行 id 与包名,用于"额外插件"判定。
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
for (const pkg of bundles) {
|
|
572
|
-
if (DEFAULT_BUNDLES.includes(pkg)) continue
|
|
573
|
-
try {
|
|
574
|
-
const pk = resolvePackageJson(pkg, profileDir)
|
|
575
|
-
const dir = dirname(pk ?? join(profileDir, 'node_modules', ...String(pkg).split('/'), 'package.json'))
|
|
576
|
-
const text = await readFile(join(dir, 'cordis.patch.yml'), 'utf8')
|
|
577
|
-
const lines = text.split(/\r?\n/u)
|
|
578
|
-
let inInsert = false
|
|
579
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
580
|
-
const line = lines[index]
|
|
581
|
-
if (/^- insert:\s*$/u.test(line)) {
|
|
582
|
-
inInsert = true
|
|
583
|
-
continue
|
|
584
|
-
}
|
|
585
|
-
if (/^- /u.test(line)) inInsert = false
|
|
586
|
-
if (!inInsert) continue
|
|
587
|
-
const idMatch = line.match(/^ {4}- id: ([A-Za-z0-9_.-]+)\s*$/u)
|
|
588
|
-
if (!idMatch) continue
|
|
589
|
-
rows.add(idMatch[1])
|
|
590
|
-
const nameMatch = (lines[index + 1] ?? '').match(/^ {6}name: ['"]([^'"]+)['"]\s*$/u)
|
|
591
|
-
if (nameMatch) rows.add(nameMatch[1])
|
|
592
|
-
}
|
|
593
|
-
} catch {}
|
|
594
|
-
}
|
|
595
|
-
} catch {}
|
|
596
|
-
return rows
|
|
597
|
-
}
|
|
598
|
-
export { runInstallJob, pnpmRemove, readExtraBundleRows, cleanupAttemptedCandidates, aiConsentFailureText, tryCandidateChannels, channelImpls, RELEASE_CHANNEL_BUDGET, GIT_CHANNEL_BUDGET_MS, GIT_SPEC_TIMEOUT_MS, GIT_MIN_ATTEMPT_MS, gitChannelBudgetMs, noteChannel, formatBudgetMs }
|
|
594
|
+
/** 读取用户额外 bundle(非官方模板)的补丁插入行 id 与包名,用于"额外插件"判定。
|
|
595
|
+
* 实现在 domain/bundle-refs.js(2026-10-13 搬出,只搬移未改逻辑:那边与 insert 行解析同属一件事,
|
|
596
|
+
* 这边腾出 600 行硬顶);这里保留同名 re-export —— **对外导出名与调用点一个字都没变**。 */
|
|
597
|
+
export { readExtraBundleRows }
|
|
598
|
+
|
|
599
|
+
export { runInstallJob, pnpmRemove, cleanupAttemptedCandidates, aiConsentFailureText, tryCandidateChannels, channelImpls, RELEASE_CHANNEL_BUDGET, GIT_CHANNEL_BUDGET_MS, GIT_SPEC_TIMEOUT_MS, GIT_MIN_ATTEMPT_MS, gitChannelBudgetMs, noteChannel, formatBudgetMs }
|
|
@@ -235,4 +235,62 @@ function alreadyInstalledTarget(profileDir, name, job = null) {
|
|
|
235
235
|
return null
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
/** 「其实已经装上了」的**三处核实**(2026-10-13 加法,用户被误导过):
|
|
239
|
+
* · ① profile `package.json` 的 `dependencies` 里有这个包(任何 spec 形态都算);
|
|
240
|
+
* · ② `dsh.profile.bundles` 里有它(bundle 层已注册);
|
|
241
|
+
* · ③ `node_modules/<包名>/package.json` 在盘上(真装着,顺便读出**版本号**)。
|
|
242
|
+
*
|
|
243
|
+
* 为什么要有它:面板的「最近安装失败」只看作业状态,于是会出现**明明已经装上了还报失败**的
|
|
244
|
+
* 误导(重试一次还可能把好端端的安装弄坏)。判据刻意是**三处任一命中即算已安装**——
|
|
245
|
+
* 缺哪一处是"持久化是否完整"的问题(由 persist.js 的 persisted 如实报),不是"装没装上"的问题。
|
|
246
|
+
* 只读、永不抛、绝不改动任何东西。返回 `{ installed, packageName, version, where, spec, note }`。 */
|
|
247
|
+
function installedEvidence(profileDir, packageName) {
|
|
248
|
+
const name = typeof packageName === 'string' ? packageName.trim() : ''
|
|
249
|
+
if (name === '') return { installed: false, packageName: null, version: null, where: [], spec: null, note: null }
|
|
250
|
+
const dir = String(profileDir ?? '')
|
|
251
|
+
const where = []
|
|
252
|
+
let spec = null
|
|
253
|
+
try {
|
|
254
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
255
|
+
const dep = manifest?.dependencies?.[name]
|
|
256
|
+
if (typeof dep === 'string' && dep !== '') { spec = dep; where.push('dependencies') }
|
|
257
|
+
} catch {}
|
|
258
|
+
try {
|
|
259
|
+
if (readBundles(dir).includes(name)) where.push('bundles')
|
|
260
|
+
} catch {}
|
|
261
|
+
let version = null
|
|
262
|
+
try {
|
|
263
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'node_modules', ...name.split('/'), 'package.json'), 'utf8'))
|
|
264
|
+
if (pkg !== null && typeof pkg === 'object' && pkg.name === name) {
|
|
265
|
+
where.push('node_modules')
|
|
266
|
+
version = typeof pkg.version === 'string' && pkg.version !== '' ? pkg.version : null
|
|
267
|
+
}
|
|
268
|
+
} catch {}
|
|
269
|
+
const installed = where.length > 0
|
|
270
|
+
if (installed !== true) return { installed: false, packageName: name, version: null, where: [], spec, note: null }
|
|
271
|
+
const place = where.join('、')
|
|
272
|
+
const versionText = version === null ? '(node_modules 里读不到版本号)' : ` v${version}`
|
|
273
|
+
return {
|
|
274
|
+
installed: true,
|
|
275
|
+
packageName: name,
|
|
276
|
+
version,
|
|
277
|
+
where,
|
|
278
|
+
spec,
|
|
279
|
+
note: `${name} 其实已经装上了${versionText} —— 命中:${place}。`
|
|
280
|
+
+ `最近一次安装报的失败**不代表没装上**(可能是重复安装/更新那一步失败),无需重装。`,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 失败作业 → 面板要显示的那段核实结果(名字从作业上按既有字段依次取,取不到就返回 null)。
|
|
285
|
+
* 顺序刻意是"最具体的先":子目录插件点名的包名 → 本次作业的候选包名 → 子目录识别结果里的包名。
|
|
286
|
+
* 名字取不到时返回 null(**不下"没装上"的结论** —— 那正是我们要避免的误导)。 */
|
|
287
|
+
function failedJobEvidence(profileDir, job) {
|
|
288
|
+
if (job === null || job === undefined) return null
|
|
289
|
+
const candidates = [job.packageName, job.candidateName, job.subplugin?.packageName, job.subplugin?.install?.packageName]
|
|
290
|
+
const name = candidates.find((n) => typeof n === 'string' && n.trim() !== '')
|
|
291
|
+
if (name === undefined) return null
|
|
292
|
+
return installedEvidence(profileDir, name)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export { verifyInstalledEntry, verifyTarballDigest, entryFieldOf, alreadyInstalledTarget, installedEvidence, failedJobEvidence, MIRROR_HINT }
|
|
296
|
+
|
|
@@ -536,6 +536,9 @@ function installJobView(job) {
|
|
|
536
536
|
suiteNote: job.suiteNote ?? null, repoSize: job.repoSize ?? null,
|
|
537
537
|
...presetJobView(job),
|
|
538
538
|
hint: job.hint ?? null,
|
|
539
|
+
// 子目录插件识别结果(2026-10-13 加法,真机 Fei2411/DeepSeek-Bot:插件在 ds-bot/ 而不是仓库根):
|
|
540
|
+
// 下发结构化字段而不只塞进 error 文案 —— 面板要能**单独显示**(哪个子目录、包名、哪些 dsh 标记)。
|
|
541
|
+
subplugin: job.subplugin ?? null,
|
|
539
542
|
// 子包级/套装级进度(2026-09-20 真装实测缺口:11 个子包的聚合仓库跑了 19 分钟,面板只显示
|
|
540
543
|
// "安装中",用户不知道在装第几个、还剩几个)。子包通道来自 install-job.js 候选循环维护的
|
|
541
544
|
// candidateXxx;套装通道来自 suite.js 维护的 suiteProgress(clone/装配两阶段)。
|
|
@@ -554,6 +557,11 @@ function installJobView(job) {
|
|
|
554
557
|
// 持久化判据(0.5.38 加法,见 domain/persist.js):`persisted:false` ⇒ 面板必须报「未持久化」
|
|
555
558
|
// (重启/任何一次 pnpm 操作后可能消失)并给出「一键钉住」动作 —— 绝不报"安装成功"。
|
|
556
559
|
persisted: job.persisted ?? null, persistNote: job.persistNote ?? null, persist: job.persist ?? null,
|
|
560
|
+
// 「其实已经装上了」的核实结果(0.5.47 加法,见 install-verify.js#installedEvidence):失败作业由路由在下发前**现查**三处覆盖。
|
|
561
|
+
installedEvidence: job.installedEvidence ?? null,
|
|
562
|
+
// 抓清单那一轮的**通道身份** + 本地落地落点(0.5.47 加法,见 domain/manifest-probe.js):
|
|
563
|
+
// 面板据此显示"清单经 npm 镜像 / 本地落地 / 直连读到的",本地命中时给出「从本地安装」的落点。
|
|
564
|
+
probe: job.probe ?? null, localDir: job.localDir ?? null, localHint: job.localHint ?? null,
|
|
557
565
|
}
|
|
558
566
|
}
|
|
559
567
|
|