dshmarket 1.37.0 → 1.38.1

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
@@ -135,6 +135,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
135
135
  notesNone: "该插件未提供更新说明。",
136
136
  notesLoadFail: "暂时读不到更新说明,稍后再试。",
137
137
  restore: "恢复",
138
+ restoreOnline: "换用线上版本",
138
139
  restoreHint: "会卸载本地版本,重新安装线上版本,并保持更新检测。无法回退,请二次确认。",
139
140
  restoreContinue: "继续更新",
140
141
  restoreNoCatalog: "精选目录里没有这个插件,无法恢复到线上版本。可以卸载本地版,或继续用本地开发。",
@@ -603,6 +604,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
603
604
  notesNone: "This plugin does not publish update notes.",
604
605
  notesLoadFail: "Update notes are unavailable right now — try again later.",
605
606
  restore: "Restore",
607
+ restoreOnline: "Use online version",
606
608
  restoreHint: "This will uninstall the local version, reinstall the catalog version, and keep update checks. This cannot be undone — please confirm again.",
607
609
  restoreContinue: "Continue update",
608
610
  restoreNoCatalog: "This plugin is not in the curated catalog, so it cannot be restored. Uninstall the local copy, or keep developing it locally.",
@@ -1066,45 +1068,101 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
1066
1068
  function isMarketItself(plugin) {
1067
1069
  return plugin.name === "dsh-market" || plugin.npm === "dshmarket";
1068
1070
  }
1071
+ /** Normalize punctuation-separated package names and human text alike. */
1072
+ function searchText(value) {
1073
+ return value.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim().replace(/\s+/g, " ");
1074
+ }
1069
1075
  /**
1070
- * The discover list: category filter, then the published-within window, then
1071
- * search across name / owner / localized description / category ids and
1072
- * localized category labels, then the selected sort.
1073
- * Pure — the section renders exactly this.
1076
+ * Normalized catalog fields are immutable for the lifetime of one registry
1077
+ * entry. Keep them with that entry so typing does not repeat unicode
1078
+ * normalization across the whole catalog, while replaced catalogs remain
1079
+ * collectible. The raw query is intentionally not cached: it is normalized
1080
+ * once per call and would otherwise grow the cache on every keystroke.
1074
1081
  */
