wallpaper-engine-dsh 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,7 @@ Media serving is defensive by design: tokens are unguessable 72-bit values, stab
64
64
 
65
65
  - Scene and application wallpapers are not listed: they are Wallpaper Engine's private `.pkg` scene packages, and their live animation can only be rendered by Wallpaper Engine itself on the desktop.
66
66
  - Web wallpapers run isolated from the page's storage, so ones that persist state across loads won't keep it.
67
+ - Web wallpapers with sound may stay silent until you interact with the page — browsers gate audio in sandboxed documents on a user gesture, and wallpapers that rely on Wallpaper Engine's system-audio feed (visualizers) have no signal to draw from.
67
68
  - The glass effect reads DSH design tokens; if a future shell redesign renames them, frosting degrades to plain transparency.
68
69
 
69
70
  ## Development
package/README.zh.md CHANGED
@@ -64,6 +64,7 @@ bundle 分为两半:
64
64
 
65
65
  - 场景与应用壁纸不出现在列表中:它们是 Wallpaper Engine 私有的 `.pkg` 场景包,动态渲染只能由桌面端的 Wallpaper Engine 完成。
66
66
  - 网页壁纸与页面存储隔离,需要跨加载保存状态的壁纸无法保留状态。
67
+ - 带声音的网页壁纸可能在你与页面交互前保持静默——浏览器对沙箱文档的音频有自动播放限制;依赖 Wallpaper Engine 系统音频输入的壁纸(如音频可视化)在浏览器中没有信号可画。
67
68
  - 玻璃效果读 DSH 设计令牌;若未来外壳重构重命名令牌,磨砂会退化为普通半透明。
68
69
 
69
70
  ## 开发
