dsh-xdpool 2.4.6 → 2.5.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/CHANGELOG.md +124 -0
- package/README.en.md +11 -2
- package/README.md +12 -3
- package/assets/community-qr.png +0 -0
- package/cordis.patch.yml +17 -41
- package/lib/bin.js +218 -21
- package/lib/client.js +277 -624
- package/lib/index.d.ts +54 -82
- package/lib/index.js +299 -386
- package/package.json +1 -4
- package/vendor/channel-pack/pack.js +27 -5
package/lib/client.js
CHANGED
|
@@ -162,22 +162,6 @@ window.__ModuleLoader__.load({
|
|
|
162
162
|
*/
|
|
163
163
|
const POOL_RELAY_PATH = "/plugins/dsh-xdpool/relay";
|
|
164
164
|
/**
|
|
165
|
-
* What the locally running billion-context proxy is doing, read-only.
|
|
166
|
-
*
|
|
167
|
-
* Its own route because the answer comes from ANOTHER plugin: billion-context
|
|
168
|
-
* runs its own loopback listener and publishes its own JSON, so this route only
|
|
169
|
-
* finds that listener and relays a filtered subset. Nothing here is folded into
|
|
170
|
-
* {@link POOL_STATUS_PATH}: the pool card polls that every 30 seconds, and a
|
|
171
|
-
* third-party proxy's session table is not something every pool poll should
|
|
172
|
-
* carry.
|
|
173
|
-
*
|
|
174
|
-
* The word "read-only" is load-bearing on both ends. This plugin sends GETs
|
|
175
|
-
* only — it never configures, restarts or writes to the proxy — and the payload
|
|
176
|
-
* it relays deliberately omits every field that could carry conversation
|
|
177
|
-
* material.
|
|
178
|
-
*/
|
|
179
|
-
const POOL_BILLION_PATH = "/plugins/dsh-xdpool/billion";
|
|
180
|
-
/**
|
|
181
165
|
* The pack's own management route, which the card posts to directly.
|
|
182
166
|
*
|
|
183
167
|
* Not a re-export of anything: the vendored bundle registers this path itself
|
|
@@ -1111,6 +1095,31 @@ window.__ModuleLoader__.load({
|
|
|
1111
1095
|
.dsm-xdpool-star svg{fill:currentColor;transition:transform .5s cubic-bezier(.34,1.56,.64,1)}
|
|
1112
1096
|
.dsm-xdpool-star:hover svg{transform:rotate(72deg) scale(1.12)}
|
|
1113
1097
|
.dsm-xdpool-star:active svg{transform:rotate(72deg) scale(.94)}
|
|
1098
|
+
/* The group button, next to the star, sharing the brand row.
|
|
1099
|
+
The nav is a grid of 1fr auto; a second control in the same cell would
|
|
1100
|
+
STACK on top of the star rather than sit beside it (grid items in one cell
|
|
1101
|
+
overlap). So the track list grows to three: brand | star | group, and both
|
|
1102
|
+
buttons are pinned to row 1 on their own columns. Auto-placement alone puts
|
|
1103
|
+
them under the tab strip, because the strip comes first in the DOM. */
|
|
1104
|
+
.dsm-xdpool-nav{grid-template-columns:1fr auto auto}
|
|
1105
|
+
.dsm-xdpool-star{grid-column:2;grid-row:1}
|
|
1106
|
+
.dsm-xdpool-group{grid-column:3;grid-row:1;margin-left:6px;cursor:pointer;font:inherit;font-size:12.5px;line-height:17px;font-weight:650;color:color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 82%, var(--dsw-alias-label-secondary,#c6c9d0));border-color:color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 88%, transparent);background:linear-gradient(140deg, color-mix(in srgb, var(--dsw-alias-bg-layer-3,#23252b) 72%, transparent), transparent 70%)}
|
|
1107
|
+
.dsm-xdpool-group:hover{transform:translateY(-1px);color:var(--dsw-alias-state-business-primary,#5686fe);box-shadow:0 0 0 4px color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 9%, transparent), 0 8px 22px rgb(0 0 0 / 22%)}
|
|
1108
|
+
/* The pulse is on the icon only, and slow: this button is a door, not a call to
|
|
1109
|
+
action, and a card that nags is a card people mute. */
|
|
1110
|
+
.dsm-xdpool-group:hover svg{transform:scale(1.1)}
|
|
1111
|
+
.dsm-xdpool-group svg{fill:currentColor;transition:transform .28s cubic-bezier(.34,1.56,.64,1)}
|
|
1112
|
+
/* The invite dialog. Positioned against the shell (which is the containing
|
|
1113
|
+
block) so it covers the card and nothing of the host dialog around it. */
|
|
1114
|
+
.dsm-xdpool-groupmask{position:absolute;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;padding:16px;border-radius:16px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#191a1e) 72%, transparent)}
|
|
1115
|
+
.dsm-xdpool-groupcard{display:flex;flex-direction:column;align-items:center;gap:10px;max-width:340px;padding:16px 18px 18px;border-radius:16px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 86%, transparent);background:var(--dsw-alias-bg-layer-2,#1c1e22);box-shadow:0 18px 48px rgb(0 0 0 / 34%), inset 0 1px 0 rgb(255 255 255 / 7%)}
|
|
1116
|
+
.dsm-xdpool-groupcardhead{display:flex;align-items:center;gap:10px;width:100%;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary,#e8e8ea)}
|
|
1117
|
+
.dsm-xdpool-groupclose{margin-left:auto;appearance:none;border:none;background:transparent;padding:0 4px;font:inherit;font-size:18px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8a90a0);cursor:pointer;border-radius:8px}
|
|
1118
|
+
.dsm-xdpool-groupclose:hover{color:var(--dsw-alias-label-primary,#e8e8ea)}
|
|
1119
|
+
/* A white plate under the code, always: a QR inverted onto a dark panel scans
|
|
1120
|
+
on some phones and not others, and there is no way to tell which from here. */
|
|
1121
|
+
.dsm-xdpool-groupqr{display:block;width:260px;height:260px;border-radius:12px;background:#fff;padding:6px;object-fit:contain}
|
|
1122
|
+
.dsm-xdpool-groupnote{width:100%;margin:0;font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-tertiary,#8a90a0);text-align:center}
|
|
1114
1123
|
.dsm-xdpool-navtabs{grid-column:1/-1;position:relative;display:flex;flex-wrap:wrap;gap:2px;padding:3px;border-radius:13px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l1,#ffffff14) 80%, transparent);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#191a1e) 62%, transparent);box-shadow:inset 0 1px 2px rgb(0 0 0 / 4%)}
|
|
1115
1124
|
/* A raised surface, not a tinted wash: the glider has to read as "this one is
|
|
1116
1125
|
sitting on top of the strip" from across the panel, and a wash alone is
|
|
@@ -1158,29 +1167,6 @@ window.__ModuleLoader__.load({
|
|
|
1158
1167
|
/* Long model ids and account emails get clipped rather than wrapped: a wrapped
|
|
1159
1168
|
number column breaks the alignment that makes it scannable. */
|
|
1160
1169
|
|
|
1161
|
-
`.trim();
|
|
1162
|
-
const BILLION_CARD_CSS = `
|
|
1163
|
-
|
|
1164
|
-
.dsm-xdpool-steps{margin:0;padding-left:20px;display:flex;flex-direction:column;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
1165
|
-
.dsm-xdpool-steps code{font-family:var(--dsw-alias-font-mono,ui-monospace,Menlo,Consolas,monospace);font-size:11px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-3,#26282e) 80%, transparent);border-radius:6px;padding:2px 6px}
|
|
1166
|
-
/* A session with requests still in flight. The dot is the only motion on this
|
|
1167
|
-
page; nothing animates while idle, so a still dot means still idle. */
|
|
1168
|
-
.dsm-xdpool-live{display:inline-block;width:6px;height:6px;border-radius:50%;margin-left:6px;vertical-align:middle;background:var(--dsw-alias-state-success,#22c55e)}
|
|
1169
|
-
/* Section header that carries its own controls: title on the left, actions on
|
|
1170
|
-
the right, both on one baseline. */
|
|
1171
|
-
.dsm-xdpool-sechead{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
|
1172
|
-
.dsm-xdpool-sechead .dsm-xdpool-sectitle{margin:0}
|
|
1173
|
-
.dsm-xdpool-secbtns{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
1174
|
-
/* A control that reads as a link-shaped button: it only navigates or reveals,
|
|
1175
|
-
it never commits state, so it stays quiet until hovered. */
|
|
1176
|
-
.dsm-xdpool-secghost{appearance:none;display:inline-flex;align-items:center;font:inherit;font-size:11.5px;font-weight:560;line-height:17px;padding:3px 10px;border-radius:8px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#36373b) 85%, transparent);background:color-mix(in srgb, var(--dsw-alias-bg-layer-3,#2a2c33) 70%, transparent);color:var(--dsw-alias-label-secondary,#c6c9d0);text-decoration:none;cursor:pointer;white-space:nowrap;transition:border-color .16s ease,color .16s ease,background .16s ease,transform .18s var(--ease-instrument)}
|
|
1177
|
-
.dsm-xdpool-secghost:hover{border-color:color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 66%, var(--dsw-alias-border-l2,#36373b));color:var(--dsw-alias-label-primary,#e8e8ea);transform:translateY(-1px)}
|
|
1178
|
-
.dsm-xdpool-secghost:focus-visible{outline:2px solid color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 78%, transparent);outline-offset:2px;box-shadow:0 0 0 4px color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 13%, transparent)}
|
|
1179
|
-
/* The session id itself. A deep link into another plugin's UI, so it has to
|
|
1180
|
-
read as a link without pretending to be the pool's own navigation. */
|
|
1181
|
-
.dsm-xdpool-sesslink{color:color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 88%, var(--dsw-alias-label-primary,#e8e8ea));text-decoration:none;border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 30%, transparent);transition:color .16s ease,border-color .16s ease}
|
|
1182
|
-
.dsm-xdpool-sesslink:hover{color:var(--dsw-alias-brand-primary,#5686fe);border-bottom-color:color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 70%, transparent)}
|
|
1183
|
-
|
|
1184
1170
|
`.trim();
|
|
1185
1171
|
const ROUTING_CARD_CSS = `
|
|
1186
1172
|
|
|
@@ -1369,9 +1355,8 @@ window.__ModuleLoader__.load({
|
|
|
1369
1355
|
.dsm-btn-primary:hover:not(:disabled){opacity:.9}
|
|
1370
1356
|
/* The borderless third of the button family: links out to a vendor's console
|
|
1371
1357
|
and the destructive "forget this key" action. It had no rule at all, so the
|
|
1372
|
-
aggregate row's three links
|
|
1373
|
-
|
|
1374
|
-
looking like the primary one. */
|
|
1358
|
+
aggregate row's three links rendered as bare browser buttons — which is how a
|
|
1359
|
+
secondary action ends up looking like the primary one. */
|
|
1375
1360
|
.dsm-btn-ghost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-tertiary,#8a90a0)}
|
|
1376
1361
|
.dsm-btn-ghost:hover:not(:disabled){border-color:var(--dsw-alias-border-l2,#36373b);color:var(--dsw-alias-label-primary,#e8e8ea);background:rgba(255,255,255,.04);text-decoration:none}
|
|
1377
1362
|
.dsm-xdpool-panel-empty{color:var(--dsw-alias-label-tertiary,#999);font-size:14px;line-height:20px}
|
|
@@ -1704,6 +1689,10 @@ window.__ModuleLoader__.load({
|
|
|
1704
1689
|
*/
|
|
1705
1690
|
const POOL_CARD_AVATAR = "data:image/jpeg;base64,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";
|
|
1706
1691
|
//#endregion
|
|
1692
|
+
//#region src/client/community-qr.ts
|
|
1693
|
+
/** The group's invite code, as a data URI. */
|
|
1694
|
+
const COMMUNITY_QR_DATA_URI = "data:image/png;base64,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";
|
|
1695
|
+
//#endregion
|
|
1707
1696
|
//#region src/client/shell.tsx
|
|
1708
1697
|
/**
|
|
1709
1698
|
* The card shell: brand bar, page tabs, per-page hero.
|
|
@@ -1730,7 +1719,8 @@ window.__ModuleLoader__.load({
|
|
|
1730
1719
|
* highlight on the correct button at the correct x, in the wrong row.
|
|
1731
1720
|
*
|
|
1732
1721
|
* 3. **Nothing here is a gate.** The star button opens the repository in a new
|
|
1733
|
-
* tab and nothing else
|
|
1722
|
+
* tab and nothing else, and the group button beside it opens a dialog with
|
|
1723
|
+
* the WeChat invite code. A settings card that can refuse to render is a card
|
|
1734
1724
|
* that, on a machine without outbound network, looks exactly like a broken
|
|
1735
1725
|
* plugin.
|
|
1736
1726
|
*
|
|
@@ -1824,6 +1814,26 @@ window.__ModuleLoader__.load({
|
|
|
1824
1814
|
function PoolShell({ t, version, avatar, tabs, active, onSelect, hero, children }) {
|
|
1825
1815
|
/** Measured box of the active tab's button, in tab-strip coordinates. */
|
|
1826
1816
|
const [glider, setGlider] = (0, react.useState)(void 0);
|
|
1817
|
+
/** Whether the group-invite dialog is open. Local: nothing outside needs it. */
|
|
1818
|
+
const [groupOpen, setGroupOpen] = (0, react.useState)(false);
|
|
1819
|
+
/**
|
|
1820
|
+
* Close the dialog on Escape.
|
|
1821
|
+
*
|
|
1822
|
+
* The overlay's own click handler covers the pointer, but a keyboard user who
|
|
1823
|
+
* opened this with Enter has no other way out — and a modal with no key exit
|
|
1824
|
+
* is a trap. Bound only while open, so the listener is not paid for on every
|
|
1825
|
+
* page of a card that is mostly not showing a dialog.
|
|
1826
|
+
*/
|
|
1827
|
+
(0, react.useEffect)(() => {
|
|
1828
|
+
if (!groupOpen) return void 0;
|
|
1829
|
+
const onKey = (event) => {
|
|
1830
|
+
if (event.key === "Escape") setGroupOpen(false);
|
|
1831
|
+
};
|
|
1832
|
+
window.addEventListener("keydown", onKey);
|
|
1833
|
+
return () => {
|
|
1834
|
+
window.removeEventListener("keydown", onKey);
|
|
1835
|
+
};
|
|
1836
|
+
}, [groupOpen]);
|
|
1827
1837
|
const tabRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
1828
1838
|
const tabsRef = (0, react.useRef)(null);
|
|
1829
1839
|
const measure = (0, react.useCallback)(() => {
|
|
@@ -1903,6 +1913,28 @@ window.__ModuleLoader__.load({
|
|
|
1903
1913
|
})
|
|
1904
1914
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("shell.star") ?? "Star on GitHub" })]
|
|
1905
1915
|
}),
|
|
1916
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1917
|
+
type: "button",
|
|
1918
|
+
className: "dsm-xdpool-star dsm-xdpool-group",
|
|
1919
|
+
title: t?.("shell.groupHint") ?? "扫码加入 XD 交流群",
|
|
1920
|
+
onClick: () => {
|
|
1921
|
+
setGroupOpen(true);
|
|
1922
|
+
},
|
|
1923
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1924
|
+
width: "15",
|
|
1925
|
+
height: "15",
|
|
1926
|
+
viewBox: "0 0 24 24",
|
|
1927
|
+
"aria-hidden": "true",
|
|
1928
|
+
focusable: "false",
|
|
1929
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1930
|
+
fill: "currentColor",
|
|
1931
|
+
d: "M9.1 3.4c-3.6 0-6.5 2.4-6.5 5.4 0 1.7 1 3.2 2.6 4.2l-.7 2.1 2.4-1.2c.7.2 1.4.3 2.2.3h.4a5.6 5.6 0 0 1-.2-1.5c0-3 2.9-5.4 6.5-5.4h.5C15.7 5.1 12.7 3.4 9.1 3.4zm-2.3 2.9a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zm4.6 0a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8z"
|
|
1932
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1933
|
+
fill: "currentColor",
|
|
1934
|
+
d: "M21.4 12.7c0-2.5-2.4-4.5-5.4-4.5s-5.4 2-5.4 4.5 2.4 4.5 5.4 4.5c.6 0 1.2-.1 1.8-.2l2 1-.6-1.7c1.4-.9 2.2-2.2 2.2-3.6zm-7.2-1.4a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6zm3.6 0a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6z"
|
|
1935
|
+
})]
|
|
1936
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("shell.group") ?? "交流群" })]
|
|
1937
|
+
}),
|
|
1906
1938
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1907
1939
|
className: "dsm-xdpool-navtabs",
|
|
1908
1940
|
role: "tablist",
|
|
@@ -1937,6 +1969,46 @@ window.__ModuleLoader__.load({
|
|
|
1937
1969
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1938
1970
|
className: "dsm-xdpool-body",
|
|
1939
1971
|
children
|
|
1972
|
+
}),
|
|
1973
|
+
!groupOpen ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1974
|
+
className: "dsm-xdpool-groupmask",
|
|
1975
|
+
role: "dialog",
|
|
1976
|
+
"aria-modal": "true",
|
|
1977
|
+
"aria-label": t?.("shell.groupTitle") ?? "XD 交流群",
|
|
1978
|
+
onClick: () => {
|
|
1979
|
+
setGroupOpen(false);
|
|
1980
|
+
},
|
|
1981
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1982
|
+
className: "dsm-xdpool-groupcard",
|
|
1983
|
+
onClick: (event) => {
|
|
1984
|
+
event.stopPropagation();
|
|
1985
|
+
},
|
|
1986
|
+
children: [
|
|
1987
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1988
|
+
className: "dsm-xdpool-groupcardhead",
|
|
1989
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t?.("shell.groupTitle") ?? "XD 交流群" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1990
|
+
type: "button",
|
|
1991
|
+
className: "dsm-xdpool-groupclose",
|
|
1992
|
+
"aria-label": t?.("shell.groupClose") ?? "关闭",
|
|
1993
|
+
onClick: () => {
|
|
1994
|
+
setGroupOpen(false);
|
|
1995
|
+
},
|
|
1996
|
+
children: "×"
|
|
1997
|
+
})]
|
|
1998
|
+
}),
|
|
1999
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
2000
|
+
className: "dsm-xdpool-groupqr",
|
|
2001
|
+
src: COMMUNITY_QR_DATA_URI,
|
|
2002
|
+
alt: t?.("shell.groupAlt") ?? "微信群二维码:用微信扫一扫加入 XD 交流群",
|
|
2003
|
+
width: 260,
|
|
2004
|
+
height: 260
|
|
2005
|
+
}),
|
|
2006
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2007
|
+
className: "dsm-xdpool-groupnote",
|
|
2008
|
+
children: t?.("shell.groupNote") ?? "用微信「扫一扫」加入 XD 交流群。二维码 7 天内有效,过期了在 GitHub 提 issue 说一声,会更新。"
|
|
2009
|
+
})
|
|
2010
|
+
]
|
|
2011
|
+
})
|
|
1940
2012
|
})
|
|
1941
2013
|
]
|
|
1942
2014
|
});
|
|
@@ -1986,7 +2058,7 @@ window.__ModuleLoader__.load({
|
|
|
1986
2058
|
*/
|
|
1987
2059
|
/** How many recent calls the detail table lists. */
|
|
1988
2060
|
const DETAIL_ROWS = 12;
|
|
1989
|
-
function fmtInt
|
|
2061
|
+
function fmtInt(value) {
|
|
1990
2062
|
return value.toLocaleString("en-US");
|
|
1991
2063
|
}
|
|
1992
2064
|
/** Milliseconds as `1.2s` / `340ms`, because `1200ms` is not a unit anyone reads. */
|
|
@@ -1995,7 +2067,7 @@ window.__ModuleLoader__.load({
|
|
|
1995
2067
|
}
|
|
1996
2068
|
/** Tokens per second, kept integer: 12.4 tok/s reads as false precision. */
|
|
1997
2069
|
function fmtTps(value) {
|
|
1998
|
-
return value === null ? "—" : `${fmtInt
|
|
2070
|
+
return value === null ? "—" : `${fmtInt(value)}`;
|
|
1999
2071
|
}
|
|
2000
2072
|
function fmtClock(at) {
|
|
2001
2073
|
const d = new Date(at);
|
|
@@ -2035,9 +2107,9 @@ window.__ModuleLoader__.load({
|
|
|
2035
2107
|
"—"
|
|
2036
2108
|
];
|
|
2037
2109
|
return [
|
|
2038
|
-
fmtInt
|
|
2039
|
-
fmtInt
|
|
2040
|
-
fmtInt
|
|
2110
|
+
fmtInt(call.usage.inputTokens),
|
|
2111
|
+
fmtInt(call.usage.outputTokens),
|
|
2112
|
+
fmtInt(call.usage.cachedTokens)
|
|
2041
2113
|
];
|
|
2042
2114
|
}
|
|
2043
2115
|
function LedgerPage({ t, usage: shared }) {
|
|
@@ -2159,20 +2231,20 @@ window.__ModuleLoader__.load({
|
|
|
2159
2231
|
children: [
|
|
2160
2232
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
2161
2233
|
label: t?.("ledger.tokens") ?? "Tokens",
|
|
2162
|
-
value: summary.usageReported ? fmtInt
|
|
2234
|
+
value: summary.usageReported ? fmtInt(totals?.input ?? 0) : t?.("ledger.notReported") ?? "not reported",
|
|
2163
2235
|
sub: summary.usageReported ? t?.("ledger.tokenSplit", {
|
|
2164
|
-
input: fmtInt
|
|
2165
|
-
output: fmtInt
|
|
2166
|
-
}) ?? `in ${fmtInt
|
|
2236
|
+
input: fmtInt(totals?.input ?? 0),
|
|
2237
|
+
output: fmtInt(totals?.output ?? 0)
|
|
2238
|
+
}) ?? `in ${fmtInt(totals?.input ?? 0)} · out ${fmtInt(totals?.output ?? 0)}` : t?.("ledger.notReportedHint") ?? "the gateway ignored stream_options.include_usage"
|
|
2167
2239
|
}),
|
|
2168
2240
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
2169
2241
|
label: t?.("ledger.turns") ?? "Turns",
|
|
2170
|
-
value: fmtInt
|
|
2242
|
+
value: fmtInt(summary.turns),
|
|
2171
2243
|
sub: t?.("ledger.callsSub", { calls: summary.calls }) ?? `${summary.calls} upstream calls`
|
|
2172
2244
|
}),
|
|
2173
2245
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
2174
2246
|
label: t?.("ledger.speed") ?? "Speed",
|
|
2175
|
-
value: totals?.avgTps === null || totals?.avgTps === void 0 ? "—" : `${fmtInt
|
|
2247
|
+
value: totals?.avgTps === null || totals?.avgTps === void 0 ? "—" : `${fmtInt(totals.avgTps)} tok/s`,
|
|
2176
2248
|
sub: totals?.avgTtftMs === null || totals?.avgTtftMs === void 0 ? t?.("ledger.speedNone") ?? "no timing reported" : t?.("ledger.ttft", { ms: fmtMs(totals.avgTtftMs) }) ?? `ttft ${fmtMs(totals.avgTtftMs)}`
|
|
2177
2249
|
}),
|
|
2178
2250
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
@@ -2181,11 +2253,11 @@ window.__ModuleLoader__.load({
|
|
|
2181
2253
|
const rate = cacheHitRate(summary);
|
|
2182
2254
|
return rate === null || !summary.usageReported ? "—" : `${Math.round(rate * 100)}%`;
|
|
2183
2255
|
})(),
|
|
2184
|
-
sub: summary.usageReported ? t?.("ledger.cacheSub", { cached: fmtInt
|
|
2256
|
+
sub: summary.usageReported ? t?.("ledger.cacheSub", { cached: fmtInt(summary.rows.reduce((n, r) => n + r.cachedTokens, 0)) }) ?? "" : ""
|
|
2185
2257
|
}),
|
|
2186
2258
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
2187
2259
|
label: t?.("ledger.requests") ?? "Requests",
|
|
2188
|
-
value: fmtInt
|
|
2260
|
+
value: fmtInt(summary.calls),
|
|
2189
2261
|
sub: summary.failedTurns === 0 ? t?.("ledger.allOk") ?? "no turn failed outright" : t?.("ledger.failedSub", {
|
|
2190
2262
|
failed: summary.failedTurns,
|
|
2191
2263
|
recovered: summary.recoveredTurns
|
|
@@ -2292,19 +2364,19 @@ window.__ModuleLoader__.load({
|
|
|
2292
2364
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: regionLabel(row.region, t) }),
|
|
2293
2365
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2294
2366
|
className: "dsm-xdpool-num",
|
|
2295
|
-
children: fmtInt
|
|
2367
|
+
children: fmtInt(row.turns)
|
|
2296
2368
|
}),
|
|
2297
2369
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2298
2370
|
className: "dsm-xdpool-num",
|
|
2299
|
-
children: fmtInt
|
|
2371
|
+
children: fmtInt(row.calls)
|
|
2300
2372
|
}),
|
|
2301
2373
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2302
2374
|
className: "dsm-xdpool-num",
|
|
2303
|
-
children: fmtInt
|
|
2375
|
+
children: fmtInt(row.inputTokens)
|
|
2304
2376
|
}),
|
|
2305
2377
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2306
2378
|
className: "dsm-xdpool-num",
|
|
2307
|
-
children: fmtInt
|
|
2379
|
+
children: fmtInt(row.outputTokens)
|
|
2308
2380
|
}),
|
|
2309
2381
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2310
2382
|
className: "dsm-xdpool-ellipsis",
|
|
@@ -2354,7 +2426,7 @@ window.__ModuleLoader__.load({
|
|
|
2354
2426
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: regionLabel(row.region, t) }),
|
|
2355
2427
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2356
2428
|
className: "dsm-xdpool-num",
|
|
2357
|
-
children: fmtInt
|
|
2429
|
+
children: fmtInt(row.calls)
|
|
2358
2430
|
}),
|
|
2359
2431
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2360
2432
|
className: "dsm-xdpool-num",
|
|
@@ -9251,29 +9323,6 @@ window.__ModuleLoader__.load({
|
|
|
9251
9323
|
}
|
|
9252
9324
|
return rows;
|
|
9253
9325
|
}
|
|
9254
|
-
function creditStatusesFrom(envelope) {
|
|
9255
|
-
const value = envelope.value;
|
|
9256
|
-
if (value === null || typeof value !== "object") return [];
|
|
9257
|
-
const accounts = value.accounts;
|
|
9258
|
-
if (!Array.isArray(accounts)) return [];
|
|
9259
|
-
const rows = [];
|
|
9260
|
-
for (const entry of accounts) {
|
|
9261
|
-
if (entry === null || typeof entry !== "object") continue;
|
|
9262
|
-
const accountId = entry.accountId;
|
|
9263
|
-
if (typeof accountId !== "string" || accountId === "") continue;
|
|
9264
|
-
const nickname = entry.nickname;
|
|
9265
|
-
const status = entry.status;
|
|
9266
|
-
const statusRow = status !== null && typeof status === "object" ? status : void 0;
|
|
9267
|
-
const todayCredit = statusRow?.["todayCredit"];
|
|
9268
|
-
rows.push({
|
|
9269
|
-
accountId,
|
|
9270
|
-
...typeof nickname === "string" && nickname !== "" ? { nickname } : {},
|
|
9271
|
-
...statusRow?.["todayCheckedIn"] === true ? { todayCheckedIn: true } : {},
|
|
9272
|
-
...typeof todayCredit === "number" && Number.isFinite(todayCredit) ? { todayCredit } : {}
|
|
9273
|
-
});
|
|
9274
|
-
}
|
|
9275
|
-
return rows;
|
|
9276
|
-
}
|
|
9277
9326
|
function autoCheckinFrom(envelope) {
|
|
9278
9327
|
const value = envelope.value;
|
|
9279
9328
|
if (value === null || typeof value !== "object") return void 0;
|
|
@@ -9886,429 +9935,6 @@ window.__ModuleLoader__.load({
|
|
|
9886
9935
|
});
|
|
9887
9936
|
}
|
|
9888
9937
|
//#endregion
|
|
9889
|
-
//#region src/client/BillionPage.tsx
|
|
9890
|
-
/**
|
|
9891
|
-
* The billion-context page: what the compressor on this machine is doing.
|
|
9892
|
-
*
|
|
9893
|
-
* This page observes; it does not compress. billion-context is a separate
|
|
9894
|
-
* plugin that very likely already runs here — on a stock desktop profile it is
|
|
9895
|
-
* mounted by the profile's bundle layer and listens on its own loopback port.
|
|
9896
|
-
* Everything below is read from that proxy's own JSON, relayed through
|
|
9897
|
-
* `src/billion.ts`, and nothing on this page can change what the proxy does.
|
|
9898
|
-
*
|
|
9899
|
-
* Three states, three different things to say, and the difference matters more
|
|
9900
|
-
* than it looks:
|
|
9901
|
-
*
|
|
9902
|
-
* - **missing** — no proxy recorded. Normal on a machine that never installed
|
|
9903
|
-
* it. The page shows the install command instead of an empty table, because
|
|
9904
|
-
* an empty table here would be indistinguishable from "compression ran and
|
|
9905
|
-
* saved nothing".
|
|
9906
|
-
* - **unreachable** — a proxy IS recorded but did not answer. Most often a
|
|
9907
|
-
* restart in progress: the origin file is rewritten on every respawn. This
|
|
9908
|
-
* must not read as an empty dashboard.
|
|
9909
|
-
* - **ready** — the numbers.
|
|
9910
|
-
*
|
|
9911
|
-
* Every figure is the proxy's own. Nothing is re-derived, and in particular the
|
|
9912
|
-
* cache hit rate below is weighted by the sessions actually shown rather than
|
|
9913
|
-
* averaged across them: a page that averaged a 200-request session with a
|
|
9914
|
-
* 1-request one would report a number that describes neither.
|
|
9915
|
-
*
|
|
9916
|
-
* WHAT THIS PAGE DOES NOT REDRAW
|
|
9917
|
-
*
|
|
9918
|
-
* billion-context ships its own dashboard, including a per-session detail view.
|
|
9919
|
-
* The rows below therefore link out to it rather than re-implementing those
|
|
9920
|
-
* screens, and the table itself starts collapsed: a fleet number is what this
|
|
9921
|
-
* page is for, and 50 rows of session ids is a wall the user did not ask for.
|
|
9922
|
-
* Both are deliberate — one answer, and the summary stays the summary.
|
|
9923
|
-
*
|
|
9924
|
-
* @module dsh-xdpool/client/BillionPage
|
|
9925
|
-
*/
|
|
9926
|
-
/** Poll cadence, matching the other pages that watch a live process. */
|
|
9927
|
-
const POLL_MS$1 = 15e3;
|
|
9928
|
-
function fmtInt(value) {
|
|
9929
|
-
return value.toLocaleString("en-US");
|
|
9930
|
-
}
|
|
9931
|
-
/** A token count in millions or billions, because 486,201,338 stops being a number. */
|
|
9932
|
-
function compact$1(value) {
|
|
9933
|
-
if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
|
9934
|
-
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
9935
|
-
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
9936
|
-
return String(value);
|
|
9937
|
-
}
|
|
9938
|
-
/** `288.6K tokens`, or an em dash when the proxy has no measurement. */
|
|
9939
|
-
function tokens(value) {
|
|
9940
|
-
return value === 0 ? "—" : `${compact$1(value)} tokens`;
|
|
9941
|
-
}
|
|
9942
|
-
/** Local clock, or an em dash when the proxy never saw a request. */
|
|
9943
|
-
function fmtLastSeen(iso) {
|
|
9944
|
-
if (iso === "") return "—";
|
|
9945
|
-
const at = Date.parse(iso);
|
|
9946
|
-
if (Number.isNaN(at)) return "—";
|
|
9947
|
-
const d = new Date(at);
|
|
9948
|
-
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
|
|
9949
|
-
}
|
|
9950
|
-
/**
|
|
9951
|
-
* Fleet cache hit rate, or null when nothing was measured.
|
|
9952
|
-
*
|
|
9953
|
-
* Weighted by input tokens rather than averaged per session, and null — never
|
|
9954
|
-
* zero — when no input was counted at all. The proxy reports `cacheHitPct: -1`
|
|
9955
|
-
* for exactly that case, and rendering it as `0%` would claim the cache never
|
|
9956
|
-
* helped, which is the opposite of what "not measured" means.
|
|
9957
|
-
*/
|
|
9958
|
-
function fleetHitRate(sessions) {
|
|
9959
|
-
let input = 0;
|
|
9960
|
-
let cached = 0;
|
|
9961
|
-
for (const row of sessions) {
|
|
9962
|
-
if (row.cacheHitPct < 0) continue;
|
|
9963
|
-
input += row.inputTokens + row.cachedTokens;
|
|
9964
|
-
cached += row.cachedTokens;
|
|
9965
|
-
}
|
|
9966
|
-
return input === 0 ? null : cached / input;
|
|
9967
|
-
}
|
|
9968
|
-
/**
|
|
9969
|
-
* A link into the proxy's own dashboard, or undefined when there is nothing to
|
|
9970
|
-
* link to.
|
|
9971
|
-
*
|
|
9972
|
-
* Two things are checked here rather than trusted. The origin has to be an
|
|
9973
|
-
* absolute http(s) URL, because a relative one would resolve against the HOST
|
|
9974
|
-
* and silently open the wrong page; and the session id is percent-encoded,
|
|
9975
|
-
* because these are proxy-chosen identifiers that may contain characters a
|
|
9976
|
-
* hash route cannot carry raw.
|
|
9977
|
-
*
|
|
9978
|
-
* The hash is the proxy's own route shape (`#/sessions/<id>`), read from its
|
|
9979
|
-
* shipped router — a deep link into someone else's UI, which is the whole
|
|
9980
|
-
* reason the table links instead of redrawing.
|
|
9981
|
-
*/
|
|
9982
|
-
function proxyLink(origin, hash) {
|
|
9983
|
-
if (typeof origin !== "string" || origin === "") return void 0;
|
|
9984
|
-
let base;
|
|
9985
|
-
try {
|
|
9986
|
-
base = new URL(origin);
|
|
9987
|
-
} catch {
|
|
9988
|
-
return;
|
|
9989
|
-
}
|
|
9990
|
-
if (base.protocol !== "http:" && base.protocol !== "https:") return void 0;
|
|
9991
|
-
const url = new URL("/__bili/", base);
|
|
9992
|
-
url.hash = hash;
|
|
9993
|
-
return url.toString();
|
|
9994
|
-
}
|
|
9995
|
-
/**
|
|
9996
|
-
* Open the proxy's dashboard in a new tab.
|
|
9997
|
-
*
|
|
9998
|
-
* `_blank` with `noopener` for the same reason the channels page does it: the
|
|
9999
|
-
* proxy owns that tab afterwards, and this page must not be able to reach back
|
|
10000
|
-
* into it. The settings panel itself is never navigated away from.
|
|
10001
|
-
*/
|
|
10002
|
-
function openProxyDashboard(url) {
|
|
10003
|
-
window.open(url, "_blank", "noopener");
|
|
10004
|
-
}
|
|
10005
|
-
function BillionPage({ t }) {
|
|
10006
|
-
const [doc, setDoc] = (0, react.useState)(void 0);
|
|
10007
|
-
const [failed, setFailed] = (0, react.useState)(false);
|
|
10008
|
-
const mounted = (0, react.useRef)(true);
|
|
10009
|
-
(0, react.useEffect)(() => () => {
|
|
10010
|
-
mounted.current = false;
|
|
10011
|
-
}, []);
|
|
10012
|
-
const load = (0, react.useCallback)(async (signal) => {
|
|
10013
|
-
try {
|
|
10014
|
-
const response = await fetch(POOL_BILLION_PATH, {
|
|
10015
|
-
headers: { accept: "application/json" },
|
|
10016
|
-
credentials: "same-origin",
|
|
10017
|
-
...signal === void 0 ? {} : { signal }
|
|
10018
|
-
});
|
|
10019
|
-
if (!response.ok) {
|
|
10020
|
-
if (mounted.current) setFailed(true);
|
|
10021
|
-
return;
|
|
10022
|
-
}
|
|
10023
|
-
const value = await response.json().catch(() => void 0);
|
|
10024
|
-
if (mounted.current && signal?.aborted !== true) {
|
|
10025
|
-
setDoc(value);
|
|
10026
|
-
setFailed(false);
|
|
10027
|
-
}
|
|
10028
|
-
} catch {
|
|
10029
|
-
if (mounted.current && signal?.aborted !== true) setFailed(true);
|
|
10030
|
-
}
|
|
10031
|
-
}, []);
|
|
10032
|
-
(0, react.useEffect)(() => {
|
|
10033
|
-
const controller = new AbortController();
|
|
10034
|
-
load(controller.signal);
|
|
10035
|
-
const timer = window.setInterval(() => {
|
|
10036
|
-
load(controller.signal);
|
|
10037
|
-
}, POLL_MS$1);
|
|
10038
|
-
return () => {
|
|
10039
|
-
window.clearInterval(timer);
|
|
10040
|
-
controller.abort();
|
|
10041
|
-
};
|
|
10042
|
-
}, [load]);
|
|
10043
|
-
/**
|
|
10044
|
-
* Fleet totals over the rows on screen.
|
|
10045
|
-
*
|
|
10046
|
-
* Summing the rows this page will render is what keeps the KPI row from
|
|
10047
|
-
* disagreeing with the table under it — the same rule the pool dashboard
|
|
10048
|
-
* follows, and for the same reason: a dashboard that gets trusted must not be
|
|
10049
|
-
* able to contradict itself.
|
|
10050
|
-
*/
|
|
10051
|
-
const totals = (0, react.useMemo)(() => {
|
|
10052
|
-
const sessions = doc?.sessions ?? [];
|
|
10053
|
-
let requests = 0;
|
|
10054
|
-
let input = 0;
|
|
10055
|
-
let cached = 0;
|
|
10056
|
-
let output = 0;
|
|
10057
|
-
let saved = 0;
|
|
10058
|
-
let live = 0;
|
|
10059
|
-
for (const row of sessions) {
|
|
10060
|
-
requests += row.requests;
|
|
10061
|
-
input += row.inputTokens;
|
|
10062
|
-
cached += row.cachedTokens;
|
|
10063
|
-
output += row.outputTokens;
|
|
10064
|
-
saved += row.tokensSaved;
|
|
10065
|
-
if (row.live) live += 1;
|
|
10066
|
-
}
|
|
10067
|
-
return {
|
|
10068
|
-
sessions,
|
|
10069
|
-
requests,
|
|
10070
|
-
input,
|
|
10071
|
-
cached,
|
|
10072
|
-
output,
|
|
10073
|
-
saved,
|
|
10074
|
-
live
|
|
10075
|
-
};
|
|
10076
|
-
}, [doc]);
|
|
10077
|
-
const hitRate = (0, react.useMemo)(() => fleetHitRate(totals.sessions), [totals.sessions]);
|
|
10078
|
-
const ready = doc?.state === "ready";
|
|
10079
|
-
/**
|
|
10080
|
-
* The rows start hidden, and the user opens them on purpose.
|
|
10081
|
-
*
|
|
10082
|
-
* The summary above is what this page is for; the fleet table is the
|
|
10083
|
-
* detail behind it. Collapsed is also the honest default on a machine with 50
|
|
10084
|
-
* sessions, where an expanded table pushes every other control off screen.
|
|
10085
|
-
*/
|
|
10086
|
-
const [rowsOpen, setRowsOpen] = (0, react.useState)(false);
|
|
10087
|
-
const dashboard = proxyLink(doc?.origin, "#/overview");
|
|
10088
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
10089
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: BILLION_CARD_CSS }),
|
|
10090
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageHero, {
|
|
10091
|
-
title: t?.("billion.title") ?? "billion-context",
|
|
10092
|
-
sub: t?.("billion.sub") ?? "Read-only view of the compressor running on this machine. This plugin never configures it.",
|
|
10093
|
-
actions: dashboard === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10094
|
-
type: "button",
|
|
10095
|
-
className: "dsm-btn dsm-btn-ghost",
|
|
10096
|
-
onClick: () => {
|
|
10097
|
-
openProxyDashboard(dashboard);
|
|
10098
|
-
},
|
|
10099
|
-
children: t?.("billion.openDashboard") ?? "Open its dashboard"
|
|
10100
|
-
}),
|
|
10101
|
-
pills: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10102
|
-
className: "dsm-xdpool-pill",
|
|
10103
|
-
"data-tone": ready ? "on" : "off",
|
|
10104
|
-
children: ready ? t?.("billion.pillRunning", { version: doc?.version ?? "?" }) ?? `running · v${doc?.version ?? "?"}` : t?.("billion.pillNotRunning") ?? "not running"
|
|
10105
|
-
}), ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10106
|
-
className: "dsm-xdpool-pill",
|
|
10107
|
-
"data-tone": "off",
|
|
10108
|
-
children: t?.("billion.pillReadOnly") ?? "read-only"
|
|
10109
|
-
}) : null] })
|
|
10110
|
-
}),
|
|
10111
|
-
failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10112
|
-
className: "dsm-xdpool-note",
|
|
10113
|
-
role: "alert",
|
|
10114
|
-
children: t?.("billion.failed") ?? "Could not reach the pool relay. The pool itself is unaffected."
|
|
10115
|
-
}) : null,
|
|
10116
|
-
doc === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10117
|
-
className: "dsm-xdpool-note",
|
|
10118
|
-
children: t?.("billion.loading") ?? "Looking for a billion-context proxy on this machine…"
|
|
10119
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [doc.state !== "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
10120
|
-
className: "dsm-xdpool-sec",
|
|
10121
|
-
children: [
|
|
10122
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
10123
|
-
className: "dsm-xdpool-sectitle",
|
|
10124
|
-
children: doc.state === "missing" ? t?.("billion.notInstalled") ?? "billion-context is not installed here" : t?.("billion.notAnswering") ?? "billion-context did not answer"
|
|
10125
|
-
}),
|
|
10126
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10127
|
-
className: "dsm-xdpool-note",
|
|
10128
|
-
children: doc.reason ?? ""
|
|
10129
|
-
}),
|
|
10130
|
-
doc.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
|
|
10131
|
-
className: "dsm-xdpool-steps",
|
|
10132
|
-
children: [
|
|
10133
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", { children: [
|
|
10134
|
-
"dsh plugin --profile ",
|
|
10135
|
-
dshProfileHint(t),
|
|
10136
|
-
" add billion-context"
|
|
10137
|
-
] }) }),
|
|
10138
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("billion.stepRestart") ?? "Restart DSH so the profile bundle layer mounts it." }),
|
|
10139
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("billion.stepBack") ?? "Come back to this page; it fills in on its own." })
|
|
10140
|
-
]
|
|
10141
|
-
}) : null
|
|
10142
|
-
]
|
|
10143
|
-
}) : null, ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10144
|
-
className: "dsm-xdpool-kpis",
|
|
10145
|
-
children: [
|
|
10146
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
10147
|
-
label: t?.("billion.sessions") ?? "Sessions",
|
|
10148
|
-
value: fmtInt(doc.totalSessions),
|
|
10149
|
-
sub: totals.live === 0 ? t?.("billion.noLive") ?? "none active right now" : t?.("billion.liveSub", { count: totals.live }) ?? `${totals.live} active right now`
|
|
10150
|
-
}),
|
|
10151
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
10152
|
-
label: t?.("billion.requests") ?? "Requests",
|
|
10153
|
-
value: fmtInt(totals.requests),
|
|
10154
|
-
sub: t?.("billion.requestsSub") ?? "through the proxy, all lanes"
|
|
10155
|
-
}),
|
|
10156
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
10157
|
-
label: t?.("billion.saved") ?? "Tokens saved",
|
|
10158
|
-
value: compact$1(totals.saved),
|
|
10159
|
-
sub: t?.("billion.savedSub") ?? "by compression, net of what it cost"
|
|
10160
|
-
}),
|
|
10161
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
10162
|
-
label: t?.("billion.tokens") ?? "Tokens",
|
|
10163
|
-
value: compact$1(totals.input),
|
|
10164
|
-
sub: t?.("billion.tokensSub", {
|
|
10165
|
-
cached: compact$1(totals.cached),
|
|
10166
|
-
out: compact$1(totals.output)
|
|
10167
|
-
}) ?? `${compact$1(totals.cached)} cached · ${compact$1(totals.output)} out`
|
|
10168
|
-
}),
|
|
10169
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
|
|
10170
|
-
label: t?.("billion.cache") ?? "Cache hit",
|
|
10171
|
-
value: hitRate === null ? "—" : `${Math.round(hitRate * 100)}%`,
|
|
10172
|
-
sub: hitRate === null ? t?.("billion.cacheNone") ?? "nothing measured yet" : t?.("billion.cacheSub") ?? "of everything sent"
|
|
10173
|
-
})
|
|
10174
|
-
]
|
|
10175
|
-
}), totals.sessions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10176
|
-
className: "dsm-xdpool-note",
|
|
10177
|
-
children: t?.("billion.empty") ?? "The proxy is up but has seen no requests yet. Chat and this fills in."
|
|
10178
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
10179
|
-
className: "dsm-xdpool-sec",
|
|
10180
|
-
children: [
|
|
10181
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10182
|
-
className: "dsm-xdpool-sechead",
|
|
10183
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
10184
|
-
className: "dsm-xdpool-sectitle",
|
|
10185
|
-
children: t?.("billion.perSession") ?? "Per session"
|
|
10186
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10187
|
-
className: "dsm-xdpool-secbtns",
|
|
10188
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10189
|
-
type: "button",
|
|
10190
|
-
className: "dsm-xdpool-secghost",
|
|
10191
|
-
"aria-expanded": rowsOpen,
|
|
10192
|
-
onClick: () => {
|
|
10193
|
-
setRowsOpen((open) => !open);
|
|
10194
|
-
},
|
|
10195
|
-
children: rowsOpen ? t?.("billion.hideRows") ?? "Hide rows" : t?.("billion.showRows", { count: totals.sessions.length }) ?? `Show ${totals.sessions.length} rows`
|
|
10196
|
-
}), dashboard === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
10197
|
-
className: "dsm-xdpool-secghost",
|
|
10198
|
-
href: dashboard,
|
|
10199
|
-
target: "_blank",
|
|
10200
|
-
rel: "noopener noreferrer",
|
|
10201
|
-
children: t?.("billion.openDashboard") ?? "Open the proxy's own dashboard"
|
|
10202
|
-
})]
|
|
10203
|
-
})]
|
|
10204
|
-
}),
|
|
10205
|
-
rowsOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10206
|
-
className: "dsm-xdpool-tablewrap",
|
|
10207
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
10208
|
-
className: "dsm-xdpool-table",
|
|
10209
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
10210
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colSession") ?? "Session" }),
|
|
10211
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colLast") ?? "Last seen" }),
|
|
10212
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colModel") ?? "Model" }),
|
|
10213
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
10214
|
-
className: "dsm-xdpool-num",
|
|
10215
|
-
children: t?.("billion.colRequests") ?? "Requests"
|
|
10216
|
-
}),
|
|
10217
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
10218
|
-
className: "dsm-xdpool-num",
|
|
10219
|
-
children: t?.("billion.colContext") ?? "Context"
|
|
10220
|
-
}),
|
|
10221
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
10222
|
-
className: "dsm-xdpool-num",
|
|
10223
|
-
children: t?.("billion.colBlocks") ?? "Blocks"
|
|
10224
|
-
}),
|
|
10225
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
10226
|
-
className: "dsm-xdpool-num",
|
|
10227
|
-
children: t?.("billion.colSaved") ?? "Saved"
|
|
10228
|
-
}),
|
|
10229
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
10230
|
-
className: "dsm-xdpool-num",
|
|
10231
|
-
children: t?.("billion.colCache") ?? "Cache"
|
|
10232
|
-
})
|
|
10233
|
-
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: totals.sessions.map((row) => {
|
|
10234
|
-
const detail = proxyLink(doc?.origin, `#/sessions/${encodeURIComponent(row.id)}`);
|
|
10235
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
10236
|
-
"data-live": row.live,
|
|
10237
|
-
children: [
|
|
10238
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
|
|
10239
|
-
className: "dsm-xdpool-ellipsis",
|
|
10240
|
-
title: row.id,
|
|
10241
|
-
children: [detail === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: row.id }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
10242
|
-
className: "dsm-xdpool-sesslink",
|
|
10243
|
-
href: detail,
|
|
10244
|
-
target: "_blank",
|
|
10245
|
-
rel: "noopener noreferrer",
|
|
10246
|
-
title: row.id,
|
|
10247
|
-
children: row.id
|
|
10248
|
-
}), row.live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10249
|
-
className: "dsm-xdpool-live",
|
|
10250
|
-
"aria-label": "live"
|
|
10251
|
-
}) : null]
|
|
10252
|
-
}),
|
|
10253
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtLastSeen(row.lastSeen) }),
|
|
10254
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
10255
|
-
className: "dsm-xdpool-ellipsis",
|
|
10256
|
-
title: row.lastModel,
|
|
10257
|
-
children: row.lastModel === "" ? "—" : row.lastModel
|
|
10258
|
-
}),
|
|
10259
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
10260
|
-
className: "dsm-xdpool-num",
|
|
10261
|
-
children: fmtInt(row.requests)
|
|
10262
|
-
}),
|
|
10263
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
10264
|
-
className: "dsm-xdpool-num",
|
|
10265
|
-
title: t?.("billion.contextTitle", {
|
|
10266
|
-
source: row.contextTokensSource,
|
|
10267
|
-
window: row.contextWindow
|
|
10268
|
-
}) ?? `source ${row.contextTokensSource} · window ${row.contextWindow}`,
|
|
10269
|
-
children: tokens(row.contextTokens)
|
|
10270
|
-
}),
|
|
10271
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
10272
|
-
className: "dsm-xdpool-num",
|
|
10273
|
-
children: fmtInt(row.blocks)
|
|
10274
|
-
}),
|
|
10275
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
10276
|
-
className: "dsm-xdpool-num",
|
|
10277
|
-
children: compact$1(row.tokensSaved)
|
|
10278
|
-
}),
|
|
10279
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
10280
|
-
className: "dsm-xdpool-num",
|
|
10281
|
-
children: row.cacheHitPct < 0 ? "—" : `${row.cacheHitPct}%`
|
|
10282
|
-
})
|
|
10283
|
-
]
|
|
10284
|
-
}, row.id);
|
|
10285
|
-
}) })]
|
|
10286
|
-
})
|
|
10287
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10288
|
-
className: "dsm-xdpool-note",
|
|
10289
|
-
children: t?.("billion.rowsHint") ?? "Hidden by default. The summary above is the whole fleet; open the rows or the proxy’s own dashboard when you need a specific session."
|
|
10290
|
-
}),
|
|
10291
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10292
|
-
className: "dsm-xdpool-note",
|
|
10293
|
-
children: t?.("billion.note") ?? "These numbers are the proxy’s own. Nothing here is an estimate, and this page never changes a setting."
|
|
10294
|
-
})
|
|
10295
|
-
]
|
|
10296
|
-
})] }) : null] })
|
|
10297
|
-
] });
|
|
10298
|
-
}
|
|
10299
|
-
/**
|
|
10300
|
-
* The profile name for the install command.
|
|
10301
|
-
*
|
|
10302
|
-
* The host publishes it as an env var; when that is absent the command still
|
|
10303
|
-
* works with `--profile` omitted on a single-profile install, so the hint says
|
|
10304
|
-
* so rather than printing an empty flag.
|
|
10305
|
-
*/
|
|
10306
|
-
function dshProfileHint(t) {
|
|
10307
|
-
const profile = typeof process === "undefined" ? void 0 : process.env.DSH_PROFILE;
|
|
10308
|
-
if (typeof profile === "string" && profile !== "") return profile;
|
|
10309
|
-
return t?.("billion.defaultProfile") ?? "<profile>";
|
|
10310
|
-
}
|
|
10311
|
-
//#endregion
|
|
10312
9938
|
//#region src/client/RoutingPage.tsx
|
|
10313
9939
|
/**
|
|
10314
9940
|
* The routing page: chains that are themselves models.
|
|
@@ -10395,8 +10021,10 @@ window.__ModuleLoader__.load({
|
|
|
10395
10021
|
const next = value;
|
|
10396
10022
|
setStatus(next);
|
|
10397
10023
|
setError(void 0);
|
|
10398
|
-
|
|
10399
|
-
|
|
10024
|
+
if (!seeded.current) {
|
|
10025
|
+
seeded.current = true;
|
|
10026
|
+
setDrafts((next.combos ?? []).map(draftOf));
|
|
10027
|
+
}
|
|
10400
10028
|
}
|
|
10401
10029
|
} catch (cause) {
|
|
10402
10030
|
if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
|
|
@@ -11568,6 +11196,18 @@ window.__ModuleLoader__.load({
|
|
|
11568
11196
|
*
|
|
11569
11197
|
* @module dsh-xdpool/client/ChannelPoolSection
|
|
11570
11198
|
*/
|
|
11199
|
+
let poolSnapshot;
|
|
11200
|
+
/**
|
|
11201
|
+
* Drop the snapshot after anything that changes the data behind it.
|
|
11202
|
+
*
|
|
11203
|
+
* Logins, deletions, toggles and manual balance refreshes all make the snapshot
|
|
11204
|
+
* stale in a way a silent revalidation cannot hide: showing yesterday's account
|
|
11205
|
+
* list for a card the user just added an account to is worse than showing the
|
|
11206
|
+
* "reading" line for a moment.
|
|
11207
|
+
*/
|
|
11208
|
+
function invalidatePoolSnapshot() {
|
|
11209
|
+
poolSnapshot = void 0;
|
|
11210
|
+
}
|
|
11571
11211
|
/** How many automatic retries a cold-start mount gets before giving up. */
|
|
11572
11212
|
const LOAD_RETRIES = 5;
|
|
11573
11213
|
/** Gap between those retries, in milliseconds. */
|
|
@@ -11660,8 +11300,18 @@ window.__ModuleLoader__.load({
|
|
|
11660
11300
|
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
11661
11301
|
}
|
|
11662
11302
|
function ChannelPoolSection({ t }) {
|
|
11663
|
-
const [entries, setEntries] = (0, react.useState)([]);
|
|
11664
|
-
const [error, setError] = (0, react.useState)(
|
|
11303
|
+
const [entries, setEntries] = (0, react.useState)(() => poolSnapshot?.entries ?? []);
|
|
11304
|
+
const [error, setError] = (0, react.useState)(() => poolSnapshot?.error);
|
|
11305
|
+
/**
|
|
11306
|
+
* True until the first wave has answered once.
|
|
11307
|
+
*
|
|
11308
|
+
* The section used to render NOTHING while loading, so a cold start and a
|
|
11309
|
+
* broken RPC bridge looked identical: blank (#95). The bridge failing is a
|
|
11310
|
+
* real, common failure — `provider.status` answers `ok: false`, the map comes
|
|
11311
|
+
* back empty, `scanProviders` is empty and the old code returned `<></>` with
|
|
11312
|
+
* `error === undefined`. Silence was indistinguishable from "no accounts".
|
|
11313
|
+
*/
|
|
11314
|
+
const [loading, setLoading] = (0, react.useState)(() => poolSnapshot === void 0);
|
|
11665
11315
|
const [notice, setNotice] = (0, react.useState)(void 0);
|
|
11666
11316
|
/** provider → the open list, or `undefined` for a card with both folded. */
|
|
11667
11317
|
const [folds, setFolds] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
@@ -11674,6 +11324,15 @@ window.__ModuleLoader__.load({
|
|
|
11674
11324
|
const entriesRef = (0, react.useRef)(entries);
|
|
11675
11325
|
entriesRef.current = entries;
|
|
11676
11326
|
const mountedRef = (0, react.useRef)(true);
|
|
11327
|
+
/**
|
|
11328
|
+
* Which load is current.
|
|
11329
|
+
*
|
|
11330
|
+
* Two things make a stale answer arrive late: the retry ladder re-runs the
|
|
11331
|
+
* whole chain, and a remount starts a new chain while the old request is
|
|
11332
|
+
* still in flight. Stamping each run is what keeps an older wave-2 result
|
|
11333
|
+
* from painting over a newer wave-1 one.
|
|
11334
|
+
*/
|
|
11335
|
+
const loadSeq = (0, react.useRef)(0);
|
|
11677
11336
|
(0, react.useEffect)(() => () => {
|
|
11678
11337
|
mountedRef.current = false;
|
|
11679
11338
|
}, []);
|
|
@@ -11686,6 +11345,7 @@ window.__ModuleLoader__.load({
|
|
|
11686
11345
|
});
|
|
11687
11346
|
};
|
|
11688
11347
|
const load = async () => {
|
|
11348
|
+
loadSeq.current += 1;
|
|
11689
11349
|
try {
|
|
11690
11350
|
const channels = (await fetchChannels()).filter((channel) => !REGION_OWNED_CHANNELS.has(channel.id));
|
|
11691
11351
|
const statusAnswer = await callPack("provider.status", { providers: channels.map((channel) => channel.id) });
|
|
@@ -11695,25 +11355,17 @@ window.__ModuleLoader__.load({
|
|
|
11695
11355
|
const wantBalances = channels.find((channel) => channel.id === provider)?.credits !== void 0;
|
|
11696
11356
|
const accountsPromise = callPack("account.list", { provider });
|
|
11697
11357
|
const modelsPromise = callPack("model.list", { provider });
|
|
11698
|
-
const
|
|
11699
|
-
const statusesPromise = wantBalances && channels.find((c) => c.id === provider)?.credits?.status === true ? callPack("credits.status", { provider }) : Promise.resolve(void 0);
|
|
11700
|
-
const [accounts, models, balances, creditStatuses] = await Promise.all([
|
|
11701
|
-
accountsPromise.catch(() => refused("account.list failed")),
|
|
11702
|
-
modelsPromise.catch(() => refused("model.list failed")),
|
|
11703
|
-
balancesPromise.catch(() => refused("credits.balances failed")),
|
|
11704
|
-
statusesPromise.catch(() => refused("credits.status failed"))
|
|
11705
|
-
]);
|
|
11358
|
+
const [accounts, models] = await Promise.all([accountsPromise.catch(() => refused("account.list failed")), modelsPromise.catch(() => refused("model.list failed"))]);
|
|
11706
11359
|
const modelRows = modelsFrom(models.value);
|
|
11707
11360
|
const modelEmptyBecauseRefused = modelRows.length === 0 && models.ok !== true;
|
|
11708
|
-
const balanceRows = balances === void 0 ? [] : balancesFrom(balances);
|
|
11709
11361
|
return [provider, {
|
|
11710
11362
|
accounts: dedupeAccounts(accountsFrom(accounts)),
|
|
11711
11363
|
models: sortChannelModels(modelRows),
|
|
11712
11364
|
facts: /* @__PURE__ */ new Map(),
|
|
11713
|
-
balances:
|
|
11714
|
-
statuses:
|
|
11715
|
-
balancePending:
|
|
11716
|
-
balanceError:
|
|
11365
|
+
balances: [],
|
|
11366
|
+
statuses: [],
|
|
11367
|
+
balancePending: wantBalances,
|
|
11368
|
+
balanceError: void 0,
|
|
11717
11369
|
modelError: modelEmptyBecauseRefused ? models.error?.message ?? "model.list refused" : void 0
|
|
11718
11370
|
}];
|
|
11719
11371
|
}));
|
|
@@ -11724,25 +11376,48 @@ window.__ModuleLoader__.load({
|
|
|
11724
11376
|
facts: /* @__PURE__ */ new Map(),
|
|
11725
11377
|
status: statuses.get(channel.id)
|
|
11726
11378
|
}));
|
|
11379
|
+
poolSnapshot = {
|
|
11380
|
+
entries: next,
|
|
11381
|
+
error: void 0
|
|
11382
|
+
};
|
|
11727
11383
|
if (!mountedRef.current) return;
|
|
11384
|
+
const seq = loadSeq.current;
|
|
11728
11385
|
setEntries(next);
|
|
11729
11386
|
setError(void 0);
|
|
11387
|
+
setLoading(false);
|
|
11730
11388
|
if (next.length === 0) {
|
|
11731
|
-
if (!statusAnswer.ok)
|
|
11389
|
+
if (!statusAnswer.ok) {
|
|
11390
|
+
const message = statusAnswer.error?.message ?? "provider.status refused";
|
|
11391
|
+
poolSnapshot = {
|
|
11392
|
+
entries: next,
|
|
11393
|
+
error: message
|
|
11394
|
+
};
|
|
11395
|
+
setError(message);
|
|
11396
|
+
}
|
|
11732
11397
|
return;
|
|
11733
11398
|
}
|
|
11399
|
+
for (const entry of next) {
|
|
11400
|
+
if (entry.channel.credits === void 0) continue;
|
|
11401
|
+
refreshBalances(entry.channel.id, false, true, seq);
|
|
11402
|
+
}
|
|
11734
11403
|
const idsByProvider = {};
|
|
11735
11404
|
for (const [provider, entry] of perProvider) if (entry.models.length > 0) idsByProvider[provider] = entry.models.map((model) => model.id);
|
|
11736
11405
|
const facts = await fetchChannelModels(next.map((entry) => entry.channel.id), idsByProvider).catch(() => /* @__PURE__ */ new Map());
|
|
11737
|
-
if (!mountedRef.current) return;
|
|
11406
|
+
if (!mountedRef.current || loadSeq.current !== seq) return;
|
|
11738
11407
|
if (facts.size === 0) return;
|
|
11739
11408
|
setEntries((previous) => previous.map((entry) => ({
|
|
11740
11409
|
...entry,
|
|
11741
11410
|
facts: new Map((facts.get(entry.channel.id) ?? []).map((model) => [model.id, model]))
|
|
11742
11411
|
})));
|
|
11743
11412
|
} catch (cause) {
|
|
11413
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
11414
|
+
poolSnapshot = {
|
|
11415
|
+
entries: entriesRef.current,
|
|
11416
|
+
error: message
|
|
11417
|
+
};
|
|
11744
11418
|
if (!mountedRef.current) return;
|
|
11745
|
-
setError(
|
|
11419
|
+
setError(message);
|
|
11420
|
+
setLoading(false);
|
|
11746
11421
|
}
|
|
11747
11422
|
};
|
|
11748
11423
|
/**
|
|
@@ -11896,8 +11571,20 @@ window.__ModuleLoader__.load({
|
|
|
11896
11571
|
await load();
|
|
11897
11572
|
}
|
|
11898
11573
|
};
|
|
11899
|
-
|
|
11900
|
-
|
|
11574
|
+
/**
|
|
11575
|
+
* Read one channel's balances and merge them into its card.
|
|
11576
|
+
*
|
|
11577
|
+
* Wave 2 of the load calls this with `quiet` true: the card is already on
|
|
11578
|
+
* screen, so the provider must not be marked busy (that would disable the
|
|
11579
|
+
* claim button for a network read the reader did not ask for) and a failure
|
|
11580
|
+
* must land in `balanceError` rather than in the section-wide notice.
|
|
11581
|
+
*
|
|
11582
|
+
* `seq` is the load that asked for this read. A reply from an older load
|
|
11583
|
+
* cannot overwrite a newer card, which is what the previous version got wrong
|
|
11584
|
+
* when a remount overlapped an in-flight refresh.
|
|
11585
|
+
*/
|
|
11586
|
+
const refreshBalances = async (provider, force, quiet = false, seq) => {
|
|
11587
|
+
if (!quiet) markBusy(provider, true);
|
|
11901
11588
|
setEntries((previous) => previous.map((entry) => entry.channel.id === provider ? {
|
|
11902
11589
|
...entry,
|
|
11903
11590
|
balancePending: true
|
|
@@ -11907,7 +11594,21 @@ window.__ModuleLoader__.load({
|
|
|
11907
11594
|
provider,
|
|
11908
11595
|
force: true
|
|
11909
11596
|
} : { provider });
|
|
11910
|
-
if (
|
|
11597
|
+
if (seq !== void 0 && loadSeq.current !== seq) return;
|
|
11598
|
+
if (!mountedRef.current) {
|
|
11599
|
+
if (envelope.ok) {
|
|
11600
|
+
const rows = balancesFrom(envelope);
|
|
11601
|
+
poolSnapshot = {
|
|
11602
|
+
entries: (poolSnapshot?.entries ?? []).map((entry) => entry.channel.id === provider ? {
|
|
11603
|
+
...entry,
|
|
11604
|
+
balancePending: false,
|
|
11605
|
+
balances: rows
|
|
11606
|
+
} : entry),
|
|
11607
|
+
error: poolSnapshot?.error
|
|
11608
|
+
};
|
|
11609
|
+
}
|
|
11610
|
+
return;
|
|
11611
|
+
}
|
|
11911
11612
|
if (!envelope.ok) {
|
|
11912
11613
|
setEntries((previous) => previous.map((entry) => entry.channel.id === provider ? {
|
|
11913
11614
|
...entry,
|
|
@@ -11924,7 +11625,7 @@ window.__ModuleLoader__.load({
|
|
|
11924
11625
|
balances: rows
|
|
11925
11626
|
} : entry));
|
|
11926
11627
|
} finally {
|
|
11927
|
-
if (mountedRef.current) markBusy(provider, false);
|
|
11628
|
+
if (mountedRef.current && !quiet) markBusy(provider, false);
|
|
11928
11629
|
}
|
|
11929
11630
|
};
|
|
11930
11631
|
const claim = async (provider) => {
|
|
@@ -11968,12 +11669,24 @@ window.__ModuleLoader__.load({
|
|
|
11968
11669
|
return;
|
|
11969
11670
|
}
|
|
11970
11671
|
if (action === "test") setNotice(t?.("channelpool.testOk") ?? "Reachable.");
|
|
11672
|
+
invalidatePoolSnapshot();
|
|
11971
11673
|
await load();
|
|
11972
11674
|
} finally {
|
|
11973
11675
|
if (mountedRef.current) markBusy(provider, false);
|
|
11974
11676
|
}
|
|
11975
11677
|
};
|
|
11976
|
-
if (entries.length === 0 && error === void 0)
|
|
11678
|
+
if (entries.length === 0 && error === void 0) {
|
|
11679
|
+
if (!loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
|
|
11680
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
11681
|
+
className: "dsm-xdpool-cpool",
|
|
11682
|
+
"aria-label": t?.("channelpool.title") ?? "Channels",
|
|
11683
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11684
|
+
className: "dsm-xdpool-note",
|
|
11685
|
+
role: "status",
|
|
11686
|
+
children: t?.("channelpool.loading") ?? "Reading scan channels…"
|
|
11687
|
+
})
|
|
11688
|
+
});
|
|
11689
|
+
}
|
|
11977
11690
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
11978
11691
|
className: "dsm-xdpool-cpool",
|
|
11979
11692
|
"aria-label": t?.("channelpool.title") ?? "Channels",
|
|
@@ -12368,7 +12081,7 @@ window.__ModuleLoader__.load({
|
|
|
12368
12081
|
*
|
|
12369
12082
|
* @module dsh-xdpool/version
|
|
12370
12083
|
*/
|
|
12371
|
-
const POOL_VERSION = "2.
|
|
12084
|
+
const POOL_VERSION = "2.5.1";
|
|
12372
12085
|
//#endregion
|
|
12373
12086
|
//#region src/client/PoolCard.tsx
|
|
12374
12087
|
/**
|
|
@@ -12734,10 +12447,6 @@ window.__ModuleLoader__.load({
|
|
|
12734
12447
|
id: "ledger",
|
|
12735
12448
|
label: t?.("shell.tabLedger") ?? "Dashboard"
|
|
12736
12449
|
},
|
|
12737
|
-
{
|
|
12738
|
-
id: "billion",
|
|
12739
|
-
label: t?.("shell.tabBillion") ?? "billion-context"
|
|
12740
|
-
},
|
|
12741
12450
|
{
|
|
12742
12451
|
id: "routing",
|
|
12743
12452
|
label: t?.("shell.tabRouting") ?? "Routing"
|
|
@@ -13582,7 +13291,7 @@ window.__ModuleLoader__.load({
|
|
|
13582
13291
|
children: page === "aggregate" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AggregatePage, { t }) : page === "channels" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChannelsPage, { t }) : page === "ledger" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LedgerPage, {
|
|
13583
13292
|
t,
|
|
13584
13293
|
usage
|
|
13585
|
-
}) : page === "
|
|
13294
|
+
}) : page === "routing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoutingPage, { t }) : page === "gateway" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GatewayPage, { t }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
13586
13295
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: POOL_CARD_CSS }),
|
|
13587
13296
|
usage === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
13588
13297
|
className: "dsm-xdpool-card dsm-xdpool-usage",
|
|
@@ -14583,6 +14292,20 @@ window.__ModuleLoader__.load({
|
|
|
14583
14292
|
global: "xdpool-global"
|
|
14584
14293
|
};
|
|
14585
14294
|
function ModelRow({ model, region, paidTwin, t, draft, editable, onToggle, onToggleImage, onBudget, onTest, probe, probing }) {
|
|
14295
|
+
/**
|
|
14296
|
+
* A per-MOUNTED-ROW scope for the two budget radios.
|
|
14297
|
+
*
|
|
14298
|
+
* HTML groups radios by `name`, and React renders both region cards at once —
|
|
14299
|
+
* the collapsible panes stay in the DOM with `hidden`, they are not unmounted.
|
|
14300
|
+
* A `name` built from the model id alone is therefore the SAME name in the
|
|
14301
|
+
* cn card and the global card, so the two rows joined one radio group: the
|
|
14302
|
+
* browser keeps only the last checked member, and whichever card renders
|
|
14303
|
+
* later (global) silently un-checks the cn row's "1M". Adding the region
|
|
14304
|
+
* would not be enough either — the settings page and the sidebar pane can
|
|
14305
|
+
* both mount the same region — so the scope has to name the row instance,
|
|
14306
|
+
* which is what `useId` is for.
|
|
14307
|
+
*/
|
|
14308
|
+
const radioScope = (0, react.useId)();
|
|
14586
14309
|
const now = /* @__PURE__ */ new Date();
|
|
14587
14310
|
const tag = tagFor(model, now, region);
|
|
14588
14311
|
const promo = promoStatusFor(model, now, region);
|
|
@@ -14651,7 +14374,7 @@ window.__ModuleLoader__.load({
|
|
|
14651
14374
|
"aria-label": t?.("row.modelContextBudget") ?? "Context",
|
|
14652
14375
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
14653
14376
|
type: "radio",
|
|
14654
|
-
name: `budget-${
|
|
14377
|
+
name: `budget-${radioScope}`,
|
|
14655
14378
|
checked: currentBudget === DEFAULT_CONTEXT_BUDGET,
|
|
14656
14379
|
disabled: !editable,
|
|
14657
14380
|
onChange: () => {
|
|
@@ -14659,7 +14382,7 @@ window.__ModuleLoader__.load({
|
|
|
14659
14382
|
}
|
|
14660
14383
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(DEFAULT_CONTEXT_BUDGET) })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
14661
14384
|
type: "radio",
|
|
14662
|
-
name: `budget-${
|
|
14385
|
+
name: `budget-${radioScope}`,
|
|
14663
14386
|
checked: currentBudget === native,
|
|
14664
14387
|
disabled: !editable,
|
|
14665
14388
|
onChange: () => {
|
|
@@ -15174,6 +14897,12 @@ window.__ModuleLoader__.load({
|
|
|
15174
14897
|
"shell.brand": "XD Pool",
|
|
15175
14898
|
"shell.star": "Star",
|
|
15176
14899
|
"shell.starHint": "Open this plugin on GitHub",
|
|
14900
|
+
"shell.group": "Community",
|
|
14901
|
+
"shell.groupHint": "Scan the code to join the XD community group",
|
|
14902
|
+
"shell.groupTitle": "XD community group",
|
|
14903
|
+
"shell.groupAlt": "WeChat group code: scan it to join the XD community group",
|
|
14904
|
+
"shell.groupClose": "Close",
|
|
14905
|
+
"shell.groupNote": "Scan with WeChat to join the XD community group. The code is valid for 7 days; if it has expired, open an issue on GitHub and it will be refreshed.",
|
|
15177
14906
|
"shell.tabPool": "Account pool",
|
|
15178
14907
|
"shell.tabChannels": "Login channels",
|
|
15179
14908
|
"shell.tabAggregate": "Providers",
|
|
@@ -15182,8 +14911,6 @@ window.__ModuleLoader__.load({
|
|
|
15182
14911
|
"shell.subAggregate": "Bring your own API keys: any OpenAI-compatible provider, reachable as extra models.",
|
|
15183
14912
|
"shell.tabLedger": "Dashboard",
|
|
15184
14913
|
"shell.subLedger": "What this machine sent and got back, since the pool last started.",
|
|
15185
|
-
"shell.tabBillion": "billion-context",
|
|
15186
|
-
"shell.subBillion": "A read-only window onto the compressor running beside this pool.",
|
|
15187
14914
|
"shell.tabRouting": "Routing",
|
|
15188
14915
|
"shell.subRouting": "Chains that try models in order, so one busy or rate-limited model falls through to the next.",
|
|
15189
14916
|
"shell.tabGateway": "Gateway",
|
|
@@ -15266,48 +14993,9 @@ window.__ModuleLoader__.load({
|
|
|
15266
14993
|
"ledger.unattributed": "(unattributed)",
|
|
15267
14994
|
"ledger.cn": "CN",
|
|
15268
14995
|
"ledger.global": "Global",
|
|
15269
|
-
"billion.title": "billion-context",
|
|
15270
|
-
"billion.sub": "What the compressor on this machine is doing. This page reads it; it cannot change it.",
|
|
15271
|
-
"billion.pillRunning": "running · v{version}",
|
|
15272
|
-
"billion.pillNotRunning": "not running",
|
|
15273
|
-
"billion.pillReadOnly": "read-only",
|
|
15274
|
-
"billion.failed": "Could not reach the pool relay. The pool itself is unaffected.",
|
|
15275
|
-
"billion.loading": "Looking for a billion-context proxy on this machine…",
|
|
15276
|
-
"billion.notInstalled": "billion-context is not installed on this machine",
|
|
15277
|
-
"billion.notAnswering": "A billion-context proxy is registered but did not answer",
|
|
15278
|
-
"billion.stepRestart": "Restart DSH so the profile bundle layer mounts it.",
|
|
15279
|
-
"billion.stepBack": "Come back to this page; it fills in on its own.",
|
|
15280
|
-
"billion.defaultProfile": "<profile>",
|
|
15281
|
-
"billion.sessions": "Sessions",
|
|
15282
|
-
"billion.noLive": "none active right now",
|
|
15283
|
-
"billion.liveSub": "{count} still active right now",
|
|
15284
|
-
"billion.requests": "Requests",
|
|
15285
|
-
"billion.requestsSub": "through the proxy, every lane",
|
|
15286
|
-
"billion.saved": "Tokens saved",
|
|
15287
|
-
"billion.savedSub": "by compression, net of what it cost",
|
|
15288
|
-
"billion.tokens": "Tokens",
|
|
15289
|
-
"billion.tokensSub": "{cached} cached · {out} out",
|
|
15290
|
-
"billion.cache": "Cache hit",
|
|
15291
|
-
"billion.cacheNone": "nothing measured yet",
|
|
15292
|
-
"billion.cacheSub": "of everything sent",
|
|
15293
|
-
"billion.empty": "The proxy is up but has seen no requests yet. Chat and this fills in.",
|
|
15294
|
-
"billion.perSession": "Per session",
|
|
15295
|
-
"billion.showRows": "Show {count} rows",
|
|
15296
|
-
"billion.hideRows": "Hide rows",
|
|
15297
|
-
"billion.openDashboard": "Open its dashboard",
|
|
15298
|
-
"billion.rowsHint": "The fleet summary above is the whole picture. Open the rows for this proxy’s numbers, or the proxy’s own dashboard when you need a single session.",
|
|
15299
|
-
"billion.note": "These are the proxy’s own numbers, read over loopback. Nothing here is an estimate.",
|
|
15300
|
-
"billion.colSession": "Session",
|
|
15301
|
-
"billion.colLast": "Last seen",
|
|
15302
|
-
"billion.colModel": "Model",
|
|
15303
|
-
"billion.colRequests": "Requests",
|
|
15304
|
-
"billion.colContext": "Context",
|
|
15305
|
-
"billion.colBlocks": "Blocks",
|
|
15306
|
-
"billion.colSaved": "Saved",
|
|
15307
|
-
"billion.colCache": "Cache",
|
|
15308
|
-
"billion.contextTitle": "measured {source} · model window {window}",
|
|
15309
14996
|
"channelpool.title": "Scan channels",
|
|
15310
14997
|
"channelpool.accountCount": "{count} account(s)",
|
|
14998
|
+
"channelpool.loading": "Reading scan channels…",
|
|
15311
14999
|
"channelpool.disabled": "disabled",
|
|
15312
15000
|
"channelpool.noModels": "No models published yet.",
|
|
15313
15001
|
"channelpool.modelError": "Could not list models: {message}",
|
|
@@ -15685,6 +15373,12 @@ window.__ModuleLoader__.load({
|
|
|
15685
15373
|
"shell.brand": "XD Pool",
|
|
15686
15374
|
"shell.star": "点Star",
|
|
15687
15375
|
"shell.starHint": "在 GitHub 上给这个插件点个星",
|
|
15376
|
+
"shell.group": "交流群",
|
|
15377
|
+
"shell.groupHint": "扫码加入 XD 交流群",
|
|
15378
|
+
"shell.groupTitle": "XD 交流群",
|
|
15379
|
+
"shell.groupAlt": "微信群二维码:用微信扫一扫加入 XD 交流群",
|
|
15380
|
+
"shell.groupClose": "关闭",
|
|
15381
|
+
"shell.groupNote": "用微信「扫一扫」加入 XD 交流群。二维码 7 天内有效,过期了在 GitHub 提个 issue 说一声,会更新。",
|
|
15688
15382
|
"shell.tabPool": "账号池",
|
|
15689
15383
|
"shell.tabChannels": "扫码登录",
|
|
15690
15384
|
"shell.tabAggregate": "聚合渠道",
|
|
@@ -15693,8 +15387,6 @@ window.__ModuleLoader__.load({
|
|
|
15693
15387
|
"shell.subAggregate": "带上你自己的 API key:任何兼容 OpenAI 的服务都能变成额外模型。",
|
|
15694
15388
|
"shell.tabLedger": "数据看板",
|
|
15695
15389
|
"shell.subLedger": "这台机器发出去什么、收回来什么,从池子上次启动算起。",
|
|
15696
|
-
"shell.tabBillion": "billion-context",
|
|
15697
|
-
"shell.subBillion": "一个只读的窗口,看着旁边跑着的那台压缩器。",
|
|
15698
15390
|
"shell.tabRouting": "路由",
|
|
15699
15391
|
"shell.subRouting": "把几个模型串成一条链,按顺序试;一个忙了或被限流,就自动落到下一个。",
|
|
15700
15392
|
"shell.tabGateway": "网关",
|
|
@@ -15777,48 +15469,9 @@ window.__ModuleLoader__.load({
|
|
|
15777
15469
|
"ledger.unattributed": "(没归属到账号)",
|
|
15778
15470
|
"ledger.cn": "国内版",
|
|
15779
15471
|
"ledger.global": "国际版",
|
|
15780
|
-
"billion.title": "billion-context",
|
|
15781
|
-
"billion.sub": "这台机器上的压缩器正在做什么。这一页只看不改,它动不了那个代理。",
|
|
15782
|
-
"billion.pillRunning": "运行中 · v{version}",
|
|
15783
|
-
"billion.pillNotRunning": "没在运行",
|
|
15784
|
-
"billion.pillReadOnly": "只读",
|
|
15785
|
-
"billion.failed": "连不上池子的中继接口,池子本身不受影响。",
|
|
15786
|
-
"billion.loading": "正在这台机器上找 billion-context 代理…",
|
|
15787
|
-
"billion.notInstalled": "这台机器上没装 billion-context",
|
|
15788
|
-
"billion.notAnswering": "登记过一个 billion-context 代理,但它没应答",
|
|
15789
|
-
"billion.stepRestart": "重启 DSH,让 profile 的 bundle 层把它挂上。",
|
|
15790
|
-
"billion.stepBack": "回到这一页,数字会自己填上。",
|
|
15791
|
-
"billion.defaultProfile": "<profile>",
|
|
15792
|
-
"billion.sessions": "会话数",
|
|
15793
|
-
"billion.noLive": "当前没有活跃会话",
|
|
15794
|
-
"billion.liveSub": "当前还有 {count} 个在跑",
|
|
15795
|
-
"billion.requests": "请求数",
|
|
15796
|
-
"billion.requestsSub": "经过代理的,全部线路",
|
|
15797
|
-
"billion.saved": "省下的 Token",
|
|
15798
|
-
"billion.savedSub": "压缩净省的,已经扣掉它自己花掉的",
|
|
15799
|
-
"billion.tokens": "Token 总量",
|
|
15800
|
-
"billion.tokensSub": "{cached} 来自缓存 · {out} 输出",
|
|
15801
|
-
"billion.cache": "缓存命中",
|
|
15802
|
-
"billion.cacheNone": "还没有测量数据",
|
|
15803
|
-
"billion.cacheSub": "在送出去的所有 token 里",
|
|
15804
|
-
"billion.empty": "代理起来了,但还没收到过请求,聊两句这里就填上了。",
|
|
15805
|
-
"billion.perSession": "按会话看",
|
|
15806
|
-
"billion.showRows": "展开 {count} 行",
|
|
15807
|
-
"billion.hideRows": "收起",
|
|
15808
|
-
"billion.openDashboard": "打开它自己的仪表盘",
|
|
15809
|
-
"billion.rowsHint": "上面的全局数字就是全部情况。要看这一个代理的明细就展开表格,要看单个会话就去它自己的仪表盘。",
|
|
15810
|
-
"billion.note": "这些是代理自己报的数字,走的是本机回环。这里没有一个是估算值。",
|
|
15811
|
-
"billion.colSession": "会话",
|
|
15812
|
-
"billion.colLast": "最后出现",
|
|
15813
|
-
"billion.colModel": "模型",
|
|
15814
|
-
"billion.colRequests": "请求",
|
|
15815
|
-
"billion.colContext": "上下文",
|
|
15816
|
-
"billion.colBlocks": "压缩块",
|
|
15817
|
-
"billion.colSaved": "省下",
|
|
15818
|
-
"billion.colCache": "缓存",
|
|
15819
|
-
"billion.contextTitle": "实测 {source} · 模型窗口 {window}",
|
|
15820
15472
|
"channelpool.title": "扫码渠道",
|
|
15821
15473
|
"channelpool.accountCount": "{count} 个账号",
|
|
15474
|
+
"channelpool.loading": "正在读取扫码渠道…",
|
|
15822
15475
|
"channelpool.disabled": "已停用",
|
|
15823
15476
|
"channelpool.noModels": "该渠道还没有发布模型。",
|
|
15824
15477
|
"channelpool.modelError": "读取模型列表失败:{message}",
|