1075
- function visiblePlugins(plugins, options) {
1076
- const query = options.query.trim().toLowerCase();
1077
- const list = plugins.filter((p) => {
1078
- if (isMarketItself(p)) return false;
1079
- const categories = pluginCategories(p);
1080
- if (options.category !== "all" && !categories.includes(options.category)) return false;
1081
- if (options.sinceDays !== void 0 && !withinDays(p.added, options.sinceDays)) return false;
1082
- if (query === "") return true;
1083
- const desc = p.description && (p.description[options.lang] || p.description.en) || "";
1084
- const categoryMatches = categories.some((category) => {
1085
- if (category.toLowerCase().includes(query)) return true;
1086
- return Object.values(options.categories?.[category] ?? {}).some((label) => typeof label === "string" && label.toLowerCase().includes(query));
1087
- });
1088
- return p.name.toLowerCase().includes(query) || p.owner.toLowerCase().includes(query) || desc.toLowerCase().includes(query) || categoryMatches;
1089
- });
1082
+ const pluginSearchTextCache = /* @__PURE__ */ new WeakMap();
1083
+ function cachedPluginSearchText(plugin, value) {
1084
+ let fields = pluginSearchTextCache.get(plugin);
1085
+ if (fields === void 0) {
1086
+ fields = /* @__PURE__ */ new Map();
1087
+ pluginSearchTextCache.set(plugin, fields);
1088
+ }
1089
+ const hit = fields.get(value);
1090
+ if (hit !== void 0) return hit;
1091
+ const normalized = searchText(value);
1092
+ fields.set(value, normalized);
1093
+ return normalized;
1094
+ }
1095
+ /**
1096
+ * Relevance within one field. Exact and prefix matches beat phrase matches;
1097
+ * for a multi-word query every word must occur in the same field.
1098
+ */
1099
+ function fieldRelevance(plugin, value, query, tokens, weight) {
1100
+ if (!value) return 0;
1101
+ const text = cachedPluginSearchText(plugin, value);
1102
+ if (text === "" || !tokens.every((token) => text.includes(token))) return 0;
1103
+ if (text === query) return weight + 300;
1104
+ if (text.startsWith(query)) return weight + 250;
1105
+ if (text.includes(query)) return weight + 200;
1106
+ return weight + 150;
1107
+ }
1108
+ /**
1109
+ * Search ranking is field-aware rather than a popularity-only filter:
1110
+ * package identities outrank owners, descriptions, and categories. The
1111
+ * selected popularity/date sort remains the tie-breaker between equally
1112
+ * relevant entries.
1113
+ */
1114
+ function pluginRelevance(plugin, query, tokens, lang, categories) {
1115
+ const descriptions = plugin.description ?? {};
1116
+ const preferredLocale = descriptions[lang] ? lang : descriptions.en ? "en" : null;
1117
+ const preferredDescription = descriptions[lang] || descriptions.en;
1118
+ const otherDescriptions = Object.entries(descriptions).filter(([locale, value]) => locale !== preferredLocale && typeof value === "string").map(([, value]) => value);
1119
+ const categoryIds = pluginCategories(plugin);
1120
+ const categoryLabels = categoryIds.flatMap((category) => Object.values(categories?.[category] ?? {}));
1121
+ return Math.max(fieldRelevance(plugin, plugin.name, query, tokens, 700), fieldRelevance(plugin, plugin.npm, query, tokens, 700), fieldRelevance(plugin, plugin.owner, query, tokens, 400), fieldRelevance(plugin, preferredDescription, query, tokens, 280), ...otherDescriptions.map((value) => fieldRelevance(plugin, value, query, tokens, 240)), ...categoryIds.map((value) => fieldRelevance(plugin, value, query, tokens, 180)), ...categoryLabels.map((value) => fieldRelevance(plugin, value, query, tokens, 180)));
1122
+ }
1123
+ /** Compare two already-filtered entries using the user's selected sort. */
1124
+ function comparePlugins(a, b, sort) {
1090
1125
  const hasDownloads = (p) => typeof p.downloads === "number";
1091
- if (options.sort === "downloads-desc") return [...list].sort((a, b) => {
1126
+ if (sort === "downloads-desc") {
1092
1127
  if (hasDownloads(a) && hasDownloads(b)) return b.downloads - a.downloads;
1093
1128
  if (hasDownloads(a)) return -1;
1094
1129
  if (hasDownloads(b)) return 1;
1095
1130
  return (b.stars ?? -1) - (a.stars ?? -1);
1096
- });
1097
- if (options.sort === "downloads-asc") return [...list].sort((a, b) => {
1131
+ }
1132
+ if (sort === "downloads-asc") {
1098
1133
  if (hasDownloads(a) && hasDownloads(b)) return a.downloads - b.downloads;
1099
1134
  if (hasDownloads(a)) return -1;
1100
1135
  if (hasDownloads(b)) return 1;
1101
1136
  return (a.stars ?? -1) - (b.stars ?? -1);
1102
- });
1103
- if (options.sort === "stars-desc") return [...list].sort((a, b) => (b.stars ?? -1) - (a.stars ?? -1));
1104
- if (options.sort === "stars-asc") return [...list].sort((a, b) => (a.stars ?? -1) - (b.stars ?? -1));
1105
- if (options.sort === "added-desc") return [...list].sort((a, b) => String(b.added).localeCompare(String(a.added)));
1106
- if (options.sort === "added-asc") return [...list].sort((a, b) => String(a.added).localeCompare(String(b.added)));
1107
- return list;
1137
+ }
1138
+ if (sort === "stars-desc") return (b.stars ?? -1) - (a.stars ?? -1);
1139
+ if (sort === "stars-asc") return (a.stars ?? -1) - (b.stars ?? -1);
1140
+ if (sort === "added-desc") return String(b.added).localeCompare(String(a.added));
1141
+ if (sort === "added-asc") return String(a.added).localeCompare(String(b.added));
1142
+ return 0;
1143
+ }
1144
+ /**
1145
+ * The discover list: category filter, then the published-within window, then
1146
+ * relevance-ranked search across package identity / owner / every localized
1147
+ * description / category ids and labels. With no search, only the selected
1148
+ * sort applies, preserving the existing discover-list behaviour.
1149
+ * Pure — the section renders exactly this.
1150
+ */
1151
+ function visiblePlugins(plugins, options) {
1152
+ const query = searchText(options.query);
1153
+ const tokens = query.split(" ").filter(Boolean);
1154
+ return plugins.flatMap((plugin, index) => {
1155
+ if (isMarketItself(plugin)) return [];
1156
+ const categories = pluginCategories(plugin);
1157
+ if (options.category !== "all" && !categories.includes(options.category)) return [];
1158
+ if (options.sinceDays !== void 0 && !withinDays(plugin.added, options.sinceDays)) return [];
1159
+ const relevance = query === "" ? 0 : pluginRelevance(plugin, query, tokens, options.lang, options.categories);
1160
+ return relevance === 0 && query !== "" ? [] : [{
1161
+ plugin,
1162
+ relevance,
1163
+ index
1164
+ }];
1165
+ }).sort((a, b) => b.relevance - a.relevance || comparePlugins(a.plugin, b.plugin, options.sort) || a.index - b.index).map((row) => row.plugin);
1108
1166
  }
1109
1167
  /** The themes tab listing: theme category only, most-starred first. */
1110
1168
  function themePlugins(plugins) {
@@ -5469,6 +5527,21 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
5469
5527
  const [qThemes, setQThemes] = (0, react.useState)("");
5470
5528
  const [qInstalled, setQInstalled] = (0, react.useState)("");
5471
5529
  const [cat, setCat] = (0, react.useState)("all");
5530
+ (0, react.useEffect)(() => {
5531
+ const target = props.preferredSubsectionId;
5532
+ if (target === void 0) return;
5533
+ const separator = target.indexOf(":");
5534
+ const kind = separator === -1 ? target : target.slice(0, separator);
5535
+ const value = separator === -1 ? "" : target.slice(separator + 1);
5536
+ if (kind === "installed") {
5537
+ setTab("installed");
5538
+ setQInstalled(value);
5539
+ } else if (kind === "discover") {
5540
+ setTab("discover");
5541
+ setCat("all");
5542
+ setQ(value);
5543
+ }
5544
+ }, [props.preferredSubsectionId]);
5472
5545
  const [confirming, setConfirming] = (0, react.useState)(null);
5473
5546
  /** The plugin whose comment thread is open, or null. */
5474
5547
  const [commentsFor, setCommentsFor] = (0, react.useState)(null);
@@ -6738,10 +6811,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6738
6811
  });
