@leaf233/dsh-llm-rate-limiter 0.2.0 → 0.3.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/CHANGELOG.md +211 -1
- package/README.md +80 -17
- package/README.zh.md +284 -0
- package/lib/client.js +182 -140
- package/lib/index.js +160 -54
- package/lib/status-rpc.js +227 -9
- package/lib/types/config.js +48 -16
- package/lib/types/index.js +1 -1
- package/package.json +13 -11
- package/COMPATIBILITY.md +0 -150
package/lib/client.js
CHANGED
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dsh-llm-rate-limiter — Client (browser) entry point.
|
|
4
4
|
*
|
|
5
|
-
* Registers
|
|
6
|
-
*
|
|
5
|
+
* Registers the rate limiter's configuration page in the Plugins page's
|
|
6
|
+
* `plugins.item` slot (DSH 0.1.7+). The page draws the card chrome — icon,
|
|
7
|
+
* title, crumb, and the "open" navigation — and asks the entry for two views:
|
|
8
|
+
*
|
|
9
|
+
* - `view: "summary"` → the one-line description on the card;
|
|
10
|
+
* - `view: "page"` → the configuration body plus the live status panel.
|
|
11
|
+
*
|
|
12
|
+
* Configuration values and the write channel come from the page itself
|
|
13
|
+
* (`props.form.state` / `props.form.mutate`), not from a plugin-owned
|
|
14
|
+
* `settingsScope`: `dsh-settings` projects the Host entry's volatile Config
|
|
15
|
+
* fields into one shared form per entry, and the page owns that form.
|
|
7
16
|
*
|
|
8
17
|
* Browser format: CJS-style factory registered via window.__ModuleLoader__.load
|
|
9
18
|
* (loaded as a plain <script>, so NO top-level ESM `export` allowed).
|
|
@@ -23,23 +32,35 @@ window.__ModuleLoader__.load({
|
|
|
23
32
|
({ jsx, jsxs } = require("react/jsx-runtime"));
|
|
24
33
|
|
|
25
34
|
// ── Shared constants ──
|
|
35
|
+
/**
|
|
36
|
+
* Settings namespace = the Host profile entry's id (its
|
|
37
|
+
* `cordis.patch.yml` row id). `dsh-settings` keys every form by
|
|
38
|
+
* `entry.options.id`, so this constant must match the row id.
|
|
39
|
+
*/
|
|
26
40
|
const name = "llm-rate-limiter";
|
|
27
|
-
|
|
41
|
+
/** Locale dictionary namespace owned by this plugin. */
|
|
42
|
+
const NS = "settings.llmRateLimiter";
|
|
43
|
+
/** Static fiber inject: every service this bundle reads. */
|
|
44
|
+
const inject = ["slots", "locale", "configForms"];
|
|
45
|
+
|
|
46
|
+
/* ── locale dictionaries ─────────────────────────────────────────── */
|
|
47
|
+
const zh = {
|
|
48
|
+
title: "LLM 调用限速",
|
|
49
|
+
description: "按模型限制 LLM 请求频率,超出限制的请求排队等待",
|
|
50
|
+
unavailable: "该插件当前未加载,暂时无法配置。",
|
|
51
|
+
readOnly: "本部署的设置为只读。",
|
|
52
|
+
};
|
|
53
|
+
const en = {
|
|
54
|
+
title: "LLM rate limiter",
|
|
55
|
+
description: "Limit per-model LLM request rates; throttled calls wait in queue",
|
|
56
|
+
unavailable: "This plugin is not loaded, so it cannot be configured right now.",
|
|
57
|
+
readOnly: "This deployment stores settings read-only.",
|
|
58
|
+
};
|
|
28
59
|
|
|
29
60
|
/* ── CSS-in-JS (design tokens, injected once) ────────────────────── */
|
|
30
61
|
(() => {
|
|
31
62
|
const styles = [
|
|
32
|
-
".rlr-
|
|
33
|
-
".rlr-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
|
|
34
|
-
".rlr-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
|
|
35
|
-
".rlr-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
|
|
36
|
-
".rlr-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
|
|
37
|
-
".rlr-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
|
|
38
|
-
".rlr-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
|
|
39
|
-
".rlr-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
|
|
40
|
-
".rlr-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}",
|
|
41
|
-
".rlr-chevronOpen{transform:rotate(180deg)}",
|
|
42
|
-
".rlr-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:8px 0 8px}",
|
|
63
|
+
".rlr-page{display:flex;flex-direction:column;gap:4px;list-style:none;margin:0}",
|
|
43
64
|
".rlr-status{color:var(--dsw-alias-label-tertiary);margin:8px 0 4px;font-size:12px;line-height:1.5}",
|
|
44
65
|
".rlr-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:6px 0}",
|
|
45
66
|
".rlr-label{min-width:140px;font-size:13px;color:var(--dsw-alias-label-secondary)}",
|
|
@@ -96,12 +117,6 @@ window.__ModuleLoader__.load({
|
|
|
96
117
|
function readNum(cfg, key, field) {
|
|
97
118
|
return cfg.models?.[key]?.[field] ?? cfg.defaults?.[field] ?? 0;
|
|
98
119
|
}
|
|
99
|
-
function setNested(scope, path, value) {
|
|
100
|
-
scope.mutate([{ op: "set", path, value }]);
|
|
101
|
-
}
|
|
102
|
-
function unsetNested(scope, path) {
|
|
103
|
-
scope.mutate([{ op: "unset", path }]);
|
|
104
|
-
}
|
|
105
120
|
|
|
106
121
|
/* ── status channel access (dsh-context defensive idiom) ─────────── */
|
|
107
122
|
const STATUS_CHANNEL = "/llm-rate-limiter";
|
|
@@ -109,7 +124,7 @@ window.__ModuleLoader__.load({
|
|
|
109
124
|
/**
|
|
110
125
|
* The connection's bound generic-RPC caller, or undefined when the
|
|
111
126
|
* service is absent or hostile. Every read is guarded, so this can
|
|
112
|
-
* never throw — a missing channel degrades the panel, not the
|
|
127
|
+
* never throw — a missing channel degrades the panel, not the page.
|
|
113
128
|
*/
|
|
114
129
|
function rpcCallOf(ctx) {
|
|
115
130
|
try {
|
|
@@ -304,164 +319,191 @@ window.__ModuleLoader__.load({
|
|
|
304
319
|
] });
|
|
305
320
|
}
|
|
306
321
|
|
|
307
|
-
/* ── React component:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
/* ── React component: the settings page (summary + page views) ───── */
|
|
323
|
+
/**
|
|
324
|
+
* Render the one-liner or the configuration body, as the Plugins page
|
|
325
|
+
* asks. Configuration values arrive in `props.form.state` (a
|
|
326
|
+
* `ConfigFormSnapshot`: `{status, value, base, user, revision,
|
|
327
|
+
* writable, mode}`) and writes go through `props.form.mutate(ops)` with
|
|
328
|
+
* the same `{op:'set'|'unset', path, value}` operations the Host
|
|
329
|
+
* validated before. The page owns the form, so it re-renders this entry
|
|
330
|
+
* whenever the settings mirror changes — the component keeps no
|
|
331
|
+
* subscription of its own.
|
|
332
|
+
*
|
|
333
|
+
* @param props - `view`, the page-owned `form`, and the injected
|
|
334
|
+
* `statusCall` (plus the framework's `t` locale seat).
|
|
335
|
+
*/
|
|
336
|
+
function RateLimiterCard(props) {
|
|
337
|
+
const t = typeof props.t === "function" ? props.t : (key) => key;
|
|
338
|
+
|
|
339
|
+
if (props.view === "summary") return jsx("span", { children: t("description") });
|
|
340
|
+
|
|
341
|
+
const form = props.form;
|
|
342
|
+
const snap = form?.state;
|
|
343
|
+
const cfg = snap?.status === "ready" && snap.value ? snap.value : {};
|
|
344
|
+
const writable = snap === undefined ? true : snap.writable !== false;
|
|
345
|
+
const statusCall = props.statusCall;
|
|
346
|
+
|
|
347
|
+
/** Queue one atomic namespace mutation; failures leave the form as-is. */
|
|
348
|
+
function push(ops) {
|
|
349
|
+
if (!form || typeof form.mutate !== "function") return;
|
|
350
|
+
try {
|
|
351
|
+
Promise.resolve(form.mutate(ops)).catch(() => undefined);
|
|
352
|
+
} catch {}
|
|
353
|
+
}
|
|
354
|
+
const setPath = (path, value) => push([{ op: "set", path, value }]);
|
|
355
|
+
const unsetPath = (path) => push([{ op: "unset", path }]);
|
|
321
356
|
|
|
322
357
|
const modelKeys = Object.keys(cfg.models ?? {});
|
|
323
358
|
|
|
324
359
|
function addModel() {
|
|
325
|
-
const key = prompt(
|
|
360
|
+
const key = prompt("添加模型限速 (格式: provider/model)\n例如: deepseek/deepseek-chat");
|
|
326
361
|
if (!key) return;
|
|
327
|
-
|
|
362
|
+
setPath(["models", key], {});
|
|
328
363
|
}
|
|
329
364
|
function removeModel(key) {
|
|
330
|
-
|
|
365
|
+
unsetPath(["models", key]);
|
|
331
366
|
}
|
|
332
367
|
function setModelField(key, field, value) {
|
|
333
368
|
const path = ["models", key, field];
|
|
334
|
-
if (value === undefined || value === "")
|
|
335
|
-
else
|
|
369
|
+
if (value === undefined || value === "") unsetPath(path);
|
|
370
|
+
else setPath(path, value);
|
|
336
371
|
}
|
|
337
372
|
function setDefault(field, value) {
|
|
338
|
-
|
|
373
|
+
setPath(["defaults", field], value);
|
|
339
374
|
}
|
|
340
375
|
function setTop(field, value) {
|
|
341
|
-
|
|
376
|
+
setPath([field], value);
|
|
342
377
|
}
|
|
343
378
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
379
|
+
return jsxs("div", { className: "rlr-page", children: [
|
|
380
|
+
/* ── unavailable / read-only notices ── */
|
|
381
|
+
snap?.status === "unavailable"
|
|
382
|
+
? jsx("div", { className: "rlr-status", children: t("unavailable") })
|
|
383
|
+
: null,
|
|
384
|
+
snap !== undefined && !writable
|
|
385
|
+
? jsx("div", { className: "rlr-status", children: t("readOnly") })
|
|
386
|
+
: null,
|
|
387
|
+
|
|
388
|
+
/* ── 实时状态面板 ── */
|
|
389
|
+
jsx(RateLimiterStatus, { statusCall, cfg }),
|
|
390
|
+
|
|
391
|
+
/* ── 启用开关 ── */
|
|
392
|
+
jsxs("div", { className: "rlr-row", children: [
|
|
393
|
+
jsx("label", { children: [
|
|
394
|
+
jsx("input", { type: "checkbox", checked: cfg.enabled ?? true, disabled: !writable, onChange: (e) => setTop("enabled", e.target.checked) }),
|
|
395
|
+
" 启用限速",
|
|
396
|
+
] }),
|
|
397
|
+
] }),
|
|
398
|
+
/* ── 默认配置 ── */
|
|
399
|
+
jsxs("div", { className: "rlr-section", children: [
|
|
400
|
+
jsx("div", { className: "rlr-sectHead", children: "默认配置" }),
|
|
364
401
|
jsxs("div", { className: "rlr-row", children: [
|
|
365
|
-
jsx("
|
|
366
|
-
|
|
367
|
-
"
|
|
402
|
+
jsx("span", { className: "rlr-label", children: "策略" }),
|
|
403
|
+
jsxs("select", { className: "rlr-select", value: cfg.strategy ?? "token-bucket", disabled: !writable, onChange: (e) => setTop("strategy", e.target.value), children: [
|
|
404
|
+
jsx("option", { value: "token-bucket", children: "令牌桶 (Token Bucket)" }),
|
|
405
|
+
jsx("option", { value: "sliding-window", children: "滑动窗口 (Sliding Window)" }),
|
|
368
406
|
] }),
|
|
369
407
|
] }),
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
jsx("
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
] }),
|
|
408
|
+
jsxs("div", { className: "rlr-row", children: [
|
|
409
|
+
jsx("span", { className: "rlr-label", children: "最大并发数" }),
|
|
410
|
+
jsx("input", { type: "number", min: 1, className: "rlr-input", disabled: !writable, value: cfg.defaults?.maxConcurrent ?? 5, onChange: (e) => setDefault("maxConcurrent", Number(e.target.value) || 1) }),
|
|
411
|
+
] }),
|
|
412
|
+
jsxs("div", { className: "rlr-row", children: [
|
|
413
|
+
jsx("span", { className: "rlr-label", children: "每分钟最大请求数" }),
|
|
414
|
+
jsx("input", { type: "number", min: 1, className: "rlr-input", disabled: !writable, value: cfg.defaults?.maxRpm ?? 60, onChange: (e) => setDefault("maxRpm", Number(e.target.value) || 1) }),
|
|
415
|
+
] }),
|
|
416
|
+
cfg.strategy !== "sliding-window" && jsxs(react.Fragment, { children: [
|
|
380
417
|
jsxs("div", { className: "rlr-row", children: [
|
|
381
|
-
jsx("span", { className: "rlr-label", children: "
|
|
382
|
-
jsx("input", { type: "number", min: 1, className: "rlr-input", value: cfg.defaults?.
|
|
418
|
+
jsx("span", { className: "rlr-label", children: "突发容量" }),
|
|
419
|
+
jsx("input", { type: "number", min: 1, className: "rlr-input", disabled: !writable, value: cfg.defaults?.burstSize ?? 10, onChange: (e) => setDefault("burstSize", Number(e.target.value) || 1) }),
|
|
383
420
|
] }),
|
|
384
421
|
jsxs("div", { className: "rlr-row", children: [
|
|
385
|
-
jsx("span", { className: "rlr-label", children: "
|
|
386
|
-
jsx("input", { type: "number", min: 1, className: "rlr-input", value: cfg.defaults?.
|
|
387
|
-
] }),
|
|
388
|
-
cfg.strategy !== "sliding-window" && jsxs(react.Fragment, { children: [
|
|
389
|
-
jsxs("div", { className: "rlr-row", children: [
|
|
390
|
-
jsx("span", { className: "rlr-label", children: "突发容量" }),
|
|
391
|
-
jsx("input", { type: "number", min: 1, className: "rlr-input", value: cfg.defaults?.burstSize ?? 10, onChange: (e) => setDefault("burstSize", Number(e.target.value) || 1) }),
|
|
392
|
-
] }),
|
|
393
|
-
jsxs("div", { className: "rlr-row", children: [
|
|
394
|
-
jsx("span", { className: "rlr-label", children: "补充速率 (个/秒)" }),
|
|
395
|
-
jsx("input", { type: "number", min: 0.1, step: 0.1, className: "rlr-input", value: cfg.defaults?.refillRate ?? 1, onChange: (e) => setDefault("refillRate", Number(e.target.value) || 0.1) }),
|
|
396
|
-
] }),
|
|
422
|
+
jsx("span", { className: "rlr-label", children: "补充速率 (个/秒)" }),
|
|
423
|
+
jsx("input", { type: "number", min: 0.1, step: 0.1, className: "rlr-input", disabled: !writable, value: cfg.defaults?.refillRate ?? 1, onChange: (e) => setDefault("refillRate", Number(e.target.value) || 0.1) }),
|
|
397
424
|
] }),
|
|
398
425
|
] }),
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
modelKeys.length === 0 && jsx("p", { className: "rlr-status", children: "未配置模型专属限速,所有模型使用默认值。" }),
|
|
406
|
-
modelKeys.map((key) => jsxs("div", { className: "rlr-modelRow", key, children: [
|
|
407
|
-
jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4, flex: 1 }, children: [
|
|
408
|
-
jsx("span", { className: "rlr-modelName", children: key }),
|
|
409
|
-
jsxs("div", { className: "rlr-modelMeta", children: [
|
|
410
|
-
jsx("span", { children: `并发: ${readNum(cfg, key, "maxConcurrent")}` }),
|
|
411
|
-
jsx("span", { children: `RPM: ${readNum(cfg, key, "maxRpm")}` }),
|
|
412
|
-
cfg.strategy !== "sliding-window" && jsx("span", { children: `突发: ${readNum(cfg, key, "burstSize")}` }),
|
|
413
|
-
] }),
|
|
414
|
-
jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 4 }, children: [
|
|
415
|
-
jsx("input", { type: "number", min: 1, className: "rlr-input", style: { width: 60 }, title: "并发", placeholder: "并发", value: cfg.models?.[key]?.maxConcurrent ?? "", onChange: (e) => setModelField(key, "maxConcurrent", e.target.value === "" ? undefined : Number(e.target.value) || 1) }),
|
|
416
|
-
jsx("input", { type: "number", min: 1, className: "rlr-input", style: { width: 60 }, title: "RPM", placeholder: "RPM", value: cfg.models?.[key]?.maxRpm ?? "", onChange: (e) => setModelField(key, "maxRpm", e.target.value === "" ? undefined : Number(e.target.value) || 1) }),
|
|
417
|
-
cfg.strategy !== "sliding-window" && jsx("input", { type: "number", min: 1, className: "rlr-input", style: { width: 60 }, title: "突发", placeholder: "突发", value: cfg.models?.[key]?.burstSize ?? "", onChange: (e) => setModelField(key, "burstSize", e.target.value === "" ? undefined : Number(e.target.value) || 1) }),
|
|
418
|
-
jsx("button", { className: "rlr-btnSec", onClick: () => { const v = cfg.models?.[key]?.enabled; setModelField(key, "enabled", v === false ? undefined : false); }, children: cfg.models?.[key]?.enabled === false ? "已禁用" : "禁用" }),
|
|
419
|
-
] }),
|
|
420
|
-
] }),
|
|
421
|
-
jsx("button", { className: "rlr-danger", onClick: () => removeModel(key), title: "删除", children: "✕" }),
|
|
422
|
-
] })),
|
|
426
|
+
] }),
|
|
427
|
+
/* ── 模型专属配置 ── */
|
|
428
|
+
jsxs("div", { className: "rlr-section", children: [
|
|
429
|
+
jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
|
|
430
|
+
jsx("div", { className: "rlr-sectHead", children: "模型专属配置" }),
|
|
431
|
+
jsx("button", { className: "rlr-btn", disabled: !writable, onClick: addModel, children: "+ 添加模型" }),
|
|
423
432
|
] }),
|
|
424
|
-
|
|
425
|
-
jsxs("div", { className: "rlr-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
jsx("
|
|
430
|
-
"
|
|
433
|
+
modelKeys.length === 0 && jsx("p", { className: "rlr-status", children: "未配置模型专属限速,所有模型使用默认值。" }),
|
|
434
|
+
modelKeys.map((key) => jsxs("div", { className: "rlr-modelRow", key, children: [
|
|
435
|
+
jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4, flex: 1 }, children: [
|
|
436
|
+
jsx("span", { className: "rlr-modelName", children: key }),
|
|
437
|
+
jsxs("div", { className: "rlr-modelMeta", children: [
|
|
438
|
+
jsx("span", { children: "并发: " + readNum(cfg, key, "maxConcurrent") }),
|
|
439
|
+
jsx("span", { children: "RPM: " + readNum(cfg, key, "maxRpm") }),
|
|
440
|
+
cfg.strategy !== "sliding-window" && jsx("span", { children: "突发: " + readNum(cfg, key, "burstSize") }),
|
|
431
441
|
] }),
|
|
432
|
-
|
|
433
|
-
jsx("input", { type: "
|
|
434
|
-
"
|
|
442
|
+
jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 4 }, children: [
|
|
443
|
+
jsx("input", { type: "number", min: 1, className: "rlr-input", disabled: !writable, style: { width: 60 }, title: "并发", placeholder: "并发", value: cfg.models?.[key]?.maxConcurrent ?? "", onChange: (e) => setModelField(key, "maxConcurrent", e.target.value === "" ? undefined : Number(e.target.value) || 1) }),
|
|
444
|
+
jsx("input", { type: "number", min: 1, className: "rlr-input", disabled: !writable, style: { width: 60 }, title: "RPM", placeholder: "RPM", value: cfg.models?.[key]?.maxRpm ?? "", onChange: (e) => setModelField(key, "maxRpm", e.target.value === "" ? undefined : Number(e.target.value) || 1) }),
|
|
445
|
+
cfg.strategy !== "sliding-window" && jsx("input", { type: "number", min: 1, className: "rlr-input", disabled: !writable, style: { width: 60 }, title: "突发", placeholder: "突发", value: cfg.models?.[key]?.burstSize ?? "", onChange: (e) => setModelField(key, "burstSize", e.target.value === "" ? undefined : Number(e.target.value) || 1) }),
|
|
446
|
+
jsx("button", { className: "rlr-btnSec", disabled: !writable, onClick: () => { const v = cfg.models?.[key]?.enabled; setModelField(key, "enabled", v === false ? undefined : false); }, children: cfg.models?.[key]?.enabled === false ? "已禁用" : "禁用" }),
|
|
435
447
|
] }),
|
|
436
448
|
] }),
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
449
|
+
jsx("button", { className: "rlr-danger", disabled: !writable, onClick: () => removeModel(key), title: "删除", children: "✕" }),
|
|
450
|
+
] })),
|
|
451
|
+
] }),
|
|
452
|
+
/* ── 被限速时的行为 ── */
|
|
453
|
+
jsxs("div", { className: "rlr-section", children: [
|
|
454
|
+
jsx("div", { className: "rlr-sectHead", children: "被限速时的行为" }),
|
|
455
|
+
jsxs("div", { className: "rlr-row", children: [
|
|
456
|
+
jsx("label", { style: { display: "flex", alignItems: "center", gap: 4, cursor: "pointer" }, children: [
|
|
457
|
+
jsx("input", { type: "radio", name: "rl-on-throttle", disabled: !writable, checked: cfg.onThrottled !== "reject", onChange: () => setTop("onThrottled", "queue") }),
|
|
458
|
+
"排队等待",
|
|
440
459
|
] }),
|
|
460
|
+
jsx("label", { style: { display: "flex", alignItems: "center", gap: 4, cursor: "pointer" }, children: [
|
|
461
|
+
jsx("input", { type: "radio", name: "rl-on-throttle", disabled: !writable, checked: cfg.onThrottled === "reject", onChange: () => setTop("onThrottled", "reject") }),
|
|
462
|
+
"拒绝请求",
|
|
463
|
+
] }),
|
|
464
|
+
] }),
|
|
465
|
+
cfg.onThrottled !== "reject" && jsxs("div", { className: "rlr-row", children: [
|
|
466
|
+
jsx("span", { className: "rlr-label", children: "最大排队等待 (ms)" }),
|
|
467
|
+
jsx("input", { type: "number", min: 1000, step: 1000, className: "rlr-input", disabled: !writable, value: cfg.maxQueueWaitMs ?? 60000, onChange: (e) => setTop("maxQueueWaitMs", Number(e.target.value) || 60000) }),
|
|
441
468
|
] }),
|
|
442
469
|
] }),
|
|
443
470
|
] });
|
|
444
471
|
}
|
|
445
472
|
|
|
446
473
|
/* ── Plugin registration ── */
|
|
474
|
+
/**
|
|
475
|
+
* Mount the configuration page while the Host serves this entry's
|
|
476
|
+
* namespace, and register the plugin's own dictionaries.
|
|
477
|
+
*
|
|
478
|
+
* @param ctx - the browser plugin context.
|
|
479
|
+
*/
|
|
447
480
|
function apply(ctx) {
|
|
448
|
-
const
|
|
449
|
-
ctx.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
481
|
+
const t = ctx.locale.bind(NS);
|
|
482
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "llm-rate-limiter: dictionaries");
|
|
483
|
+
|
|
484
|
+
// `whileServed` keeps the card off the page entirely unless the Host
|
|
485
|
+
// exposes this namespace: `dsh-settings` drops any entry whose Config
|
|
486
|
+
// has no volatile field (`lib/index.js:419`), so an entry that never
|
|
487
|
+
// appears there has nothing to configure.
|
|
488
|
+
ctx.effect(() => ctx.configForms.whileServed([name], () => ctx.slots.inject("plugins.item", () => ctx.slots.register({
|
|
489
|
+
name: "plugins.item",
|
|
490
|
+
id: name,
|
|
491
|
+
// Official `plugins.item` occupants take 10/20/30/40; 900 parks
|
|
492
|
+
// this third-party card after all of them.
|
|
493
|
+
order: 900,
|
|
494
|
+
label: () => t("title"),
|
|
495
|
+
locale: NS,
|
|
496
|
+
// `statusCall` is resolved per render (never cached) so a
|
|
497
|
+
// late-arriving or hot-reloaded connection service is picked up
|
|
498
|
+
// without re-registering the page.
|
|
499
|
+
inject: () => ({ statusCall: () => rpcCallOf(ctx) }),
|
|
500
|
+
}, RateLimiterCard))), "llm-rate-limiter: plugins page");
|
|
460
501
|
}
|
|
461
502
|
|
|
462
503
|
exports.apply = apply;
|
|
463
504
|
exports.inject = inject;
|
|
464
505
|
exports.name = name;
|
|
506
|
+
exports.NS = NS;
|
|
465
507
|
return module.exports;
|
|
466
|
-
}
|
|
467
|
-
});
|
|
508
|
+
},
|
|
509
|
+
});
|