dshmarket 1.66.24 → 1.66.26
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/client/client.js +216 -23
- package/lib/check.js +29 -2
- package/lib/host-recovery.js +159 -0
- package/lib/patch.js +10 -3
- package/lib/profile.js +108 -2
- package/lib/routes.js +100 -9
- package/lib/types/host-recovery.d.ts +94 -0
- package/lib/types/profile.d.ts +37 -0
- package/lib/types/updates.d.ts +21 -0
- package/lib/updates.js +44 -0
- package/lib/verify.js +35 -11
- package/package.json +1 -1
- package/src/check.ts +27 -2
- package/src/client/HostRecoveryBanner.tsx +152 -0
- package/src/client/MarketSection.tsx +24 -1
- package/src/client/index.ts +27 -0
- package/src/client/locales.ts +22 -0
- package/src/client/market-data.ts +3 -0
- package/src/host-recovery.ts +208 -0
- package/src/patch.ts +10 -3
- package/src/profile.ts +105 -3
- package/src/routes.ts +103 -9
- package/src/updates.ts +54 -0
- package/src/verify.ts +39 -11
package/client/client.js
CHANGED
|
@@ -193,7 +193,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
193
193
|
const IconRefreshOutline14 = pickIcon("IconRefreshOutlineRegular", "IconRefreshOutline14");
|
|
194
194
|
const IconSearchOutline16 = pickIcon("IconSearchOutlineRegular", "IconSearchOutline16");
|
|
195
195
|
const IconSparkle16 = pickIcon("IconSparkleRegular", "IconSparkle16");
|
|
196
|
-
const IconWarningOutline16 = pickIcon("IconWarningOutlineRegular", "IconWarningOutline16");
|
|
196
|
+
const IconWarningOutline16$1 = pickIcon("IconWarningOutlineRegular", "IconWarningOutline16");
|
|
197
197
|
//#endregion
|
|
198
198
|
//#region src/client/locales.ts
|
|
199
199
|
/** zh/en dictionaries for the Market settings section and install toast. */
|
|
@@ -383,6 +383,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
383
383
|
updateNow: "立即更新",
|
|
384
384
|
updateFail: "更新失败",
|
|
385
385
|
upToDate: "已是最新",
|
|
386
|
+
newerPrereleaseAvailable: "预发布版 {0} 已发布",
|
|
387
|
+
newerPrereleaseHint: "npm 上的 latest 没有更新;你在跟的这条预发布线发布了更新的版本。",
|
|
386
388
|
unreleasedCommits: "有新提交(未发版){0}",
|
|
387
389
|
unreleasedCommitsHint: "所选分支上有比你已装版本更新的提交,但没有发布新版本号;市场只安装正式版本,不会自动装这些提交。",
|
|
388
390
|
linkedDev: "本地开发",
|
|
@@ -915,7 +917,14 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
915
917
|
recoveryRestarting: "正在重启:dsh 启动后本页会刷新;若又失败,会带着新的报错回到这里。",
|
|
916
918
|
recoveryApplyFailed: "保存失败:",
|
|
917
919
|
recoveryWriteFailed: "上次的选择没有全部保存,因此没有重启:",
|
|
918
|
-
recoveryTimeout: "等待 DeepSeek Harness 启动超时。可点「调整插件」,检查是否某个插件导致无法启动。"
|
|
920
|
+
recoveryTimeout: "等待 DeepSeek Harness 启动超时。可点「调整插件」,检查是否某个插件导致无法启动。",
|
|
921
|
+
hostRecoveryTitle: "配置被换成了空壳",
|
|
922
|
+
hostRecoveryBody: "上次启动失败后,DSH 的恢复流程把配置换成了默认的。原来的 {0} 条还在备份里。",
|
|
923
|
+
hostRecoverySource: "备份:{0},{1}",
|
|
924
|
+
hostRecoveryRestore: "恢复配置",
|
|
925
|
+
hostRecoveryRestored: "已恢复 {0} 条设置。刷新页面就能看到。",
|
|
926
|
+
hostRecoveryFail: "恢复失败:",
|
|
927
|
+
hostRecoveryNote: "恢复前会自动存快照,随时能退回。"
|
|
919
928
|
};
|
|
920
929
|
const en = {
|
|
921
930
|
nav: "Plugin Market",
|
|
@@ -1102,6 +1111,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1102
1111
|
updateNow: "Update now",
|
|
1103
1112
|
updateFail: "Update failed",
|
|
1104
1113
|
upToDate: "Up to date",
|
|
1114
|
+
newerPrereleaseAvailable: "prerelease {0} published",
|
|
1115
|
+
newerPrereleaseHint: "The package's latest tag has no update; the prerelease line you are on published a newer build.",
|
|
1105
1116
|
unreleasedCommits: "New commits (unreleased) {0}",
|
|
1106
1117
|
unreleasedCommitsHint: "The selected branch has commits newer than your install, but no new version was published; the market installs releases only and will not apply them automatically.",
|
|
1107
1118
|
linkedDev: "local",
|
|
@@ -1634,7 +1645,14 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1634
1645
|
recoveryRestarting: "Restarting: this page reloads once dsh is up; if it fails again you come back here with the new error.",
|
|
1635
1646
|
recoveryApplyFailed: "Could not save: ",
|
|
1636
1647
|
recoveryWriteFailed: "The last choice was not saved in full, so nothing was restarted: ",
|
|
1637
|
-
recoveryTimeout: "Timed out waiting for DeepSeek Harness to start. \"Adjust plugins\" can tell you whether one of them is what stops it."
|
|
1648
|
+
recoveryTimeout: "Timed out waiting for DeepSeek Harness to start. \"Adjust plugins\" can tell you whether one of them is what stops it.",
|
|
1649
|
+
hostRecoveryTitle: "Your configuration was replaced",
|
|
1650
|
+
hostRecoveryBody: "After the last failed start, DSH's recovery replaced your configuration with a default one. The previous {0} settings are still in the backup.",
|
|
1651
|
+
hostRecoverySource: "Backup: {0}, {1}",
|
|
1652
|
+
hostRecoveryRestore: "Restore configuration",
|
|
1653
|
+
hostRecoveryRestored: "Restored {0} settings. Refresh the page to see them.",
|
|
1654
|
+
hostRecoveryFail: "Restore failed: ",
|
|
1655
|
+
hostRecoveryNote: "A snapshot is taken first, so you can roll this back."
|
|
1638
1656
|
};
|
|
1639
1657
|
//#endregion
|
|
1640
1658
|
//#region src/catalog-local-match.ts
|
|
@@ -4125,7 +4143,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
4125
4143
|
size: 13,
|
|
4126
4144
|
className: Market_module_css_default.reassureOk
|
|
4127
4145
|
});
|
|
4128
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
4146
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
4129
4147
|
size: 14,
|
|
4130
4148
|
className: Market_module_css_default.conflictIcon
|
|
4131
4149
|
});
|
|
@@ -4507,14 +4525,14 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
4507
4525
|
children: [
|
|
4508
4526
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4509
4527
|
className: Market_module_css_default.recoverySummary,
|
|
4510
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
4528
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
4511
4529
|
size: 14,
|
|
4512
4530
|
className: Market_module_css_default.bannerIcon
|
|
4513
4531
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: view.failure.summary || t("recoveryNoSummary") })]
|
|
4514
4532
|
}),
|
|
4515
4533
|
view.lastErrors.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4516
4534
|
className: Market_module_css_default.recoverySummary,
|
|
4517
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
4535
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
4518
4536
|
size: 14,
|
|
4519
4537
|
className: Market_module_css_default.bannerIcon
|
|
4520
4538
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("recoveryWriteFailed"), view.lastErrors.join("; ")] })]
|
|
@@ -4582,6 +4600,156 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
4582
4600
|
});
|
|
4583
4601
|
}
|
|
4584
4602
|
//#endregion
|
|
4603
|
+
//#region src/client/HostRecoveryBanner.tsx
|
|
4604
|
+
/**
|
|
4605
|
+
* #875: the way back from DSH's own fatal recovery.
|
|
4606
|
+
*
|
|
4607
|
+
* `sanitizeProfile` renames the profile patch to a `.bak-<epoch ms>` sibling
|
|
4608
|
+
* and writes a stub in its place, then boots normally — 22 KB of providers,
|
|
4609
|
+
* models and permission presets disappeared that way with nothing on screen
|
|
4610
|
+
* saying so. The server decides whether the shape is really the wipe
|
|
4611
|
+
* (`GET /dsh-market/host-recovery`); this banner is what the user sees when it
|
|
4612
|
+
* is, and it only appears then. Nothing else on the page changes, because for
|
|
4613
|
+
* every other profile this is not a thing that happened.
|
|
4614
|
+
*
|
|
4615
|
+
* Restoring is a single click, deliberately: it is undoable (the server
|
|
4616
|
+
* snapshots first and keeps the current file under a new `.bak` name), and a
|
|
4617
|
+
* confirmation here would be asking the user to judge something they cannot
|
|
4618
|
+
* see — the two patches side by side.
|
|
4619
|
+
*/
|
|
4620
|
+
function hostRecoveryOf(value) {
|
|
4621
|
+
if (value === null || typeof value !== "object") return null;
|
|
4622
|
+
const view = value;
|
|
4623
|
+
if (typeof view.offerRestore !== "boolean" || !Array.isArray(view.backups) || !Array.isArray(view.lostRowIds)) return null;
|
|
4624
|
+
const backups = [];
|
|
4625
|
+
for (const entry of view.backups) {
|
|
4626
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
4627
|
+
const item = entry;
|
|
4628
|
+
if (typeof item.name !== "string" || typeof item.bytes !== "number") continue;
|
|
4629
|
+
backups.push({
|
|
4630
|
+
name: item.name,
|
|
4631
|
+
stamp: typeof item.stamp === "number" ? item.stamp : null,
|
|
4632
|
+
bytes: item.bytes,
|
|
4633
|
+
rowIds: Array.isArray(item.rowIds) ? item.rowIds.filter((id) => typeof id === "string") : []
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
return {
|
|
4637
|
+
patchName: typeof view.patchName === "string" ? view.patchName : "cordis.patch.yml",
|
|
4638
|
+
currentBytes: typeof view.currentBytes === "number" ? view.currentBytes : 0,
|
|
4639
|
+
backups,
|
|
4640
|
+
lostRowIds: view.lostRowIds.filter((id) => typeof id === "string"),
|
|
4641
|
+
offerRestore: view.offerRestore
|
|
4642
|
+
};
|
|
4643
|
+
}
|
|
4644
|
+
/** "22.7 KB" / "1.2 MB" — the size is what makes the backup believable. */
|
|
4645
|
+
function formatBytes(bytes) {
|
|
4646
|
+
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
4647
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
4648
|
+
return `${bytes} B`;
|
|
4649
|
+
}
|
|
4650
|
+
function HostRecoveryBanner(props) {
|
|
4651
|
+
const { t, bootId } = props;
|
|
4652
|
+
const [view, setView] = (0, react.useState)(null);
|
|
4653
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
4654
|
+
const [done, setDone] = (0, react.useState)(null);
|
|
4655
|
+
const [error, setError] = (0, react.useState)(null);
|
|
4656
|
+
const [dismissed, setDismissed] = (0, react.useState)(false);
|
|
4657
|
+
(0, react.useEffect)(() => {
|
|
4658
|
+
try {
|
|
4659
|
+
setDismissed(sessionStorage.getItem("dshm-host-recovery-dismissed") === String(bootId ?? ""));
|
|
4660
|
+
} catch {}
|
|
4661
|
+
}, [bootId]);
|
|
4662
|
+
const dismiss = (0, react.useCallback)(() => {
|
|
4663
|
+
setDismissed(true);
|
|
4664
|
+
try {
|
|
4665
|
+
sessionStorage.setItem("dshm-host-recovery-dismissed", String(bootId ?? ""));
|
|
4666
|
+
} catch {}
|
|
4667
|
+
}, [bootId]);
|
|
4668
|
+
(0, react.useEffect)(() => {
|
|
4669
|
+
let live = true;
|
|
4670
|
+
fetch(api("/dsh-market/host-recovery"), { cache: "no-store" }).then((res) => res.json()).then((body) => {
|
|
4671
|
+
if (live) setView(hostRecoveryOf(body));
|
|
4672
|
+
}).catch(() => {});
|
|
4673
|
+
return () => {
|
|
4674
|
+
live = false;
|
|
4675
|
+
};
|
|
4676
|
+
}, [bootId]);
|
|
4677
|
+
const restore = (0, react.useCallback)(() => {
|
|
4678
|
+
const newest = view?.backups[0];
|
|
4679
|
+
if (newest === void 0 || busy) return;
|
|
4680
|
+
setBusy(true);
|
|
4681
|
+
setError(null);
|
|
4682
|
+
fetch(api("/dsh-market/host-recovery/restore"), {
|
|
4683
|
+
method: "POST",
|
|
4684
|
+
headers: { "content-type": "application/json" },
|
|
4685
|
+
body: JSON.stringify({ name: newest.name })
|
|
4686
|
+
}).then(async (res) => ({
|
|
4687
|
+
ok: res.ok,
|
|
4688
|
+
body: await res.json().catch(() => null)
|
|
4689
|
+
})).then(({ ok, body }) => {
|
|
4690
|
+
if (ok) setDone(typeof body?.restored?.rows === "number" ? body.restored.rows : 0);
|
|
4691
|
+
else setError(body?.error ?? "");
|
|
4692
|
+
}).catch(() => setError("network")).finally(() => setBusy(false));
|
|
4693
|
+
}, [busy, view]);
|
|
4694
|
+
if (dismissed) return null;
|
|
4695
|
+
if (done !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4696
|
+
className: Market_module_css_default.banner,
|
|
4697
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4698
|
+
className: Market_module_css_default.grow,
|
|
4699
|
+
children: t("hostRecoveryRestored").replace("{0}", String(done))
|
|
4700
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
4701
|
+
variant: "primary",
|
|
4702
|
+
size: "sm",
|
|
4703
|
+
onClick: () => location.reload(),
|
|
4704
|
+
children: t("refresh")
|
|
4705
|
+
})]
|
|
4706
|
+
});
|
|
4707
|
+
if (view === null || !view.offerRestore) return null;
|
|
4708
|
+
const newest = view.backups[0];
|
|
4709
|
+
const when = newest?.stamp != null ? new Date(newest.stamp).toLocaleString() : "";
|
|
4710
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4711
|
+
className: Market_module_css_default.banner,
|
|
4712
|
+
children: [
|
|
4713
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {
|
|
4714
|
+
size: 14,
|
|
4715
|
+
className: Market_module_css_default.bannerIcon
|
|
4716
|
+
}),
|
|
4717
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4718
|
+
className: Market_module_css_default.grow,
|
|
4719
|
+
children: [
|
|
4720
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("hostRecoveryTitle") }) }),
|
|
4721
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("hostRecoveryBody").replace("{0}", String(view.lostRowIds.length)) }),
|
|
4722
|
+
newest !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4723
|
+
className: Market_module_css_default.spec,
|
|
4724
|
+
children: [
|
|
4725
|
+
t("hostRecoverySource").replace("{0}", formatBytes(newest.bytes)).replace("{1}", when),
|
|
4726
|
+
" — ",
|
|
4727
|
+
t("hostRecoveryNote")
|
|
4728
|
+
]
|
|
4729
|
+
}),
|
|
4730
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4731
|
+
className: Market_module_css_default.spec,
|
|
4732
|
+
children: [t("hostRecoveryFail"), error]
|
|
4733
|
+
})
|
|
4734
|
+
]
|
|
4735
|
+
}),
|
|
4736
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
4737
|
+
variant: "primary",
|
|
4738
|
+
size: "sm",
|
|
4739
|
+
disabled: busy || newest === void 0,
|
|
4740
|
+
onClick: restore,
|
|
4741
|
+
children: busy ? t("backupWorking") : t("hostRecoveryRestore")
|
|
4742
|
+
}),
|
|
4743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
4744
|
+
variant: "ghost",
|
|
4745
|
+
size: "sm",
|
|
4746
|
+
onClick: dismiss,
|
|
4747
|
+
children: t("dismiss")
|
|
4748
|
+
})
|
|
4749
|
+
]
|
|
4750
|
+
});
|
|
4751
|
+
}
|
|
4752
|
+
//#endregion
|
|
4585
4753
|
//#region src/client/preset-panel.tsx
|
|
4586
4754
|
/**
|
|
4587
4755
|
* Diagnostics sub-panel — issue #98 phase 3 (plugin presets).
|
|
@@ -6462,7 +6630,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
6462
6630
|
const remaining = findings.length - preview.length;
|
|
6463
6631
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6464
6632
|
className: Market_module_css_default.banner,
|
|
6465
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
6633
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
6466
6634
|
size: 14,
|
|
6467
6635
|
className: Market_module_css_default.bannerIcon
|
|
6468
6636
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -11210,7 +11378,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11210
11378
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
11211
11379
|
redLines.filter(redLineIsUrgent).map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
11212
11380
|
className: Market_module_css_default.warnLine,
|
|
11213
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
11381
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
11214
11382
|
size: 14,
|
|
11215
11383
|
className: Market_module_css_default.bannerIcon
|
|
11216
11384
|
}), " " + redLineLabel(line)]
|
|
@@ -11409,12 +11577,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11409
11577
|
type: "button",
|
|
11410
11578
|
className: Market_module_css_default.cardBlockedMark,
|
|
11411
11579
|
onClick: openOperations,
|
|
11412
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t("queuedBadge")]
|
|
11580
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 13 }), t("queuedBadge")]
|
|
11413
11581
|
}) : blocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11414
11582
|
type: "button",
|
|
11415
11583
|
className: Market_module_css_default.cardBlockedMark,
|
|
11416
11584
|
onClick: openOperations,
|
|
11417
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t("opBlockedCard")]
|
|
11585
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 13 }), t("opBlockedCard")]
|
|
11418
11586
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
11419
11587
|
variant: "primary",
|
|
11420
11588
|
size: "sm",
|
|
@@ -11631,12 +11799,12 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11631
11799
|
type: "button",
|
|
11632
11800
|
className: Market_module_css_default.cardBlockedMark,
|
|
11633
11801
|
onClick: openOperations,
|
|
11634
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t("queuedBadge")]
|
|
11802
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 13 }), t("queuedBadge")]
|
|
11635
11803
|
}) : blocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11636
11804
|
type: "button",
|
|
11637
11805
|
className: Market_module_css_default.cardBlockedMark,
|
|
11638
11806
|
onClick: openOperations,
|
|
11639
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 13 }), t("opBlockedCard")]
|
|
11807
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 13 }), t("opBlockedCard")]
|
|
11640
11808
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
11641
11809
|
variant: "primary",
|
|
11642
11810
|
size: "sm",
|
|
@@ -12086,7 +12254,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12086
12254
|
}),
|
|
12087
12255
|
restoreErrors.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12088
12256
|
className: Market_module_css_default.banner,
|
|
12089
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
12257
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
12090
12258
|
size: 14,
|
|
12091
12259
|
className: Market_module_css_default.bannerIcon
|
|
12092
12260
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -12097,6 +12265,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12097
12265
|
}, error))]
|
|
12098
12266
|
})]
|
|
12099
12267
|
}),
|
|
12268
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostRecoveryBanner, {
|
|
12269
|
+
t,
|
|
12270
|
+
bootId
|
|
12271
|
+
}),
|
|
12100
12272
|
tab === "installed" && pendingBackup !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12101
12273
|
className: Market_module_css_default.banner,
|
|
12102
12274
|
children: [
|
|
@@ -12194,7 +12366,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12194
12366
|
}),
|
|
12195
12367
|
activationWarnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12196
12368
|
className: Market_module_css_default.banner,
|
|
12197
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
12369
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
12198
12370
|
size: 14,
|
|
12199
12371
|
className: Market_module_css_default.bannerIcon
|
|
12200
12372
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -12216,7 +12388,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12216
12388
|
}),
|
|
12217
12389
|
tab === "installed" && configNamespaceIssues.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12218
12390
|
className: Market_module_css_default.banner,
|
|
12219
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
12391
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
12220
12392
|
size: 14,
|
|
12221
12393
|
className: Market_module_css_default.bannerIcon
|
|
12222
12394
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -12269,7 +12441,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12269
12441
|
buildsSkipped !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
12270
12442
|
className: Market_module_css_default.banner,
|
|
12271
12443
|
children: [
|
|
12272
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, {
|
|
12444
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, {
|
|
12273
12445
|
size: 14,
|
|
12274
12446
|
className: Market_module_css_default.bannerIcon
|
|
12275
12447
|
}),
|
|
@@ -13656,7 +13828,17 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
13656
13828
|
className: Market_module_css_default.metaTag,
|
|
13657
13829
|
title: t("unreleasedCommitsHint"),
|
|
13658
13830
|
children: t("unreleasedCommits").replace("{0}", displayLatest(status.unreleasedCommit))
|
|
13659
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
13831
|
+
}) : status?.newerPrerelease !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
13832
|
+
className: Market_module_css_default.metaTag,
|
|
13833
|
+
title: t("newerPrereleaseHint"),
|
|
13834
|
+
children: t("newerPrereleaseAvailable").replace("{0}", status.newerPrerelease)
|
|
13835
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
13836
|
+
variant: "outline",
|
|
13837
|
+
size: "sm",
|
|
13838
|
+
disabled: updatingName !== null || removingName !== null || busyUrl !== null,
|
|
13839
|
+
onClick: () => doUpdate(name, false, false, status.newerPrerelease),
|
|
13840
|
+
children: t("install")
|
|
13841
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
13660
13842
|
className: Market_module_css_default.metaTag,
|
|
13661
13843
|
title: t("upToDate"),
|
|
13662
13844
|
children: t("upToDate")
|
|
@@ -14073,7 +14255,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14073
14255
|
]
|
|
14074
14256
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
14075
14257
|
className: Market_module_css_default.migrationWarning,
|
|
14076
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 14 }), t("migrateWarning")]
|
|
14258
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 14 }), t("migrateWarning")]
|
|
14077
14259
|
})]
|
|
14078
14260
|
}),
|
|
14079
14261
|
agentsConfirm !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
@@ -14414,17 +14596,17 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14414
14596
|
}),
|
|
14415
14597
|
exportState === "fail" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
14416
14598
|
text: t("exportLogFail"),
|
|
14417
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 14 }),
|
|
14599
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 14 }),
|
|
14418
14600
|
onDone: exportToastDone
|
|
14419
14601
|
}),
|
|
14420
14602
|
favoriteError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
14421
14603
|
text: localizeBilingual(favoriteError, lang),
|
|
14422
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 14 }),
|
|
14604
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 14 }),
|
|
14423
14605
|
onDone: favoriteErrorDone
|
|
14424
14606
|
}),
|
|
14425
14607
|
blockError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
14426
14608
|
text: localizeBilingual(blockError, lang),
|
|
14427
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 14 }),
|
|
14609
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 14 }),
|
|
14428
14610
|
onDone: blockErrorDone
|
|
14429
14611
|
}),
|
|
14430
14612
|
blockNotice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
@@ -14433,7 +14615,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14433
14615
|
}),
|
|
14434
14616
|
updateExemptError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
14435
14617
|
text: localizeBilingual(updateExemptError, lang),
|
|
14436
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 14 }),
|
|
14618
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 14 }),
|
|
14437
14619
|
onDone: updateExemptErrorDone
|
|
14438
14620
|
}),
|
|
14439
14621
|
updateExemptNotice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
@@ -14442,7 +14624,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14442
14624
|
}),
|
|
14443
14625
|
toggled !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
14444
14626
|
text: toggled.name + " " + t(toggled.enabled ? "toastToggledOn" : "toastToggledOff"),
|
|
14445
|
-
icon: toggled.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCheckOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16, { size: 14 }),
|
|
14627
|
+
icon: toggled.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCheckOutline16, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconWarningOutline16$1, { size: 14 }),
|
|
14446
14628
|
onDone: toggledDone
|
|
14447
14629
|
})
|
|
14448
14630
|
]
|
|
@@ -15231,6 +15413,17 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
15231
15413
|
}
|
|
15232
15414
|
const iconGaps = missingIcons(mod);
|
|
15233
15415
|
if (iconGaps.length > 0) console.warn("[dsh-market] host ui-primitives missing icons " + iconGaps.join(", ") + " — rendering without them");
|
|
15416
|
+
try {
|
|
15417
|
+
mountMarket(ctx);
|
|
15418
|
+
} catch (error) {
|
|
15419
|
+
console.error("[dsh-market] mounting failed, so the market is missing for this session. The rest of the app is unaffected. Report at https://github.com/dsh-market/dsh-market/issues with this line and the stack below:", error);
|
|
15420
|
+
}
|
|
15421
|
+
}
|
|
15422
|
+
/**
|
|
15423
|
+
* The market's mounts, split out only so apply() can contain a throw from any
|
|
15424
|
+
* of them. Not exported: the split is a guard, not a surface.
|
|
15425
|
+
*/
|
|
15426
|
+
function mountMarket(ctx) {
|
|
15234
15427
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
15235
15428
|
zh,
|
|
15236
15429
|
en
|
package/lib/check.js
CHANGED
|
@@ -313,6 +313,33 @@ function parseSemver(value) {
|
|
|
313
313
|
pre: m[4] === undefined ? [] : m[4].split('.'),
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Parse a comparator's target the way node-semver does: a PARTIAL version in
|
|
318
|
+
* a range is zero-filled, so `<0.3` is `<0.3.0` and `>=2` is `>=2.0.0`.
|
|
319
|
+
*
|
|
320
|
+
* Without this, every prerelease floor written as a partial — the
|
|
321
|
+
* `@deepseek-ai/dsh-*` peers of a whole family of connector plugins say
|
|
322
|
+
* `>=0.1.5 <0.3`, `>=0.1.7-rc <2`, `>=0.2.0-rc.0 <0.3` — parsed as unknown,
|
|
323
|
+
* `satisfiesRange` answered null, and the gate stood aside: measured against
|
|
324
|
+
* the host's own evaluator over 699 live catalog manifests, those were the
|
|
325
|
+
* last 7 of 2796 (package, runtime) pairs where the host refuses a plugin
|
|
326
|
+
* and the market still promised a restart would apply it.
|
|
327
|
+
*
|
|
328
|
+
* A BARE partial (`1.2`) is a different thing in semver — an x-range that
|
|
329
|
+
* means `>=1.2.0 <1.3.0` — so it deliberately stays unknown here rather than
|
|
330
|
+
* being mistaken for the exact version `1.2.0`.
|
|
331
|
+
*/
|
|
332
|
+
function parseComparatorTarget(target, op) {
|
|
333
|
+
const direct = parseSemver(target);
|
|
334
|
+
if (direct !== null)
|
|
335
|
+
return direct;
|
|
336
|
+
if (op === '')
|
|
337
|
+
return null;
|
|
338
|
+
const parts = target.trim().split('.');
|
|
339
|
+
if (parts.length > 3 || parts.some(part => !/^\d+$/u.test(part)))
|
|
340
|
+
return null;
|
|
341
|
+
return parseSemver([...parts, ...Array(3 - parts.length).fill('0')].join('.'));
|
|
342
|
+
}
|
|
316
343
|
function comparePre(a, b) {
|
|
317
344
|
const len = Math.max(a.length, b.length);
|
|
318
345
|
for (let i = 0; i < len; i += 1) {
|
|
@@ -441,7 +468,7 @@ export function satisfiesRange(version, range, options = {}) {
|
|
|
441
468
|
const m = /^(\^|~|>=|<=|>|<)?(.*)$/.exec(p);
|
|
442
469
|
const op = m?.[1] ?? '';
|
|
443
470
|
const target = (m?.[2] ?? '').trim();
|
|
444
|
-
const tv =
|
|
471
|
+
const tv = parseComparatorTarget(target, op);
|
|
445
472
|
if (tv === null)
|
|
446
473
|
return null;
|
|
447
474
|
const major = tv.major;
|
|
@@ -488,7 +515,7 @@ export function satisfiesRange(version, range, options = {}) {
|
|
|
488
515
|
// prerelease admission gate. Otherwise an unknown protocol such as
|
|
489
516
|
// `workspace:^` or `catalog:default` is misreported as a definite false
|
|
490
517
|
// whenever the resolved version happens to carry a prerelease tag.
|
|
491
|
-
if (
|
|
518
|
+
if (parseComparatorTarget(target, m[1] ?? '') === null)
|
|
492
519
|
return null;
|
|
493
520
|
return { op: m[1] ?? '', target };
|
|
494
521
|
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What DSH's own fatal recovery leaves behind (#875).
|
|
3
|
+
*
|
|
4
|
+
* When a boot fails, the desktop shell offers a recovery prompt. Using it
|
|
5
|
+
* runs `sanitizeProfile` in `@deepseek-ai/dsh-app-boot`, and that does not
|
|
6
|
+
* edit the profile patch — it renames `cordis.patch.yml` to a
|
|
7
|
+
* `<patch>.bak-<epoch ms>` sibling, writes a small default stub in its place,
|
|
8
|
+
* and restores the bundle list it was handed. The app then comes up normally,
|
|
9
|
+
* so from the outside there is no error at all: the stub looks exactly like a
|
|
10
|
+
* patch that never had content, and the backup is an unlabelled file sitting
|
|
11
|
+
* next to it with no timestamp a person would read. In the report that found
|
|
12
|
+
* this, 22 KB of providers, models, model defaults and permission presets
|
|
13
|
+
* vanished that way and nothing on screen said so.
|
|
14
|
+
*
|
|
15
|
+
* DSH core is the one destroying data, and the fix for that belongs upstream
|
|
16
|
+
* (never overwrite a patch that has content; keep an unticked entry as a
|
|
17
|
+
* disabled row instead of dropping it from `dsh.profile.bundles`). But the
|
|
18
|
+
* promise the user was acting on is this page's — the market's own recovery
|
|
19
|
+
* surface is the documented way back to a booting app — so the market carries
|
|
20
|
+
* the detection and the way back.
|
|
21
|
+
*
|
|
22
|
+
* The shape this looks for is deliberately narrow: a sibling named exactly
|
|
23
|
+
* `<patch name>.bak-<digits>`, holding rows, LARGER than the live patch, and
|
|
24
|
+
* naming rows the live patch no longer has. A hand-edited file, the market's
|
|
25
|
+
* own `.bak` shapes (`package.json.bak-asm` and friends), an older backup, or
|
|
26
|
+
* a patch that simply lost a row are all outside this surface — the point is
|
|
27
|
+
* to speak up only when the evidence is unambiguous, because a false alarm
|
|
28
|
+
* here trains the user to ignore the one that matters.
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { basename, dirname, join } from 'node:path';
|
|
32
|
+
import { readUserPatchState } from './patch.js';
|
|
33
|
+
import { createProfileSnapshot, DEFAULT_MAX_SNAPSHOTS } from './snapshot.js';
|
|
34
|
+
/**
|
|
35
|
+
* The host's backup name: `<anything>.bak-<epoch ms>`. The stamp is what the
|
|
36
|
+
* name carries, so it is read from there rather than from mtime — a copy or a
|
|
37
|
+
* restore tool rewrites mtime, and `Date.now()` in the host's own
|
|
38
|
+
* `sanitizeProfile` is what produced this name.
|
|
39
|
+
*/
|
|
40
|
+
const BACKUP_NAME_RE = /^(.+)\.bak-(\d{10,16})$/u;
|
|
41
|
+
/** Row ids a patch file declares, in any of the three shapes the market writes. */
|
|
42
|
+
function patchRowIds(patchPath) {
|
|
43
|
+
const state = readUserPatchState(patchPath);
|
|
44
|
+
return [...new Set([...state.inserts, ...state.disables, ...state.forced])];
|
|
45
|
+
}
|
|
46
|
+
function sizeOf(path) {
|
|
47
|
+
try {
|
|
48
|
+
return statSync(path).size;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Read the profile's recovery state. Never throws: a profile the market
|
|
56
|
+
* cannot read is a profile with nothing to offer, not a broken request.
|
|
57
|
+
*/
|
|
58
|
+
export function readHostRecoveryView(patchPath) {
|
|
59
|
+
const patchName = basename(patchPath);
|
|
60
|
+
const directory = dirname(patchPath);
|
|
61
|
+
const currentBytes = sizeOf(patchPath);
|
|
62
|
+
const currentRowIds = patchRowIds(patchPath);
|
|
63
|
+
let names = [];
|
|
64
|
+
try {
|
|
65
|
+
names = readdirSync(directory);
|
|
66
|
+
}
|
|
67
|
+
catch { /* unreadable directory — no candidates */ }
|
|
68
|
+
const backups = [];
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const match = BACKUP_NAME_RE.exec(name);
|
|
71
|
+
if (match === null || match[1] !== patchName)
|
|
72
|
+
continue;
|
|
73
|
+
const path = join(directory, name);
|
|
74
|
+
const rowIds = patchRowIds(path);
|
|
75
|
+
// A file with no rows holds no configuration; listing it as a restore
|
|
76
|
+
// point would only invite the user to restore nothing.
|
|
77
|
+
if (rowIds.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
backups.push({ name, stamp: Number(match[2]), bytes: sizeOf(path), rowIds });
|
|
80
|
+
}
|
|
81
|
+
backups.sort((left, right) => (right.stamp ?? 0) - (left.stamp ?? 0));
|
|
82
|
+
const newest = backups[0];
|
|
83
|
+
const lostRowIds = newest === undefined
|
|
84
|
+
? []
|
|
85
|
+
: newest.rowIds.filter(id => !currentRowIds.includes(id));
|
|
86
|
+
return {
|
|
87
|
+
patchName,
|
|
88
|
+
currentBytes,
|
|
89
|
+
currentRowIds,
|
|
90
|
+
backups,
|
|
91
|
+
lostRowIds,
|
|
92
|
+
offerRestore: newest !== undefined && lostRowIds.length > 0 && currentBytes < newest.bytes,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Put one backup back in place of the live patch.
|
|
97
|
+
*
|
|
98
|
+
* Three things happen in order, and each one is there for a reason:
|
|
99
|
+
*
|
|
100
|
+
* 1. a profile snapshot, so the restore is undoable the same way every other
|
|
101
|
+
* write on this page is (issue #98);
|
|
102
|
+
* 2. the live patch is RENAMED, never deleted — it is what the app is running
|
|
103
|
+
* with right now, and a user who restores the wrong backup must still find
|
|
104
|
+
* it there;
|
|
105
|
+
* 3. the backup's bytes are written to a temp file and renamed into place, so
|
|
106
|
+
* an interrupted write cannot leave a half patch behind, which is the one
|
|
107
|
+
* outcome that would not boot at all.
|
|
108
|
+
*
|
|
109
|
+
* The backup file itself is left where it is: it is the other copy of the
|
|
110
|
+
* user's configuration, and this function has no business being the last
|
|
111
|
+
* thing that ever held it.
|
|
112
|
+
*/
|
|
113
|
+
export function restoreHostRecoveryBackup(patchPath, profileDir, name, maxSnapshots = DEFAULT_MAX_SNAPSHOTS) {
|
|
114
|
+
const view = readHostRecoveryView(patchPath);
|
|
115
|
+
// The name is matched against the re-derived list, never used as a path.
|
|
116
|
+
// Everything reaching the filesystem is a name this function just read off
|
|
117
|
+
// the directory, so no caller input can steer a write.
|
|
118
|
+
const backup = view.backups.find(entry => entry.name === name);
|
|
119
|
+
if (backup === undefined)
|
|
120
|
+
return { ok: false, error: `找不到备份 ${name},它可能已经被移走或改名了。 / no such backup: ${name}` };
|
|
121
|
+
const backupPath = join(dirname(patchPath), backup.name);
|
|
122
|
+
let contents;
|
|
123
|
+
try {
|
|
124
|
+
contents = readFileSync(backupPath);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return { ok: false, error: `读不到备份 ${backup.name}:${messageOf(error)} — 文件可能没有权限或被占用。 / could not read the backup: ${messageOf(error)}` };
|
|
128
|
+
}
|
|
129
|
+
const captured = createProfileSnapshot(profileDir, maxSnapshots);
|
|
130
|
+
if (!captured.ok) {
|
|
131
|
+
// Refusing here is the honest failure: without a snapshot, this write
|
|
132
|
+
// would be the only one on the page with no way back.
|
|
133
|
+
return { ok: false, error: `恢复前没能存下快照,所以这次没有改动配置:${String(captured.error)} / refused to write without a snapshot: ${String(captured.error)}` };
|
|
134
|
+
}
|
|
135
|
+
if (existsSync(patchPath)) {
|
|
136
|
+
try {
|
|
137
|
+
renameSync(patchPath, `${patchPath}.bak-${Date.now()}`);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return { ok: false, error: `当前配置没能先改名保留,所以这次没有改动:${messageOf(error)} / could not move the current patch aside: ${messageOf(error)}` };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const temp = `${patchPath}.restore-${process.pid}`;
|
|
144
|
+
try {
|
|
145
|
+
writeFileSync(temp, contents);
|
|
146
|
+
renameSync(temp, patchPath);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return { ok: false, error: `写入失败:${messageOf(error)} / could not write the patch: ${messageOf(error)}` };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
snapshot: captured.snapshot?.id ?? null,
|
|
154
|
+
restored: { name: backup.name, bytes: contents.length, rows: backup.rowIds.length },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function messageOf(error) {
|
|
158
|
+
return error instanceof Error ? error.message : String(error);
|
|
159
|
+
}
|
package/lib/patch.js
CHANGED
|
@@ -29,7 +29,7 @@ import { join } from 'node:path';
|
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
import { logEvent } from './log.js';
|
|
31
31
|
import { parsePatchFile, parsePatchText } from './check.js';
|
|
32
|
-
import { bundlePatchInsertedIds, declaredBundlePatchFiles, parsePatchRows } from './profile.js';
|
|
32
|
+
import { bundleOptOutRowIds, bundlePatchInsertedIds, declaredBundlePatchFiles, parsePatchRows } from './profile.js';
|
|
33
33
|
/**
|
|
34
34
|
* Host infrastructure rows: disabling any of these breaks the very chain
|
|
35
35
|
* the patch layer runs on (e.g. timer → HMR, webserver → the page itself),
|
|
@@ -401,9 +401,16 @@ export function packagePatchFlags(host, profileDirectory, names, state) {
|
|
|
401
401
|
const forced = [];
|
|
402
402
|
for (const name of names) {
|
|
403
403
|
const rows = rowIdsForPackage(host, profileDirectory, name);
|
|
404
|
-
|
|
404
|
+
// Rows the package's own layer already ships off say nothing about what
|
|
405
|
+
// the user decided (#876): `dsh-mnemon` ships four strategy rows as
|
|
406
|
+
// opt-in, its user patch layer repeats one of them, and the package was
|
|
407
|
+
// reported as 「已停用 / 重启后保持关闭」 while it was running. Only the
|
|
408
|
+
// user's decisions ABOVE the bundle's own default count.
|
|
409
|
+
const optOut = new Set(bundleOptOutRowIds(join(profileDirectory, 'node_modules', name)));
|
|
410
|
+
const decidedByUser = (id) => !optOut.has(id);
|
|
411
|
+
if (rows.some(id => decidedByUser(id) && state.disables.includes(id)))
|
|
405
412
|
disabled.push(name);
|
|
406
|
-
if (rows.some(id => state.forced.includes(id)))
|
|
413
|
+
if (rows.some(id => decidedByUser(id) && state.forced.includes(id)))
|
|
407
414
|
forced.push(name);
|
|
408
415
|
}
|
|
409
416
|
return { disabled, forced };
|