6739
6812
  }, [doGroupAction, groups]);
6740
6813
  const selfName = installed["dshmarket"] !== void 0 ? "dshmarket" : "dsh-market";
6741
- const updatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable);
6814
+ const batchUpdatableNames = Object.keys(installed).filter((name) => name !== selfName && !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable).filter((name) => updates[name]?.restoreRequired !== true);
6742
6815
  const installedOtherCount = Object.keys(installed).filter((name) => name !== selfName).length;
6743
6816
  const doUpdateAll = (0, react.useCallback)(() => {
6744
- const names = updatableNames.slice();
6817
+ const names = batchUpdatableNames.slice();
6745
6818
  setUpdatingAll(true);
6746
6819
  const next = () => {
6747
6820
  const name = names.shift();
@@ -6752,7 +6825,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
6752
6825
  doUpdate(name).then(next, next);
6753
6826
  };
6754
6827
  next();
6755
- }, [updatableNames, doUpdate]);
6828
+ }, [batchUpdatableNames, doUpdate]);
6756
6829
  const finishRestore = (0, react.useCallback)((body) => {
6757
6830
  const errors = Array.isArray(body.errors) ? body.errors : [];
6758
6831
  const unportable = Array.isArray(body.unportable) ? body.unportable : [];
@@ -7510,18 +7583,20 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
7510
7583
  }),
