dsh-xdpool 2.4.4 → 2.4.6

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/lib/client.js +144 -184
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,43 @@
1
1
  # Changelog
2
2
  本项目的所有重要变更都会记录在此文件。
3
3
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+ ## 2.4.6 — 扫码账号每次打开都空白:一份数据卡在两个细节里
5
+
6
+ > 2.4.4/2.4.5 之后侧栏能进、能滚了,但扫码过的渠道在账号池里经常整片空白,重开也一样。磁盘上账号是齐的(`state.json` 里 15 条),所以问题全在读取链。
7
+
8
+ ### 16 秒的事实抓取把首屏挡掉了
9
+
10
+ `load()` 里先 `await fetchChannelModels(...)` 再 `setEntries(...)`。那次采样式采到了 **16.5 秒**——它要把每个模型逐个交给宿主适配器解析上下文窗口,冷启动时很慢。卡片区在这十几秒里是空的,看起来就像「没有账号」。
11
+
12
+ 改成两阶段:`provider.status` 与 `account.list` 先回来(几十毫秒),**卡片立刻渲染**;事实(窗口/识图)随后用 `setEntries(previous => ...)` 合并进去。
13
+
14
+ ### 失败被渲染成了「空」
15
+
16
+ `provider.status` 是「哪些渠道有账号」的唯一判据。它一旦整段返回 401,`scanProviders` 就是空的,`entries` 也是空的,组件走到 `if (entries.length === 0 && error === undefined) return <></>`——**静默渲染空白,一个字都不说**。读者无法区分「还没扫码」和「读取失败」。
17
+
18
+ 现在把原始信封留下来判 `ok`:真的读失败就把错误显示出来。
19
+
20
+ ### RPC 桥只捕获一次
21
+
22
+ `callPack` 必须走 `ctx.connection.rpc`(裸 `fetch('/api/channel-pack')` 被宿主 401,因为 `/api/*` 只允许经 admission 的客户端)。但 `installPackRpc(ctx)` 在 `apply()` 里只跑一次,而提供 `connection` 的是另一个 fiber——那一刻没就绪,桥就永久为空,此后所有 `callPack` 都回落裸 fetch 吃 401,正好触发上一条的静默空白。
23
+
24
+ 改为保存 ctx、每次调用前惰性解析,就绪后自动恢复。每一步仍各自 try:宿主给客户端 ctx 套的 Proxy 对未声明服务的属性访问会**抛异常**(并上报运行失败),`?.` 挡不住抛异常的 getter,只有 `ctx.get()` 豁免。
25
+
26
+ ### 其他
27
+ - 全套 79 文件 1023 用例。
28
+ - `tests/aggregate-probe-regression.test.ts` 全量并行时会因争抢跑到 60 秒超时,单独跑 26 秒通过——既有测试,与本次改动无关。
29
+ ## 2.4.5 — 侧栏面板能滚动了
30
+
31
+ > 2.4.4 把账号池搬到侧栏那行之后,页面在折叠处被切掉、怎么都翻不到下半页。这不是新功能缺失,是我把设置页里的组件原样塞进了另一个容器,而**那个容器不滚动**。
32
+
33
+ ### 面板被裁切
34
+
35
+ `@deepseek-ai/dsh-client-ui-layout` 把中央列样式成 `display:flex; flex-direction:column; overflow:hidden`——设置对话框自己带滚动容器,所以同一个组件放在设置里能滚动,放进主区域就只能被裁掉。现在页面外面包一层 `.dsm-xdpool-page`(`height:100%; min-height:0; overflow:auto`),由它当滚动容器;顺带补上设置对话框原本提供的内边距,否则卡片会贴着窗口边缘。设置页那条路径**不受影响**:那里宿主已经在滚,再加一层会双份。
36
+
37
+ ### 其他
38
+
39
+ - 侧栏那行标签保持 `XD Pool`(中英文都不译)。
40
+
4
41
  ## 2.4.4 — 扫码登录后仍显示「登录已被拒绝」,以及侧栏入口
5
42
 
6
43
  > 两个都不算新功能:一个是**我上一版自己的代码**把刚扫的凭据判成「旧的」;另一个是入口位置——账号池一直只能从「设置」里进。
