@jxgame2020/dsh-token-quota 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +965 -0
- package/lib/index.js +387 -0
- package/lib/invariant.js +25 -0
- package/lib/types/client/TokenQuotaPanel.d.ts +28 -0
- package/lib/types/client/TokenQuotaPanel.js +147 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/index.js +232 -0
- package/lib/types/client/locales.d.ts +97 -0
- package/lib/types/client/locales.js +95 -0
- package/lib/types/client/store.d.ts +89 -0
- package/lib/types/client/store.js +92 -0
- package/lib/types/index.d.ts +99 -0
- package/lib/types/index.js +410 -0
- package/lib/types/invariant.d.ts +17 -0
- package/lib/types/invariant.js +24 -0
- package/lib/types/types.d.ts +125 -0
- package/lib/types/types.js +29 -0
- package/package.json +153 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@jxgame2020/dsh-token-quota",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
//#region lib/types/client/store.js
|
|
11
|
+
/**
|
|
12
|
+
* Token-quota panel store: the shared, remount-surviving view state. The
|
|
13
|
+
* apply-world is the only writer — forwarded `token-quota/updated` snapshots
|
|
14
|
+
* and the model-directory loader feed it — while the panel reads through
|
|
15
|
+
* `useStore`. Display rows are derived data (pure function over the two
|
|
16
|
+
* sources), so the component builds them with `useMemo`, never a store scan.
|
|
17
|
+
*
|
|
18
|
+
* @module @deepseek-ai/dsh-client-ui-token-quota/client/store
|
|
19
|
+
*/
|
|
20
|
+
/** Build the stable per-model key shared by the counter, settings, and snapshot. */
|
|
21
|
+
function tokenQuotaKey(provider, model) {
|
|
22
|
+
return `${provider}/${model}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Declares the panel state and write surface.
|
|
26
|
+
* @returns the store handle.
|
|
27
|
+
*/
|
|
28
|
+
function createTokenQuotaPanelStore() {
|
|
29
|
+
return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
|
|
30
|
+
init: () => ({
|
|
31
|
+
snapshot: null,
|
|
32
|
+
groups: [],
|
|
33
|
+
current: null,
|
|
34
|
+
loading: false,
|
|
35
|
+
error: null,
|
|
36
|
+
monitored: null,
|
|
37
|
+
onFull: "stop",
|
|
38
|
+
dialogOpen: false,
|
|
39
|
+
fullNotice: null,
|
|
40
|
+
reset: null,
|
|
41
|
+
logOpen: false,
|
|
42
|
+
log: null
|
|
43
|
+
}),
|
|
44
|
+
actions: {
|
|
45
|
+
setSnapshot: (d, snapshot) => {
|
|
46
|
+
d.snapshot = snapshot;
|
|
47
|
+
},
|
|
48
|
+
setDirectory: (d, groups, current) => {
|
|
49
|
+
d.groups = groups;
|
|
50
|
+
d.current = current;
|
|
51
|
+
},
|
|
52
|
+
setLoading: (d, loading) => {
|
|
53
|
+
d.loading = loading;
|
|
54
|
+
},
|
|
55
|
+
setError: (d, error) => {
|
|
56
|
+
d.error = error;
|
|
57
|
+
},
|
|
58
|
+
setSettings: (d, monitored, onFull) => {
|
|
59
|
+
d.monitored = monitored;
|
|
60
|
+
d.onFull = onFull;
|
|
61
|
+
},
|
|
62
|
+
setDialogOpen: (d, open) => {
|
|
63
|
+
d.dialogOpen = open;
|
|
64
|
+
},
|
|
65
|
+
setFullNotice: (d, notice) => {
|
|
66
|
+
d.fullNotice = notice;
|
|
67
|
+
},
|
|
68
|
+
setReset: (d, reset) => {
|
|
69
|
+
d.reset = reset;
|
|
70
|
+
},
|
|
71
|
+
setLogOpen: (d, open) => {
|
|
72
|
+
d.logOpen = open;
|
|
73
|
+
},
|
|
74
|
+
setLog: (d, log) => {
|
|
75
|
+
d.log = log;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Merge the session's model directory with the quota snapshot into display
|
|
82
|
+
* rows. Every directory model gets a row (usage/limit fall back to
|
|
83
|
+
* `0`/`0`), and snapshot entries for routes missing from the directory are
|
|
84
|
+
* appended so no counter is ever hidden.
|
|
85
|
+
* @param groups - advisory provider groups of the current session.
|
|
86
|
+
* @param snapshot - latest quota snapshot, or null before the first one.
|
|
87
|
+
* @param current - current model selection reported by the Host.
|
|
88
|
+
* @returns rows sorted by key.
|
|
89
|
+
*/
|
|
90
|
+
function mergeModelRows(groups, snapshot, current) {
|
|
91
|
+
const entryByKey = new Map((snapshot?.entries ?? []).map((entry) => [entry.key, entry]));
|
|
92
|
+
const rows = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const group of groups) for (const model of group.models) {
|
|
94
|
+
const key = tokenQuotaKey(group.id, model.id);
|
|
95
|
+
const entry = entryByKey.get(key);
|
|
96
|
+
rows.set(key, {
|
|
97
|
+
key,
|
|
98
|
+
provider: group.id,
|
|
99
|
+
model: model.id,
|
|
100
|
+
name: model.name,
|
|
101
|
+
used: entry?.used ?? 0,
|
|
102
|
+
limit: entry?.limit ?? 0,
|
|
103
|
+
current: current !== null && current.provider === group.id && current.model === model.id
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
for (const entry of snapshot?.entries ?? []) {
|
|
107
|
+
if (rows.has(entry.key)) continue;
|
|
108
|
+
rows.set(entry.key, {
|
|
109
|
+
key: entry.key,
|
|
110
|
+
provider: entry.provider,
|
|
111
|
+
model: entry.model,
|
|
112
|
+
name: entry.model,
|
|
113
|
+
used: entry.used,
|
|
114
|
+
limit: entry.limit,
|
|
115
|
+
current: current !== null && current.provider === entry.provider && current.model === entry.model
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return [...rows.values()].sort((left, right) => left.key.localeCompare(right.key));
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region \0dsh-css:/Users/jxgame/Desktop/works/deepseek-harness-package/dsh-token-quota/packages/token-quota/src/client/TokenQuotaPanel.module.css.mjs
|
|
122
|
+
const css = ".NPLLIq_panel{box-sizing:border-box;background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l1);pointer-events:auto;z-index:40;border-radius:12px;flex-direction:column;width:320px;max-height:min(72vh,560px);display:flex;position:fixed;top:56px;right:12px;overflow:hidden;box-shadow:0 8px 28px #0000002e}.NPLLIq_header{border-bottom:1px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:flex-start;gap:8px;padding:10px 12px;display:flex}.NPLLIq_headerText{flex-direction:column;gap:2px;min-width:0;display:flex}.NPLLIq_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.NPLLIq_subtitle{color:var(--dsw-alias-label-secondary);font-size:11px}.NPLLIq_collapse{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:2px 6px;font-size:11px}.NPLLIq_collapse:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_body{padding:4px 0;overflow-y:auto}.NPLLIq_row{border-bottom:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:6px;padding:8px 12px;display:flex}.NPLLIq_rowHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.NPLLIq_rowName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;align-items:center;gap:6px;font-size:12px;font-weight:500;display:inline-flex;overflow:hidden}.NPLLIq_currentBadge{color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary);border-radius:8px;flex:none;padding:1px 6px;font-size:10px}.NPLLIq_rowMeta{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:11px}.NPLLIq_bar{background:var(--dsw-alias-bg-layer-1);border-radius:3px;height:6px;overflow:hidden}.NPLLIq_fillIdle{background:var(--dsw-alias-brand-primary);height:100%;transition:width .2s}.NPLLIq_fillWarn{background:var(--dsw-alias-state-warn-primary);height:100%;transition:width .2s}.NPLLIq_fillOver{background:var(--dsw-alias-state-error-primary);height:100%;transition:width .2s}.NPLLIq_controls{align-items:center;gap:6px;display:flex}.NPLLIq_input{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);min-width:0;color:var(--dsw-alias-label-primary);border-radius:6px;flex:1;padding:4px 8px;font-size:11px}.NPLLIq_input::placeholder{color:var(--dsw-alias-label-secondary)}.NPLLIq_save,.NPLLIq_switch{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;flex:none;padding:4px 10px;font-size:11px}.NPLLIq_save:hover,.NPLLIq_switch:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}.NPLLIq_switch:disabled{opacity:.5;cursor:default}.NPLLIq_notice,.NPLLIq_noticeError{color:var(--dsw-alias-label-secondary);padding:10px 12px;font-size:12px}.NPLLIq_noticeError{color:var(--dsw-alias-state-error-primary)}.NPLLIq_tab{background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l1);pointer-events:auto;cursor:pointer;z-index:40;border-right:none;border-radius:8px 0 0 8px;padding:10px 6px;position:fixed;top:50%;right:0;transform:translateY(-50%);box-shadow:0 4px 16px #0000001f}.NPLLIq_tab:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_tabLabel{writing-mode:vertical-rl;color:var(--dsw-alias-label-secondary);font-size:11px}.NPLLIq_headerActions{flex:none;align-items:center;gap:4px;display:flex}.NPLLIq_settingsBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:2px 6px;font-size:11px}.NPLLIq_settingsBtn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_selectBtn{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-brand-primary);cursor:pointer;background:0 0;border-radius:6px;flex:none;padding:1px 6px;font-size:10px}.NPLLIq_selectBtn:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_gearBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;margin-left:6px;padding:0 2px;font-size:12px;line-height:1}.NPLLIq_gearBtn:hover{color:var(--dsw-alias-label-primary)}.NPLLIq_fullNotice{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;margin:6px 12px;padding:8px 10px;font-size:12px}.NPLLIq_dialogBackdrop{z-index:50;background:#00000059;border-radius:12px;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.NPLLIq_dialog{box-sizing:border-box;background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;gap:12px;width:280px;max-height:80%;padding:14px;display:flex;overflow-y:auto;box-shadow:0 8px 28px #00000040}.NPLLIq_dialogTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.NPLLIq_dialogSection{flex-direction:column;gap:6px;display:flex}.NPLLIq_dialogLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500}.NPLLIq_monitorHint{color:var(--dsw-alias-label-secondary);font-size:11px}.NPLLIq_monitorList{flex-direction:column;gap:2px;max-height:180px;display:flex;overflow-y:auto}.NPLLIq_monitorRow{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:2px 4px;font-size:12px;display:flex}.NPLLIq_monitorRow:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_monitorName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.NPLLIq_radioRow{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:3px 4px;font-size:12px;display:flex}.NPLLIq_radioRow:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_dialogActions{justify-content:flex-end;gap:6px;display:flex}.NPLLIq_dialogBtn,.NPLLIq_dialogBtnPrimary{border:1px solid var(--dsw-alias-border-l1);cursor:pointer;border-radius:6px;padding:4px 12px;font-size:11px}.NPLLIq_dialogBtn{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.NPLLIq_dialogBtnPrimary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-overlay)}.NPLLIq_dialogHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.NPLLIq_dialogClose{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;font-size:18px;line-height:1;display:flex}.NPLLIq_dialogClose:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_resetRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.NPLLIq_resetSelect{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:3px 6px;font-size:12px}.NPLLIq_logTable{border-collapse:collapse;width:100%;font-size:11px}.NPLLIq_logTable th{text-align:left;color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);padding:4px 6px;font-weight:500}.NPLLIq_logTable td{color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l1);font-variant-numeric:tabular-nums;padding:4px 6px}.NPLLIq_logDayCol{white-space:nowrap;color:var(--dsw-alias-label-secondary)}.NPLLIq_logModelCol{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.NPLLIq_logUsedCol{text-align:right}";
|
|
123
|
+
const tagId = "@jxgame2020/dsh-token-quota/TokenQuotaPanel.module.css";
|
|
124
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
125
|
+
const tag = document.createElement("style");
|
|
126
|
+
tag.dataset.plugin = "@jxgame2020/dsh-token-quota";
|
|
127
|
+
tag.dataset.pluginCss = tagId;
|
|
128
|
+
tag.textContent = css;
|
|
129
|
+
document.head.appendChild(tag);
|
|
130
|
+
}
|
|
131
|
+
var TokenQuotaPanel_module_css_default = {
|
|
132
|
+
"save": "NPLLIq_save",
|
|
133
|
+
"row": "NPLLIq_row",
|
|
134
|
+
"dialog": "NPLLIq_dialog",
|
|
135
|
+
"dialogTitle": "NPLLIq_dialogTitle",
|
|
136
|
+
"controls": "NPLLIq_controls",
|
|
137
|
+
"monitorRow": "NPLLIq_monitorRow",
|
|
138
|
+
"bar": "NPLLIq_bar",
|
|
139
|
+
"fillIdle": "NPLLIq_fillIdle",
|
|
140
|
+
"switch": "NPLLIq_switch",
|
|
141
|
+
"dialogBackdrop": "NPLLIq_dialogBackdrop",
|
|
142
|
+
"currentBadge": "NPLLIq_currentBadge",
|
|
143
|
+
"header": "NPLLIq_header",
|
|
144
|
+
"dialogClose": "NPLLIq_dialogClose",
|
|
145
|
+
"logModelCol": "NPLLIq_logModelCol",
|
|
146
|
+
"panel": "NPLLIq_panel",
|
|
147
|
+
"fillWarn": "NPLLIq_fillWarn",
|
|
148
|
+
"headerActions": "NPLLIq_headerActions",
|
|
149
|
+
"resetSelect": "NPLLIq_resetSelect",
|
|
150
|
+
"logDayCol": "NPLLIq_logDayCol",
|
|
151
|
+
"notice": "NPLLIq_notice",
|
|
152
|
+
"monitorName": "NPLLIq_monitorName",
|
|
153
|
+
"tab": "NPLLIq_tab",
|
|
154
|
+
"dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
|
|
155
|
+
"logUsedCol": "NPLLIq_logUsedCol",
|
|
156
|
+
"settingsBtn": "NPLLIq_settingsBtn",
|
|
157
|
+
"headerText": "NPLLIq_headerText",
|
|
158
|
+
"dialogActions": "NPLLIq_dialogActions",
|
|
159
|
+
"monitorHint": "NPLLIq_monitorHint",
|
|
160
|
+
"subtitle": "NPLLIq_subtitle",
|
|
161
|
+
"rowHeader": "NPLLIq_rowHeader",
|
|
162
|
+
"input": "NPLLIq_input",
|
|
163
|
+
"dialogSection": "NPLLIq_dialogSection",
|
|
164
|
+
"monitorList": "NPLLIq_monitorList",
|
|
165
|
+
"fullNotice": "NPLLIq_fullNotice",
|
|
166
|
+
"dialogBtn": "NPLLIq_dialogBtn",
|
|
167
|
+
"dialogHeader": "NPLLIq_dialogHeader",
|
|
168
|
+
"body": "NPLLIq_body",
|
|
169
|
+
"noticeError": "NPLLIq_noticeError",
|
|
170
|
+
"selectBtn": "NPLLIq_selectBtn",
|
|
171
|
+
"collapse": "NPLLIq_collapse",
|
|
172
|
+
"rowMeta": "NPLLIq_rowMeta",
|
|
173
|
+
"radioRow": "NPLLIq_radioRow",
|
|
174
|
+
"resetRow": "NPLLIq_resetRow",
|
|
175
|
+
"fillOver": "NPLLIq_fillOver",
|
|
176
|
+
"logTable": "NPLLIq_logTable",
|
|
177
|
+
"dialogLabel": "NPLLIq_dialogLabel",
|
|
178
|
+
"title": "NPLLIq_title",
|
|
179
|
+
"tabLabel": "NPLLIq_tabLabel",
|
|
180
|
+
"gearBtn": "NPLLIq_gearBtn",
|
|
181
|
+
"rowName": "NPLLIq_rowName"
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region lib/types/client/TokenQuotaPanel.js
|
|
185
|
+
/**
|
|
186
|
+
* Token-quota floating panel, registered into the frame-wide `shell.overlay`
|
|
187
|
+
* seat. Renders one row per MONITORED model of the current session's
|
|
188
|
+
* directory merged with the live quota snapshot: name, a compact `选择` action,
|
|
189
|
+
* `used / limit`, a progress bar, and an icon-folded limit editor. A settings
|
|
190
|
+
* dialog (header button) chooses which models are monitored and what to do
|
|
191
|
+
* when a capped model is full. The panel is pure presentation — every fact
|
|
192
|
+
* arrives through the props shares and every mutation through the injected
|
|
193
|
+
* callbacks.
|
|
194
|
+
*/
|
|
195
|
+
/** Compact a token count for display. */
|
|
196
|
+
function formatTokens(n) {
|
|
197
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
198
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
199
|
+
return String(n);
|
|
200
|
+
}
|
|
201
|
+
function barStateOf(row) {
|
|
202
|
+
if (row.limit <= 0) return row.used > 0 ? "warn" : "idle";
|
|
203
|
+
if (row.used >= row.limit) return "over";
|
|
204
|
+
if (row.used / row.limit >= .8) return "warn";
|
|
205
|
+
return "idle";
|
|
206
|
+
}
|
|
207
|
+
/** Full-quota strategy choices, in display order. */
|
|
208
|
+
const FULL_ACTIONS = [
|
|
209
|
+
{
|
|
210
|
+
value: "stop",
|
|
211
|
+
labelKey: "fullStop"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
value: "switchQuota",
|
|
215
|
+
labelKey: "fullSwitchQuota"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
value: "switchAll",
|
|
219
|
+
labelKey: "fullSwitchAll"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
value: "switchPriority",
|
|
223
|
+
labelKey: "fullSwitchPriority"
|
|
224
|
+
}
|
|
225
|
+
];
|
|
226
|
+
/**
|
|
227
|
+
* Render the floating panel (or its collapsed tab).
|
|
228
|
+
* @param props - composed slot props.
|
|
229
|
+
* @returns the panel element tree.
|
|
230
|
+
*/
|
|
231
|
+
function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, useStore, actions, useSessions }) {
|
|
232
|
+
const [collapsed, setCollapsed] = (0, react.useState)(false);
|
|
233
|
+
const [drafts, setDrafts] = (0, react.useState)({});
|
|
234
|
+
const [editingKey, setEditingKey] = (0, react.useState)(null);
|
|
235
|
+
const sessionId = useSessions((s) => s.current);
|
|
236
|
+
const snapshot = useStore((s) => s.snapshot);
|
|
237
|
+
const groups = useStore((s) => s.groups);
|
|
238
|
+
const current = useStore((s) => s.current);
|
|
239
|
+
const monitored = useStore((s) => s.monitored);
|
|
240
|
+
const onFull = useStore((s) => s.onFull);
|
|
241
|
+
const reset = useStore((s) => s.reset);
|
|
242
|
+
const dialogOpen = useStore((s) => s.dialogOpen);
|
|
243
|
+
const logOpen = useStore((s) => s.logOpen);
|
|
244
|
+
const log = useStore((s) => s.log);
|
|
245
|
+
const fullNotice = useStore((s) => s.fullNotice);
|
|
246
|
+
const loading = useStore((s) => s.loading);
|
|
247
|
+
const error = useStore((s) => s.error);
|
|
248
|
+
(0, react.useEffect)(() => {
|
|
249
|
+
if (sessionId !== void 0) load(sessionId);
|
|
250
|
+
}, [sessionId, load]);
|
|
251
|
+
const rows = (0, react.useMemo)(() => mergeModelRows(groups, snapshot, current), [
|
|
252
|
+
groups,
|
|
253
|
+
snapshot,
|
|
254
|
+
current
|
|
255
|
+
]);
|
|
256
|
+
const allModels = (0, react.useMemo)(() => groups.flatMap((group) => group.models.map((model) => ({
|
|
257
|
+
key: `${group.id}/${model.id}`,
|
|
258
|
+
name: model.name
|
|
259
|
+
}))).sort((a, b) => a.key.localeCompare(b.key)), [groups]);
|
|
260
|
+
const isMonitoredKey = (key) => monitored === null || monitored.includes(key);
|
|
261
|
+
if (collapsed) return (0, react_jsx_runtime.jsx)("div", {
|
|
262
|
+
className: TokenQuotaPanel_module_css_default.tab,
|
|
263
|
+
role: "button",
|
|
264
|
+
tabIndex: 0,
|
|
265
|
+
title: t("title"),
|
|
266
|
+
onClick: () => {
|
|
267
|
+
setCollapsed(false);
|
|
268
|
+
},
|
|
269
|
+
children: (0, react_jsx_runtime.jsx)("span", {
|
|
270
|
+
className: TokenQuotaPanel_module_css_default.tabLabel,
|
|
271
|
+
children: t("expand")
|
|
272
|
+
})
|
|
273
|
+
});
|
|
274
|
+
const openSettings = () => {
|
|
275
|
+
actions.setDialogOpen(true);
|
|
276
|
+
};
|
|
277
|
+
(0, react.useEffect)(() => {
|
|
278
|
+
if (!logOpen) return;
|
|
279
|
+
let cancelled = false;
|
|
280
|
+
fetch("/token-quota/log", { headers: { accept: "application/json" } }).then((response) => response.json(), () => null).then((data) => {
|
|
281
|
+
if (!cancelled) actions.setLog(data);
|
|
282
|
+
});
|
|
283
|
+
return () => {
|
|
284
|
+
cancelled = true;
|
|
285
|
+
};
|
|
286
|
+
}, [logOpen, actions]);
|
|
287
|
+
const commitLimit = (row) => {
|
|
288
|
+
const raw = drafts[row.key]?.trim();
|
|
289
|
+
setDrafts((prev) => {
|
|
290
|
+
const { [row.key]: _dropped, ...rest } = prev;
|
|
291
|
+
return rest;
|
|
292
|
+
});
|
|
293
|
+
setEditingKey(null);
|
|
294
|
+
if (raw === void 0 || raw === "") return;
|
|
295
|
+
const parsed = Number(raw);
|
|
296
|
+
if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) return;
|
|
297
|
+
setLimit(row.key, parsed);
|
|
298
|
+
};
|
|
299
|
+
const visibleRows = rows.filter((row) => isMonitoredKey(row.key) || row.current);
|
|
300
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
301
|
+
className: TokenQuotaPanel_module_css_default.panel,
|
|
302
|
+
children: [
|
|
303
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
304
|
+
className: TokenQuotaPanel_module_css_default.header,
|
|
305
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
306
|
+
className: TokenQuotaPanel_module_css_default.headerText,
|
|
307
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
308
|
+
className: TokenQuotaPanel_module_css_default.title,
|
|
309
|
+
children: t("title")
|
|
310
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
311
|
+
className: TokenQuotaPanel_module_css_default.subtitle,
|
|
312
|
+
children: t("subtitle")
|
|
313
|
+
})]
|
|
314
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
315
|
+
className: TokenQuotaPanel_module_css_default.headerActions,
|
|
316
|
+
children: [
|
|
317
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
318
|
+
type: "button",
|
|
319
|
+
className: TokenQuotaPanel_module_css_default.settingsBtn,
|
|
320
|
+
onClick: () => {
|
|
321
|
+
actions.setLogOpen(true);
|
|
322
|
+
},
|
|
323
|
+
children: t("logs")
|
|
324
|
+
}),
|
|
325
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
326
|
+
type: "button",
|
|
327
|
+
className: TokenQuotaPanel_module_css_default.settingsBtn,
|
|
328
|
+
onClick: openSettings,
|
|
329
|
+
children: t("settings")
|
|
330
|
+
}),
|
|
331
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
332
|
+
type: "button",
|
|
333
|
+
className: TokenQuotaPanel_module_css_default.collapse,
|
|
334
|
+
onClick: () => {
|
|
335
|
+
setCollapsed(true);
|
|
336
|
+
},
|
|
337
|
+
children: t("collapse")
|
|
338
|
+
})
|
|
339
|
+
]
|
|
340
|
+
})]
|
|
341
|
+
}),
|
|
342
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
343
|
+
className: TokenQuotaPanel_module_css_default.body,
|
|
344
|
+
children: [
|
|
345
|
+
loading && (0, react_jsx_runtime.jsx)("div", {
|
|
346
|
+
className: TokenQuotaPanel_module_css_default.notice,
|
|
347
|
+
children: t("loading")
|
|
348
|
+
}),
|
|
349
|
+
error !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
350
|
+
className: TokenQuotaPanel_module_css_default.noticeError,
|
|
351
|
+
children: error
|
|
352
|
+
}),
|
|
353
|
+
fullNotice !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
354
|
+
className: TokenQuotaPanel_module_css_default.fullNotice,
|
|
355
|
+
role: "alert",
|
|
356
|
+
children: fullNotice
|
|
357
|
+
}),
|
|
358
|
+
!loading && error === null && visibleRows.length === 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
359
|
+
className: TokenQuotaPanel_module_css_default.notice,
|
|
360
|
+
children: monitored !== null && monitored.length === 0 ? t("noMonitored") : t("waiting")
|
|
361
|
+
}),
|
|
362
|
+
visibleRows.map((row) => {
|
|
363
|
+
const state = barStateOf(row);
|
|
364
|
+
const pct = row.limit > 0 ? Math.min(100, Math.round(row.used / row.limit * 100)) : 0;
|
|
365
|
+
const barClass = state === "over" ? TokenQuotaPanel_module_css_default.fillOver : state === "warn" ? TokenQuotaPanel_module_css_default.fillWarn : TokenQuotaPanel_module_css_default.fillIdle;
|
|
366
|
+
const editing = editingKey === row.key;
|
|
367
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
368
|
+
className: TokenQuotaPanel_module_css_default.row,
|
|
369
|
+
children: [
|
|
370
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
371
|
+
className: TokenQuotaPanel_module_css_default.rowHeader,
|
|
372
|
+
children: [(0, react_jsx_runtime.jsxs)("span", {
|
|
373
|
+
className: TokenQuotaPanel_module_css_default.rowName,
|
|
374
|
+
title: row.key,
|
|
375
|
+
children: [
|
|
376
|
+
row.name,
|
|
377
|
+
row.current && (0, react_jsx_runtime.jsx)("span", {
|
|
378
|
+
className: TokenQuotaPanel_module_css_default.currentBadge,
|
|
379
|
+
children: t("current")
|
|
380
|
+
}),
|
|
381
|
+
sessionId !== void 0 && !row.current && (0, react_jsx_runtime.jsx)("button", {
|
|
382
|
+
type: "button",
|
|
383
|
+
className: TokenQuotaPanel_module_css_default.selectBtn,
|
|
384
|
+
onClick: () => {
|
|
385
|
+
selectModel(sessionId, row.provider, row.model);
|
|
386
|
+
},
|
|
387
|
+
children: t("select")
|
|
388
|
+
})
|
|
389
|
+
]
|
|
390
|
+
}), (0, react_jsx_runtime.jsxs)("span", {
|
|
391
|
+
className: TokenQuotaPanel_module_css_default.rowMeta,
|
|
392
|
+
children: [row.limit > 0 ? `${formatTokens(row.used)} / ${formatTokens(row.limit)}` : `${formatTokens(row.used)} · ${t("unlimited")}`, (0, react_jsx_runtime.jsx)("button", {
|
|
393
|
+
type: "button",
|
|
394
|
+
className: TokenQuotaPanel_module_css_default.gearBtn,
|
|
395
|
+
title: t("setLimitHint"),
|
|
396
|
+
onClick: () => {
|
|
397
|
+
setEditingKey(editing ? null : row.key);
|
|
398
|
+
},
|
|
399
|
+
children: "⚙"
|
|
400
|
+
})]
|
|
401
|
+
})]
|
|
402
|
+
}),
|
|
403
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
404
|
+
className: TokenQuotaPanel_module_css_default.bar,
|
|
405
|
+
children: (0, react_jsx_runtime.jsx)("div", {
|
|
406
|
+
className: barClass,
|
|
407
|
+
style: { width: `${pct}%` }
|
|
408
|
+
})
|
|
409
|
+
}),
|
|
410
|
+
editing && (0, react_jsx_runtime.jsxs)("div", {
|
|
411
|
+
className: TokenQuotaPanel_module_css_default.controls,
|
|
412
|
+
children: [(0, react_jsx_runtime.jsx)("input", {
|
|
413
|
+
className: TokenQuotaPanel_module_css_default.input,
|
|
414
|
+
type: "number",
|
|
415
|
+
min: 0,
|
|
416
|
+
step: 1e4,
|
|
417
|
+
placeholder: t("limitPlaceholder"),
|
|
418
|
+
value: drafts[row.key] ?? "",
|
|
419
|
+
onChange: (event) => {
|
|
420
|
+
setDrafts((prev) => ({
|
|
421
|
+
...prev,
|
|
422
|
+
[row.key]: event.target.value
|
|
423
|
+
}));
|
|
424
|
+
},
|
|
425
|
+
onKeyDown: (event) => {
|
|
426
|
+
if (event.key === "Enter") commitLimit(row);
|
|
427
|
+
}
|
|
428
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
429
|
+
type: "button",
|
|
430
|
+
className: TokenQuotaPanel_module_css_default.save,
|
|
431
|
+
onClick: () => {
|
|
432
|
+
commitLimit(row);
|
|
433
|
+
},
|
|
434
|
+
children: t("save")
|
|
435
|
+
})]
|
|
436
|
+
})
|
|
437
|
+
]
|
|
438
|
+
}, row.key);
|
|
439
|
+
})
|
|
440
|
+
]
|
|
441
|
+
}),
|
|
442
|
+
dialogOpen && (0, react_jsx_runtime.jsx)("div", {
|
|
443
|
+
className: TokenQuotaPanel_module_css_default.dialogBackdrop,
|
|
444
|
+
onClick: () => {
|
|
445
|
+
actions.setDialogOpen(false);
|
|
446
|
+
},
|
|
447
|
+
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
448
|
+
className: TokenQuotaPanel_module_css_default.dialog,
|
|
449
|
+
onClick: (event) => {
|
|
450
|
+
event.stopPropagation();
|
|
451
|
+
},
|
|
452
|
+
children: [
|
|
453
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
454
|
+
className: TokenQuotaPanel_module_css_default.dialogHeader,
|
|
455
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
456
|
+
className: TokenQuotaPanel_module_css_default.dialogTitle,
|
|
457
|
+
children: t("settingsTitle")
|
|
458
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
459
|
+
type: "button",
|
|
460
|
+
className: TokenQuotaPanel_module_css_default.dialogClose,
|
|
461
|
+
title: t("close"),
|
|
462
|
+
onClick: () => {
|
|
463
|
+
actions.setDialogOpen(false);
|
|
464
|
+
},
|
|
465
|
+
children: "×"
|
|
466
|
+
})]
|
|
467
|
+
}),
|
|
468
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
469
|
+
className: TokenQuotaPanel_module_css_default.dialogSection,
|
|
470
|
+
children: [
|
|
471
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
472
|
+
className: TokenQuotaPanel_module_css_default.dialogLabel,
|
|
473
|
+
children: t("monitorLabel")
|
|
474
|
+
}),
|
|
475
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
476
|
+
className: TokenQuotaPanel_module_css_default.monitorHint,
|
|
477
|
+
children: t("monitorHint")
|
|
478
|
+
}),
|
|
479
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
480
|
+
className: TokenQuotaPanel_module_css_default.monitorList,
|
|
481
|
+
children: allModels.map((model) => (0, react_jsx_runtime.jsxs)("label", {
|
|
482
|
+
className: TokenQuotaPanel_module_css_default.monitorRow,
|
|
483
|
+
children: [(0, react_jsx_runtime.jsx)("input", {
|
|
484
|
+
type: "checkbox",
|
|
485
|
+
checked: monitored === null || monitored.includes(model.key),
|
|
486
|
+
onChange: (event) => {
|
|
487
|
+
const base = monitored === null ? allModels.map((m) => m.key) : monitored;
|
|
488
|
+
const next = new Set(base);
|
|
489
|
+
if (event.target.checked) next.add(model.key);
|
|
490
|
+
else next.delete(model.key);
|
|
491
|
+
setMonitored(next.size === allModels.length ? null : [...next]);
|
|
492
|
+
}
|
|
493
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
494
|
+
className: TokenQuotaPanel_module_css_default.monitorName,
|
|
495
|
+
title: model.key,
|
|
496
|
+
children: model.name
|
|
497
|
+
})]
|
|
498
|
+
}, model.key))
|
|
499
|
+
})
|
|
500
|
+
]
|
|
501
|
+
}),
|
|
502
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
503
|
+
className: TokenQuotaPanel_module_css_default.dialogSection,
|
|
504
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
505
|
+
className: TokenQuotaPanel_module_css_default.dialogLabel,
|
|
506
|
+
children: t("fullActionLabel")
|
|
507
|
+
}), FULL_ACTIONS.map((action) => (0, react_jsx_runtime.jsxs)("label", {
|
|
508
|
+
className: TokenQuotaPanel_module_css_default.radioRow,
|
|
509
|
+
children: [(0, react_jsx_runtime.jsx)("input", {
|
|
510
|
+
type: "radio",
|
|
511
|
+
name: "token-quota-onfull",
|
|
512
|
+
checked: onFull === action.value,
|
|
513
|
+
onChange: () => {
|
|
514
|
+
setOnFull(action.value);
|
|
515
|
+
}
|
|
516
|
+
}), (0, react_jsx_runtime.jsx)("span", { children: t(action.labelKey) })]
|
|
517
|
+
}, action.value))]
|
|
518
|
+
}),
|
|
519
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
520
|
+
className: TokenQuotaPanel_module_css_default.dialogSection,
|
|
521
|
+
children: [
|
|
522
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
523
|
+
className: TokenQuotaPanel_module_css_default.dialogLabel,
|
|
524
|
+
children: t("resetLabel")
|
|
525
|
+
}),
|
|
526
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
527
|
+
className: TokenQuotaPanel_module_css_default.monitorHint,
|
|
528
|
+
children: t("resetHint")
|
|
529
|
+
}),
|
|
530
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
531
|
+
className: TokenQuotaPanel_module_css_default.resetRow,
|
|
532
|
+
children: [
|
|
533
|
+
(0, react_jsx_runtime.jsx)("select", {
|
|
534
|
+
className: TokenQuotaPanel_module_css_default.resetSelect,
|
|
535
|
+
value: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
|
|
536
|
+
onChange: (event) => {
|
|
537
|
+
setReset({
|
|
538
|
+
offsetHours: Number(event.target.value),
|
|
539
|
+
hour: reset?.hour ?? 0,
|
|
540
|
+
minute: reset?.minute ?? 0
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
children: Array.from({ length: 27 }, (_, i) => i - 12).map((offset) => (0, react_jsx_runtime.jsxs)("option", {
|
|
544
|
+
value: offset,
|
|
545
|
+
children: ["UTC", offset >= 0 ? `+${offset}` : offset]
|
|
546
|
+
}, offset))
|
|
547
|
+
}),
|
|
548
|
+
(0, react_jsx_runtime.jsx)("select", {
|
|
549
|
+
className: TokenQuotaPanel_module_css_default.resetSelect,
|
|
550
|
+
value: reset?.hour ?? 0,
|
|
551
|
+
onChange: (event) => {
|
|
552
|
+
setReset({
|
|
553
|
+
offsetHours: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
|
|
554
|
+
hour: Number(event.target.value),
|
|
555
|
+
minute: reset?.minute ?? 0
|
|
556
|
+
});
|
|
557
|
+
},
|
|
558
|
+
children: Array.from({ length: 24 }, (_, i) => i).map((hour) => (0, react_jsx_runtime.jsxs)("option", {
|
|
559
|
+
value: hour,
|
|
560
|
+
children: [String(hour).padStart(2, "0"), " 时"]
|
|
561
|
+
}, hour))
|
|
562
|
+
}),
|
|
563
|
+
(0, react_jsx_runtime.jsx)("select", {
|
|
564
|
+
className: TokenQuotaPanel_module_css_default.resetSelect,
|
|
565
|
+
value: reset?.minute ?? 0,
|
|
566
|
+
onChange: (event) => {
|
|
567
|
+
setReset({
|
|
568
|
+
offsetHours: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
|
|
569
|
+
hour: reset?.hour ?? 0,
|
|
570
|
+
minute: Number(event.target.value)
|
|
571
|
+
});
|
|
572
|
+
},
|
|
573
|
+
children: Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (0, react_jsx_runtime.jsxs)("option", {
|
|
574
|
+
value: minute,
|
|
575
|
+
children: [String(minute).padStart(2, "0"), " 分"]
|
|
576
|
+
}, minute))
|
|
577
|
+
})
|
|
578
|
+
]
|
|
579
|
+
})
|
|
580
|
+
]
|
|
581
|
+
})
|
|
582
|
+
]
|
|
583
|
+
})
|
|
584
|
+
}),
|
|
585
|
+
logOpen && (0, react_jsx_runtime.jsx)("div", {
|
|
586
|
+
className: TokenQuotaPanel_module_css_default.dialogBackdrop,
|
|
587
|
+
onClick: () => {
|
|
588
|
+
actions.setLogOpen(false);
|
|
589
|
+
},
|
|
590
|
+
children: (0, react_jsx_runtime.jsxs)("div", {
|
|
591
|
+
className: TokenQuotaPanel_module_css_default.dialog,
|
|
592
|
+
onClick: (event) => {
|
|
593
|
+
event.stopPropagation();
|
|
594
|
+
},
|
|
595
|
+
children: [
|
|
596
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
597
|
+
className: TokenQuotaPanel_module_css_default.dialogHeader,
|
|
598
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
599
|
+
className: TokenQuotaPanel_module_css_default.dialogTitle,
|
|
600
|
+
children: t("logsTitle")
|
|
601
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
602
|
+
type: "button",
|
|
603
|
+
className: TokenQuotaPanel_module_css_default.dialogClose,
|
|
604
|
+
title: t("close"),
|
|
605
|
+
onClick: () => {
|
|
606
|
+
actions.setLogOpen(false);
|
|
607
|
+
},
|
|
608
|
+
children: "×"
|
|
609
|
+
})]
|
|
610
|
+
}),
|
|
611
|
+
log !== null && log.entries.length === 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
612
|
+
className: TokenQuotaPanel_module_css_default.notice,
|
|
613
|
+
children: t("logEmpty")
|
|
614
|
+
}),
|
|
615
|
+
(0, react_jsx_runtime.jsxs)("table", {
|
|
616
|
+
className: TokenQuotaPanel_module_css_default.logTable,
|
|
617
|
+
children: [(0, react_jsx_runtime.jsx)("thead", { children: (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
618
|
+
(0, react_jsx_runtime.jsx)("th", { children: t("logDay") }),
|
|
619
|
+
(0, react_jsx_runtime.jsx)("th", { children: t("logModel") }),
|
|
620
|
+
(0, react_jsx_runtime.jsx)("th", {
|
|
621
|
+
className: TokenQuotaPanel_module_css_default.logUsedCol,
|
|
622
|
+
children: t("logUsed")
|
|
623
|
+
})
|
|
624
|
+
] }) }), (0, react_jsx_runtime.jsx)("tbody", { children: log?.entries.map((entry) => (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
625
|
+
(0, react_jsx_runtime.jsx)("td", {
|
|
626
|
+
className: TokenQuotaPanel_module_css_default.logDayCol,
|
|
627
|
+
children: entry.day
|
|
628
|
+
}),
|
|
629
|
+
(0, react_jsx_runtime.jsx)("td", {
|
|
630
|
+
className: TokenQuotaPanel_module_css_default.logModelCol,
|
|
631
|
+
title: entry.key,
|
|
632
|
+
children: entry.key
|
|
633
|
+
}),
|
|
634
|
+
(0, react_jsx_runtime.jsx)("td", {
|
|
635
|
+
className: TokenQuotaPanel_module_css_default.logUsedCol,
|
|
636
|
+
children: formatTokens(entry.used)
|
|
637
|
+
})
|
|
638
|
+
] }, `${entry.day}/${entry.key}`)) })]
|
|
639
|
+
})
|
|
640
|
+
]
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
]
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region lib/types/client/locales.js
|
|
648
|
+
/**
|
|
649
|
+
* Token-quota panel copy (zh + en). The panel is a product surface, so the
|
|
650
|
+
* primary copy is Chinese with an English pair, mirroring the shipped UI.
|
|
651
|
+
*
|
|
652
|
+
* @module @jxgame2020/dsh-token-quota/client/locales
|
|
653
|
+
*/
|
|
654
|
+
/** Simplified Chinese copy (primary). */
|
|
655
|
+
const zh = {
|
|
656
|
+
title: "每日 Token 限额",
|
|
657
|
+
subtitle: "按模型实时统计今天的 Token 消耗",
|
|
658
|
+
current: "当前",
|
|
659
|
+
unlimited: "不限",
|
|
660
|
+
usedToday: "今日已用",
|
|
661
|
+
limitLabel: "上限",
|
|
662
|
+
limitPlaceholder: "填数字,0=不限",
|
|
663
|
+
save: "保存",
|
|
664
|
+
select: "选择",
|
|
665
|
+
setLimitHint: "设置限额",
|
|
666
|
+
loading: "加载中…",
|
|
667
|
+
waiting: "等待数据…",
|
|
668
|
+
loadError: "加载失败",
|
|
669
|
+
empty: "暂无模型数据",
|
|
670
|
+
collapse: "收起",
|
|
671
|
+
expand: "展开",
|
|
672
|
+
settings: "设置",
|
|
673
|
+
settingsTitle: "限额设置",
|
|
674
|
+
monitorLabel: "监控模型",
|
|
675
|
+
monitorHint: "未勾选的模型不显示、不计入用量、不受限额限制",
|
|
676
|
+
monitorAll: "全部",
|
|
677
|
+
monitorNone: "无",
|
|
678
|
+
noMonitored: "未监控任何模型",
|
|
679
|
+
fullActionLabel: "满额后处理",
|
|
680
|
+
fullStop: "停止请求并提示",
|
|
681
|
+
fullSwitchQuota: "自动切换到其它限额模型",
|
|
682
|
+
fullSwitchAll: "自动切换到其它可用模型(含非限额)",
|
|
683
|
+
fullSwitchPriority: "自动切换(优先非限额,其次未监控)",
|
|
684
|
+
fullNotice: "当前模型今日额度已用尽,请求已停止。请选择其它模型或调整限额。",
|
|
685
|
+
fullSwitchFailed: "没有可切换的模型(所有限额模型均已满额)",
|
|
686
|
+
close: "关闭",
|
|
687
|
+
logs: "日志",
|
|
688
|
+
logsTitle: "用量日志",
|
|
689
|
+
logEmpty: "暂无记录",
|
|
690
|
+
logDay: "日期",
|
|
691
|
+
logModel: "模型",
|
|
692
|
+
logUsed: "用量",
|
|
693
|
+
resetLabel: "每日重置时间",
|
|
694
|
+
resetHint: "选择时区与时间,到点自动清零当日计数",
|
|
695
|
+
resetOffset: "时区",
|
|
696
|
+
resetTime: "时间"
|
|
697
|
+
};
|
|
698
|
+
/** English copy. */
|
|
699
|
+
const en = {
|
|
700
|
+
title: "Daily Token Quota",
|
|
701
|
+
subtitle: "Real-time per-model token usage today",
|
|
702
|
+
current: "current",
|
|
703
|
+
unlimited: "unlimited",
|
|
704
|
+
usedToday: "used today",
|
|
705
|
+
limitLabel: "limit",
|
|
706
|
+
limitPlaceholder: "number, 0 = unlimited",
|
|
707
|
+
save: "Save",
|
|
708
|
+
select: "Select",
|
|
709
|
+
setLimitHint: "Set limit",
|
|
710
|
+
loading: "Loading…",
|
|
711
|
+
waiting: "Waiting…",
|
|
712
|
+
loadError: "Load failed",
|
|
713
|
+
empty: "No model data",
|
|
714
|
+
collapse: "Collapse",
|
|
715
|
+
expand: "Expand",
|
|
716
|
+
settings: "Settings",
|
|
717
|
+
settingsTitle: "Quota Settings",
|
|
718
|
+
monitorLabel: "Monitored models",
|
|
719
|
+
monitorHint: "Unchecked models are hidden, not metered, and never capped",
|
|
720
|
+
monitorAll: "All",
|
|
721
|
+
monitorNone: "None",
|
|
722
|
+
noMonitored: "No model monitored",
|
|
723
|
+
fullActionLabel: "When a model is full",
|
|
724
|
+
fullStop: "Stop and prompt",
|
|
725
|
+
fullSwitchQuota: "Switch to another quota model",
|
|
726
|
+
fullSwitchAll: "Switch to any other model (incl. uncapped)",
|
|
727
|
+
fullSwitchPriority: "Prefer uncapped, then unmonitored",
|
|
728
|
+
fullNotice: "Today’s quota for the current model is exhausted; the request was stopped. Pick another model or raise its limit.",
|
|
729
|
+
fullSwitchFailed: "No switchable model (all quota models are full)",
|
|
730
|
+
close: "Close",
|
|
731
|
+
logs: "Logs",
|
|
732
|
+
logsTitle: "Usage Log",
|
|
733
|
+
logEmpty: "No records",
|
|
734
|
+
logDay: "Day",
|
|
735
|
+
logModel: "Model",
|
|
736
|
+
logUsed: "Used",
|
|
737
|
+
resetLabel: "Daily reset",
|
|
738
|
+
resetHint: "Pick a timezone and time; counters reset there",
|
|
739
|
+
resetOffset: "Timezone",
|
|
740
|
+
resetTime: "Time"
|
|
741
|
+
};
|
|
742
|
+
//#endregion
|
|
743
|
+
//#region lib/types/client/index.js
|
|
744
|
+
/** Dictionary namespace owning the panel copy. */
|
|
745
|
+
const NS = "token-quota";
|
|
746
|
+
/** Settings namespace the Host quota package owns. */
|
|
747
|
+
const TOKEN_QUOTA_NAMESPACE = "token-quota";
|
|
748
|
+
/** Snapshot pull cadence in milliseconds. */
|
|
749
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
750
|
+
/** Required services: slot registry, connection RPC, locale, settings scope. */
|
|
751
|
+
const inject = [
|
|
752
|
+
"slots",
|
|
753
|
+
"connection",
|
|
754
|
+
"locale",
|
|
755
|
+
"settingsScope"
|
|
756
|
+
];
|
|
757
|
+
/**
|
|
758
|
+
* Client plugin body: poll the Host snapshot route, run the full-quota
|
|
759
|
+
* strategy, wire the injected face (directory load, limit write, model
|
|
760
|
+
* switch, preferences), and register the floating panel into `shell.overlay`.
|
|
761
|
+
* @param ctx - client root context.
|
|
762
|
+
*/
|
|
763
|
+
function apply(ctx) {
|
|
764
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
765
|
+
zh,
|
|
766
|
+
en
|
|
767
|
+
}), "token-quota-ui: panel dictionaries");
|
|
768
|
+
const t = ctx.locale.bind(NS);
|
|
769
|
+
const scope = ctx.settingsScope.bind({ namespace: TOKEN_QUOTA_NAMESPACE });
|
|
770
|
+
const connection = ctx.get("connection");
|
|
771
|
+
const store = createTokenQuotaPanelStore();
|
|
772
|
+
let bound;
|
|
773
|
+
let lastGroups = [];
|
|
774
|
+
let lastCurrent = null;
|
|
775
|
+
let lastSessionId;
|
|
776
|
+
let lastMonitored = null;
|
|
777
|
+
let lastOnFull = "stop";
|
|
778
|
+
let lastReset = null;
|
|
779
|
+
const keyOf = (selection) => {
|
|
780
|
+
if (selection === null) return void 0;
|
|
781
|
+
return `${selection.provider}/${selection.model}`;
|
|
782
|
+
};
|
|
783
|
+
const isMonitoredKey = (key) => lastMonitored === null || lastMonitored.includes(key);
|
|
784
|
+
/** Pick an auto-switch target for the configured strategy. */
|
|
785
|
+
const pickSwitchTarget = (snapshot, currentKey) => {
|
|
786
|
+
const entryByKey = new Map(snapshot.entries.map((entry) => [entry.key, entry]));
|
|
787
|
+
const candidates = [];
|
|
788
|
+
for (const group of lastGroups) for (const model of group.models) {
|
|
789
|
+
const key = `${group.id}/${model.id}`;
|
|
790
|
+
if (key === currentKey) continue;
|
|
791
|
+
const entry = entryByKey.get(key);
|
|
792
|
+
candidates.push({
|
|
793
|
+
key,
|
|
794
|
+
provider: group.id,
|
|
795
|
+
model: model.id,
|
|
796
|
+
limit: entry?.limit ?? 0,
|
|
797
|
+
used: entry?.used ?? 0
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
const available = (candidate) => isMonitoredKey(candidate.key) && (candidate.limit <= 0 || candidate.used < candidate.limit);
|
|
801
|
+
if (lastOnFull === "switchQuota") return candidates.filter((candidate) => isMonitoredKey(candidate.key) && candidate.limit > 0 && candidate.used < candidate.limit).sort((a, b) => a.used / a.limit - b.used / b.limit)[0];
|
|
802
|
+
if (lastOnFull === "switchAll") return candidates.filter(available)[0];
|
|
803
|
+
const uncapped = candidates.find((candidate) => isMonitoredKey(candidate.key) && candidate.limit <= 0);
|
|
804
|
+
if (uncapped !== void 0) return uncapped;
|
|
805
|
+
const unmonitored = candidates.find((candidate) => !isMonitoredKey(candidate.key));
|
|
806
|
+
if (unmonitored !== void 0) return unmonitored;
|
|
807
|
+
return candidates.find(available);
|
|
808
|
+
};
|
|
809
|
+
/** Run the configured full-quota strategy once per exhausted model. */
|
|
810
|
+
const actOnFull = (snapshot) => {
|
|
811
|
+
const currentKey = keyOf(lastCurrent);
|
|
812
|
+
if (currentKey === void 0) return;
|
|
813
|
+
if (!isMonitoredKey(currentKey)) return;
|
|
814
|
+
const entry = snapshot.entries.find((candidate) => candidate.key === currentKey);
|
|
815
|
+
if (entry === void 0 || entry.limit <= 0 || entry.used < entry.limit) return;
|
|
816
|
+
if (lastOnFull === "stop" || lastSessionId === void 0) {
|
|
817
|
+
bound?.setFullNotice(t("fullNotice"));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const target = pickSwitchTarget(snapshot, currentKey);
|
|
821
|
+
if (target === void 0) {
|
|
822
|
+
bound?.setFullNotice(t("fullSwitchFailed"));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
connection.api.sessions.selectModel({
|
|
826
|
+
sessionId: lastSessionId,
|
|
827
|
+
provider: target.provider,
|
|
828
|
+
model: target.model
|
|
829
|
+
}).then(({ result }) => {
|
|
830
|
+
if (result.ok) {
|
|
831
|
+
lastCurrent = result.value.selected;
|
|
832
|
+
bound?.setFullNotice(null);
|
|
833
|
+
pull();
|
|
834
|
+
} else bound?.setFullNotice(t("fullSwitchFailed"));
|
|
835
|
+
}, () => {
|
|
836
|
+
bound?.setFullNotice(t("fullSwitchFailed"));
|
|
837
|
+
});
|
|
838
|
+
};
|
|
839
|
+
let pullCount = 0;
|
|
840
|
+
const refreshCurrent = () => {
|
|
841
|
+
if (lastSessionId === void 0) return;
|
|
842
|
+
connection.api.sessions.models({ sessionId: lastSessionId }).then(({ result }) => {
|
|
843
|
+
if (result.ok) {
|
|
844
|
+
lastGroups = result.value.groups;
|
|
845
|
+
lastCurrent = result.value.current;
|
|
846
|
+
bound?.setDirectory(result.value.groups, result.value.current);
|
|
847
|
+
}
|
|
848
|
+
}, () => {});
|
|
849
|
+
};
|
|
850
|
+
const pull = () => {
|
|
851
|
+
pullCount += 1;
|
|
852
|
+
const doc = scope.getSnapshot().value;
|
|
853
|
+
lastMonitored = doc?.monitored !== void 0 && doc.monitored.length > 0 ? doc.monitored : null;
|
|
854
|
+
lastOnFull = doc?.onFull ?? "stop";
|
|
855
|
+
lastReset = doc?.reset !== void 0 && doc.reset !== null && typeof doc.reset === "object" && typeof doc.reset.offsetHours === "number" ? doc.reset : null;
|
|
856
|
+
bound?.setSettings(lastMonitored, lastOnFull);
|
|
857
|
+
bound?.setReset(lastReset);
|
|
858
|
+
fetch("/token-quota", { headers: { accept: "application/json" } }).then((response) => {
|
|
859
|
+
if (!response.ok) {
|
|
860
|
+
bound?.setError(`quota route: ${String(response.status)}`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
return response.json();
|
|
864
|
+
}, () => {
|
|
865
|
+
bound?.setError("quota snapshot pull failed");
|
|
866
|
+
}).then((snapshot) => {
|
|
867
|
+
if (snapshot === void 0) return;
|
|
868
|
+
bound?.setSnapshot(snapshot);
|
|
869
|
+
actOnFull(snapshot);
|
|
870
|
+
});
|
|
871
|
+
if (pullCount % 4 === 0) refreshCurrent();
|
|
872
|
+
};
|
|
873
|
+
ctx.effect(() => {
|
|
874
|
+
pull();
|
|
875
|
+
const timer = setInterval(pull, POLL_INTERVAL_MS);
|
|
876
|
+
return () => {
|
|
877
|
+
clearInterval(timer);
|
|
878
|
+
};
|
|
879
|
+
}, "token-quota-ui: snapshot poll + full strategy");
|
|
880
|
+
const load = (sessionId) => {
|
|
881
|
+
lastSessionId = sessionId;
|
|
882
|
+
bound?.setLoading(true);
|
|
883
|
+
bound?.setError(null);
|
|
884
|
+
connection.api.sessions.models({ sessionId }).then(({ result }) => {
|
|
885
|
+
if (result.ok) {
|
|
886
|
+
lastGroups = result.value.groups;
|
|
887
|
+
lastCurrent = result.value.current;
|
|
888
|
+
bound?.setDirectory(result.value.groups, result.value.current);
|
|
889
|
+
bound?.setLoading(false);
|
|
890
|
+
} else {
|
|
891
|
+
bound?.setError(`${result.error.code}: ${result.error.message}`);
|
|
892
|
+
bound?.setLoading(false);
|
|
893
|
+
}
|
|
894
|
+
}, () => {
|
|
895
|
+
bound?.setLoading(false);
|
|
896
|
+
bound?.setError("directory load failed");
|
|
897
|
+
});
|
|
898
|
+
};
|
|
899
|
+
const setLimit = (key, limit) => {
|
|
900
|
+
const current = scope.getSnapshot().value?.limits ?? {};
|
|
901
|
+
scope.set("limits", {
|
|
902
|
+
...current,
|
|
903
|
+
[key]: limit
|
|
904
|
+
});
|
|
905
|
+
};
|
|
906
|
+
const setMonitored = (monitored) => {
|
|
907
|
+
lastMonitored = monitored;
|
|
908
|
+
bound?.setSettings(lastMonitored, lastOnFull);
|
|
909
|
+
scope.set("monitored", monitored ?? []);
|
|
910
|
+
};
|
|
911
|
+
const setOnFull = (action) => {
|
|
912
|
+
lastOnFull = action;
|
|
913
|
+
bound?.setSettings(lastMonitored, lastOnFull);
|
|
914
|
+
scope.set("onFull", action);
|
|
915
|
+
};
|
|
916
|
+
const setReset = (reset) => {
|
|
917
|
+
lastReset = reset;
|
|
918
|
+
bound?.setReset(reset);
|
|
919
|
+
scope.set("reset", reset);
|
|
920
|
+
};
|
|
921
|
+
const selectModel = (sessionId, provider, model) => {
|
|
922
|
+
connection.api.sessions.selectModel({
|
|
923
|
+
sessionId,
|
|
924
|
+
provider,
|
|
925
|
+
model
|
|
926
|
+
}).then(({ result }) => {
|
|
927
|
+
if (result.ok) {
|
|
928
|
+
lastCurrent = result.value.selected;
|
|
929
|
+
bound?.setDirectory(lastGroups, result.value.selected);
|
|
930
|
+
pull();
|
|
931
|
+
} else bound?.setError(`${result.error.code}: ${result.error.message}`);
|
|
932
|
+
}, () => {
|
|
933
|
+
bound?.setError("model switch failed");
|
|
934
|
+
});
|
|
935
|
+
};
|
|
936
|
+
const injected = (actions) => {
|
|
937
|
+
bound = actions;
|
|
938
|
+
return {
|
|
939
|
+
load,
|
|
940
|
+
setLimit,
|
|
941
|
+
selectModel,
|
|
942
|
+
setMonitored,
|
|
943
|
+
setOnFull,
|
|
944
|
+
setReset
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
948
|
+
name: "shell.overlay",
|
|
949
|
+
id: "token-quota",
|
|
950
|
+
order: 100,
|
|
951
|
+
label: () => t("title"),
|
|
952
|
+
store,
|
|
953
|
+
locale: NS,
|
|
954
|
+
inject: injected
|
|
955
|
+
}, TokenQuotaPanel));
|
|
956
|
+
}
|
|
957
|
+
//#endregion
|
|
958
|
+
exports.apply = apply;
|
|
959
|
+
exports.inject = inject;
|
|
960
|
+
exports.mergeModelRows = mergeModelRows;
|
|
961
|
+
return module.exports;
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
//# sourceMappingURL=client.js.map
|