7511
7584
  (() => {
7512
7585
  const self = installed["dshmarket"] !== void 0 ? "dshmarket" : "dsh-market";
7513
- return updates[self] && updates[self].updateAvailable && !updatedNames.includes(self) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
7586
+ const status = updates[self];
7587
+ return status && status.updateAvailable && !updatedNames.includes(self) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
7514
7588
  variant: "primary",
7515
7589
  size: "sm",
7516
7590
  disabled: updatingName !== null || busyUrl !== null,
7517
7591
  onClick: () => {
7518
7592
  setTab("installed");
7519
- doUpdate(self);
7593
+ if (status.restoreRequired === true) askRestore(self);
7594
+ else doUpdate(self);
7520
7595
  },
7521
- children: updatingName === self ? t("updating") : t("marketUpdate")
7596
+ children: updatingName === self ? t("updating") : status.restoreRequired === true ? t("restoreOnline") : t("marketUpdate")
7522
7597
  });
7523
7598
  })(),
7524
- updatableNames.length >= 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
7599
+ batchUpdatableNames.length >= 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
7525
7600
  variant: "primary",
7526
7601
  size: "sm",
7527
7602
  disabled: updatingAll || updatingName !== null || busyUrl !== null || removingName !== null,
@@ -7529,7 +7604,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
7529
7604
  setTab("installed");
7530
7605
  doUpdateAll();
7531
7606
  },
7532
- children: updatingAll ? t("updating") : t("updateAll") + " (" + updatableNames.length + ")"
7607
+ children: updatingAll ? t("updating") : t("updateAll") + " (" + batchUpdatableNames.length + ")"
7533
7608
  })
7534
7609
  ]
7535
7610
  }),
@@ -8881,8 +8956,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
8881
8956
  size: "sm",
8882
8957
  className: Market_module_css_default.warnBtn,
8883
8958
  disabled: updatingName !== null,
8884
- onClick: () => doUpdate(name),
8885
- children: t("update")
8959
+ onClick: () => {
8960
+ if (status.restoreRequired === true) askRestore(name);
8961
+ else doUpdate(name);
8962
+ },
8963
+ children: status.restoreRequired === true ? t("restoreOnline") : t("update")
8886
8964
  }) : localDev ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8887
8965
  className: Market_module_css_default.metaTag,
8888
8966
  title: t("linkedDev"),
@@ -8897,7 +8975,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
8897
8975
  className: Market_module_css_default.dangerBtn,
8898
8976
  disabled: true,
8899
8977
  children: t("uninstalling")