package/lib/client.js CHANGED
@@ -10,6 +10,11 @@ window.__ModuleLoader__.load({
10
10
  const SETTINGS_KEY = "wallpaper-engine-dsh:selection";
11
11
  const INVENTORY_URL = "/we-background/inventory";
12
12
  const ACTIVE_ATTR = "data-webg-wallpaper";
13
+ // Plugin-owned dark-theme signal for CSS. The shell's old dark-mode body
14
+ // attribute is gone in DSH >= 0.2.1-rc.5; the client now derives dark from
15
+ // the shell's theme service and mirrors it here, so the stylesheet never
16
+ // keys off host DOM internals.
17
+ const DARK_ATTR = "data-webg-dark";
13
18
  const LAYER_ID = "wallpaper-engine-dsh-layer";
14
19
  const SCRIM_ID = "wallpaper-engine-dsh-scrim";
15
20
  const STYLE_TAG_ID = "wallpaper-engine-dsh/styles";
@@ -688,6 +693,7 @@ window.__ModuleLoader__.load({
688
693
  media = document.createElement("iframe");
689
694
  media.src = sel.url;
690
695
  media.setAttribute("sandbox", "allow-scripts");
696
+ media.setAttribute("allow", "autoplay");
691
697
  media.setAttribute("referrerpolicy", "no-referrer");
692
698
  media.setAttribute("frameborder", "0");
693
699
  media.setAttribute("scrolling", "no");
@@ -1019,7 +1025,13 @@ window.__ModuleLoader__.load({
1019
1025
  if (existing) {
1020
1026
  existing.classList.add("webg-layer--leave");
1021
1027
  const stale = existing;
1022
- schedule(() => stale.remove(), LEAVE_MS);
1028
+ schedule(() => {
1029
+ const v = stale.querySelector("video");
1030
+ if (v) { v.pause(); v.removeAttribute("src"); if (typeof v.load === "function") v.load(); }
1031
+ const f = stale.querySelector("iframe");
1032
+ if (f) { f.src = "about:blank"; }
1033
+ stale.remove();
1034
+ }, LEAVE_MS);
1023
1035
  }
1024
1036
  }
1025
1037
  const node = document.getElementById(LAYER_ID);
@@ -1035,11 +1047,18 @@ window.__ModuleLoader__.load({
1035
1047
  existing.classList.add("webg-layer--leave");
1036
1048
  const stale = existing;
1037
1049
  schedule(() => {
1050
+ const v = stale.querySelector("video");
1051
+ if (v) { v.pause(); v.removeAttribute("src"); if (typeof v.load === "function") v.load(); }
1052
+ const f = stale.querySelector("iframe");
1053
+ if (f) { f.src = "about:blank"; }
1038
1054
  stale.remove();
1039
1055
  if (!document.getElementById(LAYER_ID)) {
1040
1056
  const s = document.getElementById(SCRIM_ID);
1041
1057
  if (s) s.remove();
1042
1058
  document.body.removeAttribute(ACTIVE_ATTR);
1059
+ if (previewProbe) { previewProbe.remove(); previewProbe = null; }
1060
+ sampledLuminance = null;
1061
+ clearEffects();
1043
1062
  }
1044
1063
  }, LEAVE_MS);
1045
1064
  }
@@ -1060,6 +1079,9 @@ window.__ModuleLoader__.load({
1060
1079
  if (selection.type === "web") {
1061
1080
  const w = selection.inventory.wallpapers.find((x) => x.id === selection.id);
1062
1081
  if (w && w.preview) probePreviewLuminance(w.preview);
1082
+ } else if (previewProbe) {
1083
+ previewProbe.remove();
1084
+ previewProbe = null;
1063
1085
  }
1064
1086
  }
1065
1087
  }
@@ -1076,6 +1098,9 @@ window.__ModuleLoader__.load({
1076
1098
  // tick would rewrite all CSS variables AND force a synchronous layout for
1077
1099
  // nothing. clearEffects() resets it so the next apply always writes.
1078
1100
  let lastEffectKey = null;
1101
+ // Resolved dark-theme state, driven by the shell theme service (or the OS
1102
+ // prefers-color-scheme fallback). Mirrored onto body[DARK_ATTR] for CSS.
1103
+ let themeDark = false;
1079
1104
 
1080
1105
  /** Average Rec.601 luminance (0..1) of a drawable media frame, or null. */
1081
1106
  function luminanceOf(media) {
@@ -1146,10 +1171,20 @@ window.__ModuleLoader__.load({
1146
1171
  // The veil OPPOSES the text colour: dark theme (light text) darkens bright
1147
1172
  // wallpapers, light theme (dark text) brightens dark ones — either way the
1148
1173
  // text side wins. This is what makes ANY wallpaper survivable.
1149
- const dark = Boolean(document.body.hasAttribute && document.body.hasAttribute("data-ds-dark-theme"));
1174
+ const dark = themeDark;
1175
+ if (typeof document !== "undefined" && document.body) {
1176
+ if (dark) {
1177
+ if (!(document.body.hasAttribute && document.body.hasAttribute(DARK_ATTR))) {
1178
+ document.body.setAttribute(DARK_ATTR, "");
1179
+ }
1180
+ } else if (document.body.hasAttribute && document.body.hasAttribute(DARK_ATTR)) {
1181
+ document.body.removeAttribute(DARK_ATTR);
1182
+ }
1183
+ }
1184
+ const roundedLum = sampledLuminance !== null ? Math.round(sampledLuminance * 100) / 100 : null;
1150
1185
  const key = [
1151
1186
  dark, selection.scrim, selection.border, selection.blur, selection.wallpaperBlur,
1152
- selection.autoScrim, sampledLuminance, selection.fit, selection.zoom,
1187
+ selection.autoScrim, roundedLum, selection.fit, selection.zoom,
1153
1188
  selection.offsetX, selection.offsetY, selection.rotation, selection.mirrored,
1154
1189
  ].join("\u0000");
1155
1190
  if (key === lastEffectKey) return; // nothing it reads changed → no writes, no reflow
@@ -1200,9 +1235,6 @@ window.__ModuleLoader__.load({
1200
1235
  // slider feedback lands on this frame even on stalled compositors.
1201
1236
  const scrim = document.getElementById(SCRIM_ID);
1202
1237
  if (scrim) scrim.style.background = veilColor;
1203
- if (document.body && document.body.offsetHeight !== undefined) {
1204
- void document.body.offsetHeight;
1205
- }
1206
1238
  }
1207
1239
 
1208
1240
  function clearEffects() {
@@ -1242,7 +1274,7 @@ window.__ModuleLoader__.load({
1242
1274
  }
1243
1275
  onInput(v);
1244
1276
  };
1245
- return React.createElement(React.Fragment, null,
1277
+ return React.createElement(React.Fragment, { key: label },
1246
1278
  React.createElement("span", { className: "webg-hint webg-label" }, label),
1247
1279
  React.createElement("input", {
1248
1280
  className: "webg-slider", type: "range",
@@ -1446,6 +1478,7 @@ window.__ModuleLoader__.load({
1446
1478
  className: "webg-text webg-search", type: "search",
1447
1479
  placeholder: t.searchPlaceholder,
1448
1480
  value: query,
1481
+ onChange: (e) => setQuery(e.target.value),
1449
1482
  onInput: (e) => setQuery(e.target.value),
1450
1483
  }),
1451
1484
  query.trim() && React.createElement("span", { className: "webg-hint" },
@@ -1476,14 +1509,14 @@ window.__ModuleLoader__.load({
1476
1509
  React.createElement("span", { className: "webg-card-close" }, t.closeCard),
1477
1510
  ),
1478
1511
  usableList.length === 0
1479
- ? React.createElement("span", { className: "webg-hint" },
1512
+ ? React.createElement("span", { className: "webg-hint webg-grid-empty" },
1480
1513
  sel.inventory.installDir
1481
1514
  ? (sel.inventory.total > 0
1482
1515
  ? t.emptyNoUsable(sel.inventory.total)
1483
1516
  : t.emptyNoProjects)
1484
1517
  : t.emptyNoInstall)
1485
1518
  : filteredList.length === 0
1486
- ? React.createElement("span", { className: "webg-hint" }, t.noMatch(q))
1519
+ ? React.createElement("span", { className: "webg-hint webg-grid-empty" }, t.noMatch(q))
1487
1520
  : filteredList.map((w) => ThumbCard(w, w.id === sel.id, () => applySelection(w.id), t)),
1488
1521
  );
1489
1522
  }
@@ -1563,7 +1596,9 @@ window.__ModuleLoader__.load({
1563
1596
  React.createElement("input", {
1564
1597
  className: "webg-text", type: "text",
1565
1598
  value: editing.name,
1566
- onInput: (e) => { editing.name = e.target.value; emit(); },
1599
+ onChange: (e) => { editing.name = e.target.value; if (!e.nativeEvent || !e.nativeEvent.isComposing) emit(); },
1600
+ onInput: (e) => { editing.name = e.target.value; },
1601
+ onCompositionEnd: (e) => { editing.name = e.target.value; emit(); },
1567
1602
  }),
1568
1603
  ),
1569
1604
  React.createElement("div", { className: "webg-row" },
@@ -1588,6 +1623,7 @@ window.__ModuleLoader__.load({
1588
1623
  className: "webg-text", type: "search",
1589
1624
  placeholder: t.filterPlaceholder,
1590
1625
  value: editorQuery,
1626
+ onChange: (e) => setEditorQuery(e.target.value),
1591
1627
  onInput: (e) => setEditorQuery(e.target.value),
1592
1628
  }),
1593
1629
  React.createElement("div", { className: "webg-editor-grid" },
@@ -1595,10 +1631,10 @@ window.__ModuleLoader__.load({
1595
1631
  const eq = editorQuery.trim();
1596
1632
  const pool = usableList.filter((w) => matchesQuery(w, eq));
1597
1633
  if (usableList.length === 0) {
1598
- return React.createElement("span", { className: "webg-hint" }, t.noUsable);
1634
+ return React.createElement("span", { className: "webg-hint webg-grid-empty" }, t.noUsable);
1599
1635
  }
1600
1636
  if (pool.length === 0) {
1601
- return React.createElement("span", { className: "webg-hint" }, t.noMatch(eq));
1637
+ return React.createElement("span", { className: "webg-hint webg-grid-empty" }, t.noMatch(eq));
1602
1638
  }
1603
1639
  return pool.map((w) => EditorCard(w, editing, t));
1604
1640
  })(),
@@ -1685,7 +1721,7 @@ window.__ModuleLoader__.load({
1685
1721
  className: "webg-btn", type: "button", onClick: enterAdjust,
1686
1722
  disabled: sel.adjusting,
1687
1723
  }, t.adjust),
1688
- (sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0) &&
1724
+ (sel.zoom !== 1 || sel.offsetX !== 0 || sel.offsetY !== 0 || sel.rotation !== 0) &&
1689
1725
  React.createElement("button", {
1690
1726
  className: "webg-btn", type: "button", onClick: resetCrop,
1691
1727
  }, t.resetCrop),
@@ -1892,7 +1928,7 @@ window.__ModuleLoader__.load({
1892
1928
 
1893
1929
  /* Light-scheme text ramp: grays were tuned against a near-white page and
1894
1930
  lose contrast over busy wallpapers — push them darker while active. */
1895
- body[${ACTIVE_ATTR}]:not([data-ds-dark-theme]) {
1931
+ body[${ACTIVE_ATTR}]:not([${DARK_ATTR}]) {
1896
1932
  --dsw-alias-label-primary: rgb(0, 0, 0);
1897
1933
  --dsw-alias-label-primary-dimmed: rgb(10, 10, 12);
1898
1934
  --dsw-alias-label-secondary: rgb(40, 42, 46);
@@ -1910,7 +1946,7 @@ window.__ModuleLoader__.load({
1910
1946
  --dsw-specific-input-major: rgba(255, 255, 255, 0.15);
1911
1947
  --dsw-specific-bubble: rgba(255, 255, 255, 0.12);
1912
1948
  }
1913
- body[data-ds-dark-theme][${ACTIVE_ATTR}] {
1949
+ body[${DARK_ATTR}][${ACTIVE_ATTR}] {
1914
1950
  --dsw-specific-input-major: rgba(255, 255, 255, 0.06);
1915
1951
  --dsw-specific-bubble: rgba(255, 255, 255, 0.05);
1916
1952
  }
@@ -2070,6 +2106,11 @@ window.__ModuleLoader__.load({
2070
2106
  display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2071
2107
  gap: 8px; max-height: 300px; overflow-y: auto; padding: 2px;
2072
2108
  }
2109
+ .webg-grid-empty {
2110
+ grid-column: 1 / -1;
2111
+ display: flex; align-items: center; justify-content: center;
2112
+ padding: 24px 12px; text-align: center;
2113
+ }
2073
2114
  .webg-card {
2074
2115
  position: relative; width: 100%; padding: 0; cursor: pointer;
2075
2116
  aspect-ratio: 16 / 9; display: block; overflow: hidden;
@@ -2146,7 +2187,10 @@ window.__ModuleLoader__.load({
2146
2187
  `;
2147
2188
 
2148
2189
  // ── Plugin exports ──────────────────────────────────────────────────────────
2149
- const inject = ["slots"];
2190
+ // The client runner (dsh-cordis-client-runner) rejects ctx.get() of any
2191
+ // service this plugin does not declare. slots registers the settings row,
2192
+ // locale drives the bilingual UI, theme drives the veil/scheme reactions.
2193
+ const inject = ["slots", "locale", "theme"];
2150
2194
 
2151
2195
  function apply(ctx) {
2152
2196
  // A plugin UPDATE re-applies this module WITHOUT re-evaluating it: the old
@@ -2174,6 +2218,42 @@ window.__ModuleLoader__.load({
2174
2218
  } catch { /* event bus unavailable → static language */ }
2175
2219
  }
2176
2220
 
2221
+ // 0b. Theme: follow the shell's theme service (snapshot.active.colorScheme
2222
+ // is "light"|"dark"; "theme/change" emits the new snapshot). Without
2223
+ // the service, fall back to the OS prefers-color-scheme media query.
2224
+ // A flip re-runs applyEffects() so the veil colour (black ↔ white) and
2225
+ // the DARK_ATTR mirror follow immediately.
2226
+ const themeSvc = typeof ctx.get === "function" ? ctx.get("theme") : undefined;
2227
+ if (themeSvc && typeof themeSvc.getTheme === "function") {
2228
+ try {
2229
+ const snap = themeSvc.getTheme();
2230
+ themeDark = Boolean(snap && snap.active && snap.active.colorScheme === "dark");
2231
+ } catch { /* keep previous state */ }
2232
+ if (typeof ctx.on === "function") {
2233
+ try {
2234
+ ctx.on("theme/change", (snap) => {
2235
+ const next = Boolean(snap && snap.active && snap.active.colorScheme === "dark");
2236
+ if (next !== themeDark) { themeDark = next; applyEffects(); }
2237
+ });
2238
+ } catch { /* event bus unavailable → static theme */ }
2239
+ }
2240
+ } else if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
2241
+ try {
2242
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
2243
+ themeDark = Boolean(mq && mq.matches);
2244
+ if (typeof mq.addEventListener === "function") {
2245
+ const onScheme = (e) => {
2246
+ const next = Boolean(e && e.matches);
2247
+ if (next !== themeDark) { themeDark = next; applyEffects(); }
2248
+ };
2249
+ mq.addEventListener("change", onScheme);
2250
+ if (typeof ctx.effect === "function") {
2251
+ ctx.effect(() => () => mq.removeEventListener("change", onScheme));
2252
+ }
2253
+ }
2254
+ } catch { /* no matchMedia → light default */ }
2255
+ }
2256
+
2177
2257
  // 1. Styles: owned by the fiber — injected on apply, REMOVED on dispose.
2178
2258
  if (ctx.effect && typeof document !== "undefined") {
2179
2259
  ctx.effect(() => {
@@ -2206,12 +2286,6 @@ window.__ModuleLoader__.load({
2206
2286
  applyEffects();
2207
2287
  }, 2500);
2208
2288
  }
2209
- // A theme switch flips the veil colour immediately (black ↔ white).
2210
- let observer = null;
2211
- if (typeof MutationObserver === "function") {
2212
- observer = new MutationObserver(() => applyEffects());
2213
- observer.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
2214
- }
2215
2289
  return () => {
2216
2290
  // Stop any in-flight inventory fetch from committing state (and
2217
2291
  // re-arming the rotation timer) after teardown began.
@@ -2222,7 +2296,6 @@ window.__ModuleLoader__.load({
2222
2296
  // removes the bar and its listeners) before tearing layers down.
2223
2297
  exitAdjust();
2224
2298
  if (samplerId !== null) window.clearInterval(samplerId);
2225
- if (observer) observer.disconnect();
2226
2299
  sampledLuminance = null;
2227
2300
  if (previewProbe) { previewProbe.remove(); previewProbe = null; }
2228
2301
  clearRotationTimer();
@@ -2241,6 +2314,7 @@ window.__ModuleLoader__.load({
2241
2314
  if (scrim) scrim.remove();
2242
2315
  clearEffects();
2243
2316
  document.body.removeAttribute(ACTIVE_ATTR);
2317
+ document.body.removeAttribute(DARK_ATTR);
2244
2318
  };
2245
2319
  });
2246
2320
  }
@@ -2323,7 +2397,7 @@ window.__ModuleLoader__.load({
2323
2397
  if (ctx.slots) {
2324
2398
  ctx.slots.inject("settings.general.item", () =>
2325
2399
  ctx.slots.register(
2326
- { name: "settings.general.item", id: "we-background", order: 500, label: S().settingsLabel },
2400
+ { name: "settings.general.item", id: "we-background", order: 500, label: () => S().settingsLabel },
2327
2401
  () => React.createElement(WallpaperPicker),
2328
2402
  ),
2329
2403
  );
package/lib/core.js CHANGED
@@ -174,8 +174,10 @@ export function steamProbeDirs(platform = process.platform, home = homedir) {
174
174
  return [
175
175
  'C:\\Program Files (x86)\\Steam',
176
176
  'C:\\Program Files\\Steam',
177
+ 'C:\\SteamLibrary',
177
178
  'D:\\Steam',
178
179
  'D:\\SteamLibrary',
180
+ 'E:\\Steam',
179
181
  'E:\\SteamLibrary',
180
182
  ];
181
183
  case 'darwin':
@@ -536,9 +538,13 @@ export function mimeFor(absPath) {
536
538
  js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
537
539
  css: 'text/css; charset=utf-8', json: 'application/json; charset=utf-8',
538
540
  jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
539
- png: 'image/png', webp: 'image/webp', svg: 'image/svg+xml',
541
+ png: 'image/png', webp: 'image/webp', svg: 'image/svg+xml', ico: 'image/x-icon',
540
542
  mp3: 'audio/mpeg', ogg: 'audio/ogg', wav: 'audio/wav',
543
+ flac: 'audio/flac', aac: 'audio/aac', m4a: 'audio/mp4',
541
544
  woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf',
545
+ otf: 'font/otf', eot: 'application/vnd.ms-fontobject',
546
+ wasm: 'application/wasm', txt: 'text/plain; charset=utf-8',
547
+ xml: 'application/xml; charset=utf-8',
542
548
  }[ext] || 'application/octet-stream';
543
549
  }
544
550
 
package/lib/index.js CHANGED
@@ -265,7 +265,7 @@ export function createRouteRegistrar(webServer, opts = {}) {
265
265
  // unguessable token remains the only capability — wildcard origin
266
266
  // grants nothing beyond what a known URL already grants.
267
267
  res.setHeader('Access-Control-Allow-Origin', '*');
268
- if (mime === 'text/html' || mime === 'image/svg+xml') {
268
+ if (mime.startsWith('text/html') || mime === 'image/svg+xml') {
269
269
  // Script-capable documents (web wallpapers, SVG previews) get a
270
270
  // sandbox CSP even on direct top-level navigation, so they can
271
271
  // never execute against the DSH origin. `allow-scripts` keeps the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallpaper-engine-dsh",
3
- "version": "1.0.7",
3
+ "version": "2.0.0",
4
4
  "description": "Wallpaper Engine library as the DSH web GUI background — video/web/still wallpapers, rotation lists, search, resource monitor, bilingual UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "client": {
55
55
  "inject": [
56
- "@deepseek-ai/dsh-client-runtime"
56
+ "@deepseek-ai/dsh-client-ui-settings-general"
57
57
  ],
58
58
  "platform": "web",
59
59
  "immediately": true
@@ -71,23 +71,28 @@
71
71
  "release:major": "npm version major -m \"chore: release v%s\" && git push --follow-tags"
72
72
  },
73
73
  "engines": {
74
+ "dsh": ">=0.2.1-rc.5",
74
75
  "node": ">=18.17"
75
76
  },
76
77
  "peerDependencies": {
77
78
  "@deepseek-ai/cordis": "^4.0.1",
78
- "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.6",
79
- "@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.6",
80
- "@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.5",
80
+ "@deepseek-ai/dsh-client-ui-settings-general": ">=0.2.1-rc.5",
81
+ "@deepseek-ai/dsh-client-ui-theme": ">=0.2.1-rc.5",
82
+ "@deepseek-ai/dsh-host-webserver": ">=0.2.1-rc.5",
81
83
  "react": "^18.2.0"
82
84
  },
83
85
  "peerDependenciesMeta": {
84
86
  "@deepseek-ai/cordis": {
85
87
  "optional": true
86
88
  },
87
- "@deepseek-ai/dsh-client-runtime": {
89
+ "@deepseek-ai/dsh-client-locale": {
88
90
  "optional": true
89
91
  },
90
- "@deepseek-ai/dsh-client-ui-slots": {
92
+ "@deepseek-ai/dsh-client-ui-settings-general": {
93
+ "optional": true
94
+ },
95
+ "@deepseek-ai/dsh-client-ui-theme": {
91
96
  "optional": true
92
97
  },
93
98
  "@deepseek-ai/dsh-host-webserver": {