package/lib/client.js CHANGED
@@ -1670,39 +1670,19 @@ window.__ModuleLoader__.load({
1670
1670
  ):active:not(:disabled){transform:none}
1671
1671
  }
1672
1672
 
1673
- /* ── Docked entry beside the settings control ────────────────────────────────
1674
- The sidebar button and the overlay are two separate registrations with no
1675
- shared ancestor, so neither can carry the shell's own custom properties: the
1676
- easing curves are re-declared here rather than inherited. Everything below is
1677
- scoped to .dsm-xdpool-dock so it cannot reach into the host's sidebar. */
1678
- .dsm-xdpool-dock{--ease-instrument:cubic-bezier(.16,1,.3,1);position:relative;z-index:1;display:flex;align-items:center;gap:8px;width:100%;min-width:0;padding:6px 8px;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#c6c9d0);font:inherit;font-size:12px;line-height:17px;font-weight:560;text-align:left;cursor:pointer;overflow:hidden;transition:color .2s var(--ease-instrument),background .2s var(--ease-instrument),border-color .2s var(--ease-instrument)}
1679
- .dsm-xdpool-dock:hover{color:var(--dsw-alias-label-primary,#e8e8ea);background:color-mix(in srgb, var(--dsw-alias-label-primary,#e8e8ea) 7%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 70%, transparent)}
1680
- .dsm-xdpool-dock-on{color:var(--dsw-alias-label-primary,#e8e8ea);background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 14%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 40%, transparent)}
1681
- /* The sweep is the same gesture the card's own buttons make, so the dock reads
1682
- as part of the plugin rather than a host control. */
1683
- .dsm-xdpool-dock::after{content:"";position:absolute;top:0;bottom:0;left:-60%;width:42%;pointer-events:none;background:linear-gradient(100deg,transparent,rgb(255 255 255 / 12%),transparent);transform:skewX(-18deg);transition:left .5s var(--ease-instrument)}
1684
- .dsm-xdpool-dock:hover::after{left:118%}
1685
- .dsm-xdpool-dockicon{flex:none;width:16px;height:16px;background-color:currentColor;-webkit-mask-image:var(--dsm-xdpool-dock-glyph);mask-image:var(--dsm-xdpool-dock-glyph);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:16px 16px;mask-size:16px 16px}
1686
- .dsm-xdpool-docklabel{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1687
- /* No dark backdrop: the panel is a companion to the conversation, not a modal,
1688
- and dimming the whole host for a settings card reads as a page navigation.
1689
- Clicking outside still closes it — the scrim covers the viewport and is
1690
- transparent, so only the click is caught. */
1691
- .dsm-xdpool-dockscrim{position:fixed;inset:0;z-index:60;display:flex;align-items:stretch;justify-content:flex-end;background:transparent}
1692
- .dsm-xdpool-dockpanel{--ease-instrument:cubic-bezier(.16,1,.3,1);display:flex;flex-direction:column;width:min(760px, 92vw);margin:14px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 82%, transparent);border-radius:16px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#141517) 96%, transparent);box-shadow:0 20px 60px rgb(0 0 0 / 45%);overflow:hidden;animation:dsm-xdpool-dock-in .3s var(--ease-instrument)}
1693
- @keyframes dsm-xdpool-dock-in{from{opacity:0;transform:translateX(18px)}to{opacity:1;transform:none}}
1694
- .dsm-xdpool-dockhead{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l1,#ffffff14) 80%, transparent)}
1695
- .dsm-xdpool-docktitle{flex:1 1 auto;font-size:12px;line-height:17px;font-weight:640;color:var(--dsw-alias-label-primary,#e8e8ea)}
1696
- .dsm-xdpool-dockclose{appearance:none;flex:none;width:22px;height:22px;padding:0;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--dsw-alias-label-tertiary,#8a90a0);font:inherit;font-size:15px;line-height:1;cursor:pointer;transition:color .16s ease,background .16s ease}
1697
- .dsm-xdpool-dockclose:hover{color:var(--dsw-alias-label-primary,#e8e8ea);background:color-mix(in srgb, var(--dsw-alias-label-primary,#e8e8ea) 9%, transparent)}
1698
- /* The card inside the panel was built for the settings dialog's width; scroll
1699
- the panel body, never the card, so its own sticky header keeps working. */
1700
- .dsm-xdpool-dockbody{flex:1 1 auto;min-height:0;overflow:auto;padding:14px}
1701
- @media (prefers-reduced-motion:reduce){
1702
- .dsm-xdpool-dock::after{display:none}
1703
- .dsm-xdpool-dockpanel{animation:none}
1704
- }
1705
-
1673
+ `.trim();
1674
+ /**
1675
+ * Frame for hosting the pool in the main column instead of the settings dialog.
1676
+ *
1677
+ * `.centerCol` is `overflow:hidden`, so this element is the scrollport; without
1678
+ * it the page clips at the fold. The horizontal inset is asymmetric on purpose —
1679
+ * the sidebar sits on the left of this column, so the left edge needs less air
1680
+ * than the right one to look centred.
1681
+ */
1682
+ const PAGE_CSS = `
1683
+ .dsm-xdpool-page{flex:1 1 auto;height:100%;min-height:0;overflow:auto;box-sizing:border-box;padding:20px 28px 34px}
1684
+ .dsm-xdpool-page > .dsm-xdpool-shell{margin:0 auto}
1685
+ @media (max-width:720px){.dsm-xdpool-page{padding:14px 16px 26px}}
1706
1686
  `.trim();
1707
1687
  //#endregion
1708
1688
  //#region src/client/avatar.ts
@@ -8967,6 +8947,25 @@ window.__ModuleLoader__.load({
8967
8947
  let packRpcBridge;
8968
8948
  /** Capture the client ctx's connection service for {@link callPack}. */
8969
8949
  function installPackRpc(ctx) {
8950
+ packRpcContext = ctx;
8951
+ packRpcBridge = resolvePackRpcBridge();
8952
+ }
8953
+ /** The client ctx captured by {@link installPackRpc}, for late resolution. */
8954
+ let packRpcContext;
8955
+ /**
8956
+ * Read `connection.rpc.call` off the captured ctx, or `undefined` if the
8957
+ * service is still absent.
8958
+ *
8959
+ * Each step gets its own try. The host hands the client ctx a Proxy that
8960
+ * THROWS on direct property access to an undeclared service
8961
+ * (cordis-client-runner's dynamic ctx), and that throw also reports the run as
8962
+ * failed — so one shared try/catch would let a rejected `ctx.connection` skip
8963
+ * the exempt `ctx.get()` below and silently leave the bridge unset. `?.` does
8964
+ * not help: it guards null/undefined, not a throwing getter.
8965
+ */
8966
+ function resolvePackRpcBridge() {
8967
+ const ctx = packRpcContext;
8968
+ if (ctx === void 0 || ctx === null) return void 0;
8970
8969
  let connection;
8971
8970
  try {
8972
8971
  connection = ctx.connection;
@@ -8980,8 +8979,8 @@ window.__ModuleLoader__.load({
8980
8979
  }
8981
8980
  try {
8982
8981
  const call = connection?.rpc?.call;
8983
- if (typeof call !== "function") return;
8984
- packRpcBridge = { call: async (method, payload) => {
8982
+ if (typeof call !== "function") return void 0;
8983
+ return { call: async (method, payload) => {
8985
8984
  const bound = call.bind(connection);
8986
8985
  const controller = new AbortController();
8987
8986
  const timer = setTimeout(() => controller.abort(), 9e4);
@@ -8995,7 +8994,7 @@ window.__ModuleLoader__.load({
8995
8994
  }
8996
8995
  } };
8997
8996
  } catch {
8998
- packRpcBridge = void 0;
8997
+ return;
8999
8998
  }
9000
8999
  }
9001
9000
  /**
@@ -11689,7 +11688,8 @@ window.__ModuleLoader__.load({
11689
11688
  const load = async () => {
11690
11689
  try {
11691
11690
  const channels = (await fetchChannels()).filter((channel) => !REGION_OWNED_CHANNELS.has(channel.id));
11692
- const statuses = providerStatusesFrom(await callPack("provider.status", { providers: channels.map((channel) => channel.id) }));
11691
+ const statusAnswer = await callPack("provider.status", { providers: channels.map((channel) => channel.id) });
11692
+ const statuses = providerStatusesFrom(statusAnswer);
11693
11693
  const scanProviders = channels.map((channel) => channel.id).filter((id) => (statuses.get(id)?.accountsTotal ?? 0) > 0);
11694
11694
  const perProvider = await Promise.all(scanProviders.map(async (provider) => {
11695
11695
  const wantBalances = channels.find((channel) => channel.id === provider)?.credits !== void 0;
@@ -11718,18 +11718,28 @@ window.__ModuleLoader__.load({
11718
11718
  }];
11719
11719
  }));
11720
11720
  const byProvider = new Map(perProvider);
11721
- const idsByProvider = {};
11722
- for (const [provider, entry] of perProvider) if (entry.models.length > 0) idsByProvider[provider] = entry.models.map((model) => model.id);
11723
- const facts = await fetchChannelModels(scanProviders, idsByProvider).catch(() => /* @__PURE__ */ new Map());
11724
11721
  const next = channels.filter((channel) => byProvider.has(channel.id)).map((channel) => ({
11725
11722
  channel,
11726
11723
  ...byProvider.get(channel.id),
11727
- facts: new Map((facts.get(channel.id) ?? []).map((model) => [model.id, model])),
11724
+ facts: /* @__PURE__ */ new Map(),
11728
11725
  status: statuses.get(channel.id)
11729
11726
  }));
11730
11727
  if (!mountedRef.current) return;
11731
11728
  setEntries(next);
11732
11729
  setError(void 0);
11730
+ if (next.length === 0) {
11731
+ if (!statusAnswer.ok) setError(statusAnswer.error?.message ?? "provider.status refused");
11732
+ return;
11733
+ }
11734
+ const idsByProvider = {};
11735
+ for (const [provider, entry] of perProvider) if (entry.models.length > 0) idsByProvider[provider] = entry.models.map((model) => model.id);
11736
+ const facts = await fetchChannelModels(next.map((entry) => entry.channel.id), idsByProvider).catch(() => /* @__PURE__ */ new Map());
11737
+ if (!mountedRef.current) return;
11738
+ if (facts.size === 0) return;
11739
+ setEntries((previous) => previous.map((entry) => ({
11740
+ ...entry,
11741
+ facts: new Map((facts.get(entry.channel.id) ?? []).map((model) => [model.id, model]))
11742
+ })));
11733
11743
  } catch (cause) {
11734
11744
  if (!mountedRef.current) return;
11735
11745
  setError(cause instanceof Error ? cause.message : String(cause));
@@ -12358,7 +12368,7 @@ window.__ModuleLoader__.load({
12358
12368
  *
12359
12369
  * @module dsh-xdpool/version
12360
12370
  */
12361
- const POOL_VERSION = "2.4.4";
12371
+ const POOL_VERSION = "2.4.6";
12362
12372
  //#endregion
12363
12373
  //#region src/client/PoolCard.tsx
12364
12374
  /**
@@ -14732,15 +14742,33 @@ window.__ModuleLoader__.load({
14732
14742
  * `fill-rule="evenodd"` cuts the interior notches out of the silhouette so the
14733
14743
  * three layers stay distinguishable at that size.
14734
14744
  */
14745
+ /**
14746
+ * The mark's geometry, independent of any wrapper element.
14747
+ *
14748
+ * Kept separate from {@link POOL_NAV_ICON_SVG} because the two consumers need
14749
+ * different frames: the settings nav row masks a standalone document, while the
14750
+ * sidebar panel row renders paths into an `svg` the shell sizes for it (16px
14751
+ * wide, 18px in the collapsed rail). Sharing the paths — rather than the whole
14752
+ * document, parsed back out with a regex — is what keeps the two placements from
14753
+ * drifting apart when the artwork changes.
14754
+ */
14755
+ const POOL_NAV_ICON_PATHS = [{
14756
+ fill: "currentColor",
14757
+ fillRule: "evenodd",
14758
+ d: "M12 2.6a3.1 3.1 0 1 1 0 6.2 3.1 3.1 0 0 1 0-6.2Zm0 1.7a1.4 1.4 0 1 0 0 2.8 1.4 1.4 0 0 0 0-2.8ZM6.6 8.9a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2Zm0 1.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM17.4 8.9a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2Zm0 1.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"
14759
+ }, {
14760
+ fill: "none",
14761
+ stroke: "currentColor",
14762
+ strokeWidth: 1.7,
14763
+ strokeLinecap: "round",
14764
+ strokeDasharray: "2.6 2.2",
14765
+ d: "M4.4 17.2a8.6 8.6 0 0 0 15.2 0"
14766
+ }];
14767
+ /** The same mark as a standalone SVG document, for `mask-image`. */
14735
14768
  const POOL_NAV_ICON_SVG = [
14736
14769
  "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" width=\"16\" height=\"16\">",
14737
- "<path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"",
14738
- "M12 2.6a3.1 3.1 0 1 1 0 6.2 3.1 3.1 0 0 1 0-6.2Zm0 1.7a1.4 1.4 0 1 0 0 2.8 1.4 1.4 0 0 0 0-2.8Z",
14739
- "M6.6 8.9a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2Zm0 1.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z",
14740
- "M17.4 8.9a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2Zm0 1.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z",
14741
- "\"/>",
14742
- "<path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\"",
14743
- " d=\"M4.4 17.2a8.6 8.6 0 0 0 15.2 0\" stroke-dasharray=\"2.6 2.2\"/>",
14770
+ `<path fill="currentColor" fill-rule="evenodd" d="${POOL_NAV_ICON_PATHS[0].d}"/>`,
14771
+ `<path fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" d="${POOL_NAV_ICON_PATHS[1].d}" stroke-dasharray="2.6 2.2"/>`,
14744
14772
  "</svg>"
14745
14773
  ].join("");
14746
14774
  /**
@@ -14752,135 +14780,67 @@ window.__ModuleLoader__.load({
14752
14780
  */
14753
14781
  const POOL_NAV_ICON_MASK_URL = `data:image/svg+xml;utf8,${encodeURIComponent(POOL_NAV_ICON_SVG)}`;
14754
14782
  //#endregion
14755
- //#region src/client/dock.tsx
14783
+ //#region src/client/panel-icon.tsx
14756
14784
  /**
14757
- * The docked entry: a `sidebar.footer.action` button that opens the pool card in
14758
- * a `shell.overlay` panel.
14759
- *
14760
- * Why not link to the Settings page the card already lives on: the host's
14761
- * sidebar slot hands a registrant exactly `{ wide }` — no `openSettings`, no
14762
- * navigation seat. dsh-purge, dshmarket and everything else third-party solve it
14763
- * the same way, by owning the surface: a button that toggles an overlay of its
14764
- * own. Re-implementing the card for the overlay would mean two copies to keep in
14765
- * step, so the overlay renders the very same `PoolCard` the settings section
14766
- * renders, page state and all.
14767
- *
14768
- * The open flag lives at module scope rather than in React state because the
14769
- * button and the panel are two separate slot registrations with no common
14770
- * ancestor; module state plus a subscription is the only channel between them.
14785
+ * Render the pool's mark.
14786
+ *
14787
+ * Inlined rather than masked from a data URI because the shell gives the glyph an
14788
+ * 18px box in the collapsed rail: a `mask-size: 16px` silhouette would read as
14789
+ * smaller than the first-party icons beside it, and scaling the mask would blur
14790
+ * its 1.7px arc. Both placements share {@link POOL_NAV_ICON_PATHS}, so the artwork
14791
+ * cannot drift between the settings nav row and this one.
14771
14792
  */
14772
- /** Subscribers re-read {@link dockedOpen}; the setter notifies them. */
14773
- const listeners = /* @__PURE__ */ new Set();
14774
- let dockedOpen = false;
14775
- /** Read whether the docked panel is showing. */
14776
- function isDockedOpen() {
14777
- return dockedOpen;
14778
- }
14779
- /** Show or hide the docked panel, notifying every registrant. */
14780
- function setDockedOpen(open) {
14781
- if (dockedOpen === open) return;
14782
- dockedOpen = open;
14783
- for (const listener of [...listeners]) listener();
14784
- }
14785
- /** Subscribe to {@link setDockedOpen}. Returns the unsubscribe. */
14786
- function subscribeDockedOpen(listener) {
14787
- listeners.add(listener);
14788
- return () => {
14789
- listeners.delete(listener);
14790
- };
14791
- }
14792
- /** Subscribe a component to the open flag. */
14793
- function useDockedOpen() {
14794
- const [open, setOpen] = (0, react.useState)(isDockedOpen);
14795
- (0, react.useEffect)(() => {
14796
- setOpen(isDockedOpen());
14797
- return subscribeDockedOpen(() => {
14798
- setOpen(isDockedOpen());
14799
- });
14800
- }, []);
14801
- return open;
14793
+ function PanelIcon({ size }) {
14794
+ const edge = typeof size === "number" && size > 0 ? size : 16;
14795
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
14796
+ xmlns: "http://www.w3.org/2000/svg",
14797
+ viewBox: "0 0 24 24",
14798
+ width: edge,
14799
+ height: edge,
14800
+ "aria-hidden": "true",
14801
+ focusable: "false",
14802
+ children: POOL_NAV_ICON_PATHS.map((shape, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { ...shape }, index))
14803
+ });
14802
14804
  }
14805
+ //#endregion
14806
+ //#region src/client/page.tsx
14803
14807
  /**
14804
- * The sidebar button.
14808
+ * The wrapper that makes the pool usable as a *sidebar panel*.
14805
14809
  *
14806
- * Renders a glyph and, only when the rail is wide, the product name: a label in
14807
- * the collapsed 56px rail is clipped mid-word, which reads as a layout bug.
14810
+ * Two problems it solves, both invisible from inside `PoolShell`:
14811
+ *
14812
+ * 1. Scrolling. The settings dialog owns a scroll container, so a page dropped
14813
+ * into `settings.plugin.item` can be as tall as it likes. The main column
14814
+ * cannot: `@deepseek-ai/dsh-client-ui-layout` styles `.centerCol` as
14815
+ * `display:flex; flex-direction:column; overflow:hidden`, and the slot is
14816
+ * rendered into that box with no scrolling of its own. Without a scrollport
14817
+ * here the page simply clips — which is what happens if you register
14818
+ * `PoolCard` into the `main` slot directly. The reference community plugin
14819
+ * solves it the same way, with `height:100%; min-height:0; overflow:auto` on
14820
+ * its page root.
14821
+ *
14822
+ * 2. Insets. The settings dialog supplies padding around the card; the main
14823
+ * column supplies none, so the same markup arrives flush against the window
14824
+ * edge. The padding below restores the frame the dialog used to provide.
14825
+ *
14826
+ * `PAGE_CSS` is injected here rather than living in `SHARED_CARD_CSS` because
14827
+ * the stylesheets in `PoolShell` are rendered *inside* the shell — this wrapper
14828
+ * is its parent, so nothing from that subtree could style it.
14829
+ *
14830
+ * @module dsh-xdpool/client/page
14808
14831
  */
14809
- function SidebarEntry({ wide }) {
14810
- const open = useDockedOpen();
14811
- const label = "XD Pool";
14812
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
14813
- type: "button",
14814
- className: `dsm-xdpool-dock${open ? " dsm-xdpool-dock-on" : ""}`,
14815
- title: open ? "收起 XD Pool" : "打开 XD Pool",
14816
- "aria-expanded": open,
14817
- "aria-label": label,
14818
- style: { "--dsm-xdpool-dock-glyph": `url("${POOL_NAV_ICON_MASK_URL}")` },
14819
- onClick: () => {
14820
- setDockedOpen(!isDockedOpen());
14821
- },
14822
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14823
- className: "dsm-xdpool-dockicon",
14824
- "aria-hidden": "true"
14825
- }), wide === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14826
- className: "dsm-xdpool-docklabel",
14827
- children: label
14828
- }) : null]
14829
- });
14830
- }
14831
14832
  /**
14832
- * The panel, rendered into `shell.overlay`.
14833
+ * `PoolCard`, sized and scrolled for the main column.
14833
14834
  *
14834
- * `children` is the card itself, passed in rather than imported so this module
14835
- * stays free of the card's dependency graph (the overlay must not drag the
14836
- * settings scope in with it when the host line has no scope to hand out).
14835
+ * Kept as a separate component instead of an option on `PoolCard` so the
14836
+ * settings registration stays exactly what it was: the dialog already provides
14837
+ * both the scrollport and the insets, and adding either again would double
14838
+ * them.
14837
14839
  */
14838
- function DockedPanel({ children }) {
14839
- const open = useDockedOpen();
14840
- (0, react.useEffect)(() => {
14841
- if (!open) return;
14842
- const onKeyDown = (event) => {
14843
- if (event.key === "Escape") setDockedOpen(false);
14844
- };
14845
- window.addEventListener("keydown", onKeyDown);
14846
- return () => {
14847
- window.removeEventListener("keydown", onKeyDown);
14848
- };
14849
- }, [open]);
14850
- if (!open) return null;
14851
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14852
- className: "dsm-xdpool-dockscrim",
14853
- role: "presentation",
14854
- onClick: () => {
14855
- setDockedOpen(false);
14856
- },
14857
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14858
- className: "dsm-xdpool-dockpanel",
14859
- role: "dialog",
14860
- "aria-modal": "false",
14861
- "aria-label": "XD Pool",
14862
- onClick: (event) => {
14863
- event.stopPropagation();
14864
- },
14865
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14866
- className: "dsm-xdpool-dockhead",
14867
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
14868
- className: "dsm-xdpool-docktitle",
14869
- children: "XD Pool"
14870
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
14871
- type: "button",
14872
- className: "dsm-xdpool-dockclose",
14873
- "aria-label": "关闭",
14874
- onClick: () => {
14875
- setDockedOpen(false);
14876
- },
14877
- children: "×"
14878
- })]
14879
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
14880
- className: "dsm-xdpool-dockbody",
14881
- children
14882
- })]
14883
- })
14840
+ function PoolPage(props) {
14841
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
14842
+ className: "dsm-xdpool-page",
14843
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: PAGE_CSS }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PoolCard, { ...props })]
14884
14844
  });