8900
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [localDev && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
8978
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [localDev && status?.restoreRequired !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
8901
8979
  variant: "outline",
8902
8980
  size: "sm",
8903
8981
  disabled: removingName !== null || busyUrl !== null || updatingName !== null,
@@ -9411,7 +9489,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9411
9489
  return {
9412
9490
  updateAvailable: own.updateAvailable === true,
9413
9491
  latest: own.latest ?? null,
9414
- channelSwitch: own.channelSwitch ?? null
9492
+ channelSwitch: own.channelSwitch ?? null,
9493
+ restoreRequired: own.restoreRequired === true
9415
9494
  };
9416
9495
  }
9417
9496
  /**
@@ -9433,6 +9512,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9433
9512
  const [status, setStatus] = (0, react.useState)(null);
9434
9513
  const [update, setUpdate] = (0, react.useState)(null);
9435
9514
  const [phase, setPhase] = (0, react.useState)("idle");
9515
+ const [restoreConfirming, setRestoreConfirming] = (0, react.useState)(false);
9436
9516
  const [purge, setPurge] = (0, react.useState)(false);
9437
9517
  const [error, setError] = (0, react.useState)(null);
9438
9518
  /**
@@ -9482,6 +9562,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9482
9562
  })).json();
9483
9563
  }, []);
9484
9564
  const onUpdate = (0, react.useCallback)((force = false) => {
9565
+ setRestoreConfirming(false);
9485
9566
  setPhase("working");
9486
9567
  setError(null);
9487
9568
  setStale(false);
@@ -9489,6 +9570,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9489
9570
  try {
9490
9571
  const body = await post(api("/dsh-market/update"), {
9491
9572
  name: "dshmarket",
9573
+ ...update?.restoreRequired === true ? { restore: true } : {},
9492
9574
  ...force ? { force: true } : {}
9493
9575
  });
9494
9576
  if (body.ok === true) setPhase("updated");
@@ -9502,7 +9584,11 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9502
9584
  setPhase("failed");
9503
9585
  }
9504
9586
  })();
9505
- }, [post, t]);
9587
+ }, [
9588
+ post,
9589
+ t,
9590
+ update?.restoreRequired
9591
+ ]);
9506
9592
  const onRemove = (0, react.useCallback)(() => {
9507
9593
  setPhase("working");
9508
9594
  setError(null);
@@ -9608,13 +9694,26 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9608
9694
  variant: "primary",
9609
9695
  size: "sm",
9610
9696
  disabled: busy,
9611
- onClick: () => onUpdate()
9612
- }, t("setSelfUpdate")) : update?.channelSwitch != null ? (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
9697
+ onClick: () => {
9698
+ if (update.restoreRequired) setRestoreConfirming(true);
9699
+ else onUpdate();
9700
+ }
9701
+ }, update.restoreRequired ? t("restoreOnline") : t("setSelfUpdate")) : update?.channelSwitch != null ? (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
9613
9702
  variant: "outline",
9614
9703
  size: "sm",
9615
9704
  disabled: busy,
9616
9705
  onClick: () => onUpdate()
9617
- }, t("setChannelSwitch")) : null) : null, status?.selfManaged === true ? row(t("setChannel"), t(CHANNEL_HINT[status.channel]), (0, react.createElement)("div", { className: Market_module_css_default.setSeg }, (status?.channels ?? ["stable", "beta"]).map((id) => (0, react.createElement)("button", {
9706
+ }, t("setChannelSwitch")) : null) : null, status?.selfManaged === true && restoreConfirming ? (0, react.createElement)("div", { className: Market_module_css_default.setConfirm }, (0, react.createElement)("div", { className: Market_module_css_default.setHint }, t("restoreHint")), (0, react.createElement)("div", { className: Market_module_css_default.setActions }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
9707
+ variant: "ghost",
9708
+ size: "sm",
9709
+ onClick: () => {
9710
+ setRestoreConfirming(false);
9711
+ }
9712
+ }, t("setSelfCancel")), (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
9713
+ variant: "primary",
9714
+ size: "sm",
9715
+ onClick: () => onUpdate()
9716
+ }, t("restoreContinue")))) : null, status?.selfManaged === true ? row(t("setChannel"), t(CHANNEL_HINT[status.channel]), (0, react.createElement)("div", { className: Market_module_css_default.setSeg }, (status?.channels ?? ["stable", "beta"]).map((id) => (0, react.createElement)("button", {
9618
9717
  key: id,
9619
9718
  type: "button",
9620
9719
  className: status?.channel === id ? `${Market_module_css_default.setSegBtn} ${Market_module_css_default.setSegOn}` : Market_module_css_default.setSegBtn,
@@ -9725,14 +9824,15 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
9725
9824
  label: () => t("nav"),
9726
9825
  locale: NS,
9727
9826
  inject: () => ({ t })
9728
- }, () => (0, react.createElement)(MarketSection, {
9827
+ }, (ownerProps = {}) => (0, react.createElement)(MarketSection, {
9729
9828
  t,
9730
9829
  locale: ctx.locale,
9731
9830
  theme: ctx.theme,
9732
9831
  themeStore: {
9733
9832
  subscribe: (cb) => ctx.on("theme/change", cb),
9734
9833
  getSnapshot: () => ctx.theme.getTheme()
9735
- }
9834
+ },
9835
+ preferredSubsectionId: ownerProps.preferredSubsectionId
9736
9836
  }));
9737
9837
  if (typeof off === "function") retireSection = off;
9738
9838
  return off;