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 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 = parseSemver(target);
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 (parseSemver(target) === null)
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
- if (rows.some(id => state.disables.includes(id)))
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 };