14885
14845
  }
14886
14846
  //#endregion
@@ -15961,22 +15921,22 @@ window.__ModuleLoader__.load({
15961
15921
  }, PoolCard));
15962
15922
  installNavIcon(ctx, () => t("row.navLabel"));
15963
15923
  try {
15964
- ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
15965
- name: "sidebar.footer.action",
15966
- id: "xdpool-dock",
15967
- order: 60,
15968
- label: "XD Pool"
15969
- }, SidebarEntry));
15970
- ctx.slots.inject("shell.overlay", () => ctx.slots.register({
15971
- name: "shell.overlay",
15972
- id: "xdpool-dock-panel",
15973
- order: 60
15974
- }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DockedPanel, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PoolCard, { ...settingsScope === void 0 ? { t } : {
15975
- t,
15976
- settingsScope
15977
- } }) })));
15924
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
15925
+ name: "sidebar.panellist",
15926
+ id: "xdpool",
15927
+ order: 20,
15928
+ label: () => t("row.navLabel")
15929
+ }, PanelIcon));
15930
+ ctx.slots.inject("main", () => ctx.slots.register({
15931
+ name: "main",
15932
+ key: "xdpool",
15933
+ inject: () => settingsScope === void 0 ? { t } : {
15934
+ t,
15935
+ settingsScope
15936
+ }
15937
+ }, PoolPage));
15978
15938
  } catch (error) {
15979
- console.error("[dsh-xdpool] docked entry unavailable (settings page unaffected):", error);
15939
+ console.error("[dsh-xdpool] sidebar entry unavailable (settings page unaffected):", error);
15980
15940
  }
15981
15941
  } catch (error) {
15982
15942
  console.error("[dsh-xdpool] client page failed to load (host provider unaffected):", error);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-xdpool",
3
3
  "displayName": "DSH XD Pool",
4
4
  "description": "One DSH model pool for every free quota you have: merge all locally signed-in WorkBuddy accounts with auto-failover, sign in to 13 vendors by QR/web, aggregate 76 vendors' free tiers, plus a usage dashboard, model routing, a local OpenAI-compatible gateway and a points-automation suite.",
5
- "version": "2.4.4",
5
+ "version": "2.4.6",
6
6
  "license": "MIT",
7
7
  "author": "XDTrees",
8
8
  "homepage": "https://github.com/XDTrees/dsh-xdpool#readme",