@leaves615/dsh-llm-ctl 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.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * React views for the two `settings.models` slots.
3
+ *
4
+ * The client bundle runs inside the DSH ModuleLoader, so React arrives through
5
+ * `require('react')`; the build script rewrites these imports. Everything here
6
+ * is a pure view over a small state object plus callbacks, so the data shaping is
7
+ * testable without a browser.
8
+ *
9
+ * @module dsh-llm-ctl/settings-ui
10
+ */
11
+ import React from 'react';
12
+ import type { VisibilitySettings } from './visibility.ts';
13
+ /** One provider row of the settings page view model. */
14
+ export interface ProviderVisibilityView {
15
+ provider: string;
16
+ displayName: string;
17
+ visible: boolean;
18
+ models: Array<{
19
+ model: string;
20
+ name: string;
21
+ visible: boolean;
22
+ }>;
23
+ }
24
+ /** Inputs for the settings page view model. */
25
+ export interface VisibilityViewInput {
26
+ providers: ReadonlyArray<{
27
+ provider: string;
28
+ displayName: string;
29
+ models: ReadonlyArray<{
30
+ model: string;
31
+ name: string;
32
+ }>;
33
+ }>;
34
+ settings: VisibilitySettings;
35
+ patterns: readonly string[];
36
+ }
37
+ /** Actions the settings views call back into. */
38
+ export interface VisibilityActions {
39
+ setProvider(provider: string, visible: boolean): void;
40
+ setModel(provider: string, model: string, visible: boolean): void;
41
+ resetAll(): void;
42
+ openQueue?(): void;
43
+ }
44
+ /** Effective global queue budget as the plugin-config card renders it. */
45
+ export interface QueueConfigView {
46
+ /** Effective wait budget in ms (settings override wins over the cordis base). */
47
+ maxWaitMs: number;
48
+ /** Effective queue depth cap. */
49
+ maxQueueDepth: number;
50
+ /** Effective default per-provider concurrency; `0` means unlimited. */
51
+ defaultConcurrency: number;
52
+ /** Effective provider-specific entries, excluding `default`; `0` means unlimited. */
53
+ perProviderConcurrency: Record<string, number>;
54
+ /** Cordis composition base, before the user-layer override. */
55
+ defaults: {
56
+ maxWaitMs: number;
57
+ maxQueueDepth: number;
58
+ defaultConcurrency: number;
59
+ };
60
+ /** True when at least one field carries a user-layer override. */
61
+ overridden: boolean;
62
+ /** Section revision the snapshot was read at, for write fencing. */
63
+ revision: number;
64
+ }
65
+ /** Outcome of one queue-override write; `error` carries the server message when present. */
66
+ export interface QueueWriteResult {
67
+ ok: boolean;
68
+ error?: string;
69
+ }
70
+ /** Actions the plugin-config card calls back into. */
71
+ export interface QueueConfigActions {
72
+ /** Persist a partial override; each defined field replaces the current one. */
73
+ setQueue(input: {
74
+ maxWaitMs?: number;
75
+ maxQueueDepth?: number;
76
+ defaultConcurrency?: number;
77
+ perProviderConcurrency?: Record<string, number>;
78
+ expectedRevision?: number;
79
+ }): Promise<QueueWriteResult>;
80
+ /** Drop the override, re-inheriting the cordis composition base. */
81
+ resetQueue(expectedRevision?: number): Promise<boolean>;
82
+ }
83
+ /** Provider directory entry the concurrency editor offers per-provider rows for. */
84
+ export interface QueueProviderOption {
85
+ provider: string;
86
+ displayName: string;
87
+ }
88
+ /**
89
+ * Build the per-provider view model the settings cards render.
90
+ *
91
+ * @param input - Provider entries, visibility settings, and preset patterns.
92
+ * @returns Array of provider views with computed visibility.
93
+ */
94
+ export declare function buildProviderViews(input: VisibilityViewInput): ProviderVisibilityView[];
95
+ /**
96
+ * Count hidden providers and models across the view model.
97
+ *
98
+ * @param views - Provider views to summarize.
99
+ * @returns Counts of hidden providers, hidden models, and their sum.
100
+ */
101
+ export declare function summarize(views: readonly ProviderVisibilityView[]): {
102
+ providers: number;
103
+ models: number;
104
+ total: number;
105
+ };
106
+ /** Lists longer than this start collapsed so a 70-model provider never floods the page. */
107
+ export declare const COLLAPSE_THRESHOLD = 8;
108
+ /**
109
+ * Whether a list of the given length starts expanded.
110
+ *
111
+ * @param count Row count of the list.
112
+ * @returns True for short lists; long lists start collapsed.
113
+ */
114
+ export declare function defaultExpanded(count: number): boolean;
115
+ /**
116
+ * Collapsed flag that follows list growth until the user toggles manually.
117
+ * Must be called unconditionally (hooks rule), even by components that may
118
+ * return null below.
119
+ *
120
+ * @param count Current row count of the list.
121
+ * @returns The effective collapsed flag plus its toggle.
122
+ */
123
+ export declare function useAutoCollapse(count: number): [boolean, () => void];
124
+ /**
125
+ * Chevron toggle shared by collapsible model lists.
126
+ *
127
+ * @param props Collapsed flag, row count, section label, and toggle callback.
128
+ * @returns The rendered button element.
129
+ */
130
+ export declare function CollapseToggle(props: {
131
+ collapsed: boolean;
132
+ count: number;
133
+ label: string;
134
+ onToggle: () => void;
135
+ }): React.ReactElement;
136
+ /**
137
+ * Provider-card extras: one switch for the provider and one per model.
138
+ * When the provider has no models in the catalog, renders the provider switch
139
+ * plus a fallback message indicating per-model control is unavailable.
140
+ *
141
+ * @param props - View model for this provider plus the write actions.
142
+ * @returns The rendered element.
143
+ */
144
+ export declare function ProviderVisibilityCard(props: {
145
+ view: ProviderVisibilityView;
146
+ actions: VisibilityActions;
147
+ }): React.ReactElement;
148
+ /**
149
+ * Models-page footer: hidden totals, restore-all, and queue pressure.
150
+ *
151
+ * @param props - Summarized counts, queue state, and actions.
152
+ * @returns The rendered element.
153
+ */
154
+ export declare function VisibilityFooter(props: {
155
+ summary: {
156
+ providers: number;
157
+ models: number;
158
+ total: number;
159
+ };
160
+ queue: {
161
+ queued: number;
162
+ cooling: number;
163
+ };
164
+ actions: VisibilityActions;
165
+ }): React.ReactElement;
166
+ /**
167
+ * One plugin card inside the 插件配置 tab (`settings.plugin.item`, keyed by
168
+ * the settings namespace `llm-ctl`).
169
+ *
170
+ * The tab renders every card inside its own `<ul>` and dispatches this seat
171
+ * with empty owner props, so the card owns its whole surface and nests as an
172
+ * `<li>`: a header naming the plugin, a body of staged fields, and the
173
+ * save/discard that writes them. Edits are staged locally and only reach the
174
+ * Host on save, fenced by the revision the card was rendered from.
175
+ *
176
+ * @param props - effective budget, cordis base, and the write actions.
177
+ * @returns The rendered card.
178
+ */
179
+ export declare function PluginConfigCard(props: {
180
+ config: QueueConfigView;
181
+ providers?: QueueProviderOption[];
182
+ actions: QueueConfigActions;
183
+ }): React.ReactElement;
@@ -0,0 +1,367 @@
1
+ /**
2
+ * React views for the two `settings.models` slots.
3
+ *
4
+ * The client bundle runs inside the DSH ModuleLoader, so React arrives through
5
+ * `require('react')`; the build script rewrites these imports. Everything here
6
+ * is a pure view over a small state object plus callbacks, so the data shaping is
7
+ * testable without a browser.
8
+ *
9
+ * @module dsh-llm-ctl/settings-ui
10
+ */
11
+ import React from 'react';
12
+ import { isModelVisible, isProviderVisible } from "./visibility.js";
13
+ import { concurrencyFor } from "./concurrency.js";
14
+ /**
15
+ * Build the per-provider view model the settings cards render.
16
+ *
17
+ * @param input - Provider entries, visibility settings, and preset patterns.
18
+ * @returns Array of provider views with computed visibility.
19
+ */
20
+ export function buildProviderViews(input) {
21
+ const config = { hiddenPatterns: input.patterns };
22
+ return input.providers.map((entry) => ({
23
+ provider: entry.provider,
24
+ displayName: entry.displayName,
25
+ visible: isProviderVisible(entry.provider, input.settings, config),
26
+ models: entry.models.map((model) => ({
27
+ model: model.model,
28
+ name: model.name,
29
+ visible: isModelVisible(entry.provider, model.model, input.settings, config),
30
+ })),
31
+ }));
32
+ }
33
+ /**
34
+ * Count hidden providers and models across the view model.
35
+ *
36
+ * @param views - Provider views to summarize.
37
+ * @returns Counts of hidden providers, hidden models, and their sum.
38
+ */
39
+ export function summarize(views) {
40
+ let providers = 0;
41
+ let models = 0;
42
+ for (const view of views) {
43
+ if (!view.visible)
44
+ providers += 1;
45
+ for (const model of view.models)
46
+ if (!model.visible)
47
+ models += 1;
48
+ }
49
+ return { providers, models, total: providers + models };
50
+ }
51
+ const rowStyle = { display: 'flex', alignItems: 'center', gap: 8, padding: '2px 0' };
52
+ const nameStyle = { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
53
+ const buttonStyle = {
54
+ border: '1px solid var(--dsw-alias-border-inverted, rgba(127,127,127,.35))',
55
+ background: 'transparent',
56
+ color: 'inherit',
57
+ borderRadius: 6,
58
+ padding: '1px 8px',
59
+ fontSize: 11,
60
+ cursor: 'pointer',
61
+ };
62
+ /** Lists longer than this start collapsed so a 70-model provider never floods the page. */
63
+ export const COLLAPSE_THRESHOLD = 8;
64
+ /**
65
+ * Whether a list of the given length starts expanded.
66
+ *
67
+ * @param count Row count of the list.
68
+ * @returns True for short lists; long lists start collapsed.
69
+ */
70
+ export function defaultExpanded(count) {
71
+ return count <= COLLAPSE_THRESHOLD;
72
+ }
73
+ /**
74
+ * Collapsed flag that follows list growth until the user toggles manually.
75
+ * Must be called unconditionally (hooks rule), even by components that may
76
+ * return null below.
77
+ *
78
+ * @param count Current row count of the list.
79
+ * @returns The effective collapsed flag plus its toggle.
80
+ */
81
+ export function useAutoCollapse(count) {
82
+ const [manual, setManual] = React.useState(undefined);
83
+ const collapsed = manual ?? !defaultExpanded(count);
84
+ const toggle = () => {
85
+ setManual(!collapsed);
86
+ };
87
+ return [collapsed, toggle];
88
+ }
89
+ /**
90
+ * Chevron toggle shared by collapsible model lists.
91
+ *
92
+ * @param props Collapsed flag, row count, section label, and toggle callback.
93
+ * @returns The rendered button element.
94
+ */
95
+ export function CollapseToggle(props) {
96
+ return React.createElement('button', {
97
+ type: 'button',
98
+ style: buttonStyle,
99
+ title: props.collapsed ? '展开' : '收起',
100
+ 'aria-expanded': !props.collapsed,
101
+ onClick: () => props.onToggle(),
102
+ }, (props.collapsed ? '▸ ' : '▾ ') + props.label + '(' + props.count + ')');
103
+ }
104
+ /** Eye toggle shared by provider and model rows. */
105
+ function toggle(label, visible, onToggle) {
106
+ return React.createElement('button', {
107
+ type: 'button',
108
+ style: buttonStyle,
109
+ title: visible ? '隐藏' : '显示',
110
+ 'aria-pressed': !visible,
111
+ onClick: onToggle,
112
+ }, visible ? '👁' : '🚫', label.length > 0 ? React.createElement('span', { style: { marginLeft: 4 } }, label) : null);
113
+ }
114
+ /**
115
+ * Provider-card extras: one switch for the provider and one per model.
116
+ * When the provider has no models in the catalog, renders the provider switch
117
+ * plus a fallback message indicating per-model control is unavailable.
118
+ *
119
+ * @param props - View model for this provider plus the write actions.
120
+ * @returns The rendered element.
121
+ */
122
+ export function ProviderVisibilityCard(props) {
123
+ const { view, actions } = props;
124
+ const [modelsCollapsed, toggleModels] = useAutoCollapse(view.models.length);
125
+ return React.createElement('div', { style: { marginTop: 8, fontSize: 12 } }, React.createElement('div', { style: rowStyle }, React.createElement('span', { style: nameStyle }, view.visible ? '提供方可见' : '提供方已隐藏'), toggle('', view.visible, () => actions.setProvider(view.provider, !view.visible))), view.models.length > 0
126
+ ? React.createElement(React.Fragment, null, React.createElement('div', { style: { ...rowStyle, paddingLeft: 12 } }, CollapseToggle({ collapsed: modelsCollapsed, count: view.models.length, label: '模型', onToggle: toggleModels })), modelsCollapsed
127
+ ? null
128
+ : view.models.map((model) => React.createElement('div', { key: model.model, style: { ...rowStyle, paddingLeft: 12, opacity: model.visible ? 1 : 0.5 } }, React.createElement('span', { style: nameStyle }, model.name), toggle('', model.visible, () => actions.setModel(view.provider, model.model, !model.visible)))))
129
+ : React.createElement('div', { style: { ...rowStyle, paddingLeft: 12, opacity: 0.6, fontSize: 11 } }, '该提供方暂无模型列表,无法逐模型控制'));
130
+ }
131
+ /**
132
+ * Models-page footer: hidden totals, restore-all, and queue pressure.
133
+ *
134
+ * @param props - Summarized counts, queue state, and actions.
135
+ * @returns The rendered element.
136
+ */
137
+ export function VisibilityFooter(props) {
138
+ const { summary, queue, actions } = props;
139
+ const parts = [];
140
+ if (summary.total === 0)
141
+ parts.push('没有隐藏的模型');
142
+ else
143
+ parts.push(`已隐藏 ${summary.providers} 个提供方 / ${summary.models} 个模型`);
144
+ if (queue.queued > 0 || queue.cooling > 0)
145
+ parts.push(`排队 ${queue.queued} · 冷却 ${queue.cooling}`);
146
+ return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 0', fontSize: 12 } }, React.createElement('span', { style: nameStyle }, parts.join(' · ')), summary.total > 0
147
+ ? React.createElement('button', { type: 'button', style: buttonStyle, onClick: () => actions.resetAll() }, '全部恢复')
148
+ : null);
149
+ }
150
+ /** Card chrome mirroring the built-in `settings.plugin.item` cards. */
151
+ const pluginCardStyle = {
152
+ border: '0.5px solid var(--dsw-alias-border-l4, rgba(127,127,127,.28))',
153
+ background: 'var(--dsw-alias-bg-layer-3, transparent)',
154
+ borderRadius: 16,
155
+ listStyle: 'none',
156
+ };
157
+ const pluginHeaderStyle = {
158
+ appearance: 'none',
159
+ width: '100%',
160
+ font: 'inherit',
161
+ color: 'inherit',
162
+ textAlign: 'left',
163
+ cursor: 'pointer',
164
+ background: 'none',
165
+ border: 0,
166
+ borderRadius: 12,
167
+ display: 'flex',
168
+ alignItems: 'center',
169
+ gap: 12,
170
+ padding: '14px 16px',
171
+ };
172
+ const pluginHeadTextStyle = { display: 'flex', flexDirection: 'column', flex: 1, gap: 4, minWidth: 0 };
173
+ const pluginNameStyle = { color: 'var(--dsw-alias-label-primary, inherit)', fontSize: 15, fontWeight: 600, lineHeight: 1.4 };
174
+ const pluginDescriptionStyle = { color: 'var(--dsw-alias-label-tertiary, inherit)', fontSize: 13, lineHeight: 1.5 };
175
+ const pluginBodyStyle = { borderTop: '0.5px solid var(--dsw-alias-border-l2, rgba(127,127,127,.2))', margin: '0 16px', paddingBottom: 8 };
176
+ const pluginFieldStyle = { display: 'flex', flexDirection: 'column', gap: 4, padding: '12px 0 0' };
177
+ const pluginLabelStyle = { color: 'var(--dsw-alias-label-primary, inherit)', fontSize: 13, lineHeight: 1.5 };
178
+ const pluginHintStyle = { color: 'var(--dsw-alias-label-tertiary, inherit)', margin: 0, fontSize: 12, lineHeight: 1.5 };
179
+ const pluginInputStyle = {
180
+ border: '0.5px solid var(--dsw-alias-border-l4, rgba(127,127,127,.28))',
181
+ background: 'var(--dsw-alias-bg-layer-3, transparent)',
182
+ height: 34,
183
+ font: 'inherit',
184
+ color: 'var(--dsw-alias-label-primary, inherit)',
185
+ borderRadius: 8,
186
+ padding: '0 12px',
187
+ fontSize: 13,
188
+ };
189
+ const pluginFooterStyle = { borderTop: '0.5px solid var(--dsw-alias-border-l2, rgba(127,127,127,.2))', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, padding: '12px 0 4px' };
190
+ const pluginFailedStyle = { flex: 1, minWidth: 0, margin: 0, color: 'var(--dsw-alias-label-error, #d33)', fontSize: 12, lineHeight: 1.5 };
191
+ const pluginActionStyle = { appearance: 'none', font: 'inherit', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.28))', color: 'var(--dsw-alias-label-secondary, inherit)', background: 'none', borderRadius: 8, padding: '5px 14px', fontSize: 13, lineHeight: 1.5 };
192
+ /**
193
+ * One plugin card inside the 插件配置 tab (`settings.plugin.item`, keyed by
194
+ * the settings namespace `llm-ctl`).
195
+ *
196
+ * The tab renders every card inside its own `<ul>` and dispatches this seat
197
+ * with empty owner props, so the card owns its whole surface and nests as an
198
+ * `<li>`: a header naming the plugin, a body of staged fields, and the
199
+ * save/discard that writes them. Edits are staged locally and only reach the
200
+ * Host on save, fenced by the revision the card was rendered from.
201
+ *
202
+ * @param props - effective budget, cordis base, and the write actions.
203
+ * @returns The rendered card.
204
+ */
205
+ export function PluginConfigCard(props) {
206
+ const { config, actions } = props;
207
+ // Tolerate state documents from an older Host that predate the concurrency
208
+ // fields: without these fallbacks the card throws on mount and vanishes.
209
+ const effectiveDefault = config.defaultConcurrency ?? config.defaults?.defaultConcurrency ?? 2;
210
+ const effectivePer = config.perProviderConcurrency ?? {};
211
+ const [open, setOpen] = React.useState(false);
212
+ const [waitSec, setWaitSec] = React.useState(() => String(Math.round(config.maxWaitMs / 1000)));
213
+ const [depth, setDepth] = React.useState(() => String(config.maxQueueDepth));
214
+ const [defaultConc, setDefaultConc] = React.useState(() => String(effectiveDefault));
215
+ const [perDrafts, setPerDrafts] = React.useState(() => Object.fromEntries(Object.entries(effectivePer).map(([key, value]) => [key, String(value)])));
216
+ const [dirty, setDirty] = React.useState(false);
217
+ const [saving, setSaving] = React.useState(false);
218
+ const [failed, setFailed] = React.useState(false);
219
+ /** The state document predates the concurrency fields, so its Host cannot accept them. */
220
+ const legacyHost = config.defaultConcurrency === undefined || config.perProviderConcurrency === undefined;
221
+ const perTableJson = JSON.stringify(effectivePer);
222
+ // Re-seed drafts when the Host moves underneath and no edit is pending.
223
+ React.useEffect(() => {
224
+ if (dirty || saving)
225
+ return;
226
+ setWaitSec(String(Math.round(config.maxWaitMs / 1000)));
227
+ setDepth(String(config.maxQueueDepth));
228
+ setDefaultConc(String(effectiveDefault));
229
+ setPerDrafts(Object.fromEntries(Object.entries(effectivePer).map(([key, value]) => [key, String(value)])));
230
+ }, [config.revision, config.maxWaitMs, config.maxQueueDepth, effectiveDefault, perTableJson, dirty, saving]);
231
+ const seconds = Number(waitSec);
232
+ const count = Number(depth);
233
+ const defaultCount = Number(defaultConc);
234
+ /** Normalize per-provider drafts: blank inherits the default; `0` means unlimited. */
235
+ const perTable = (() => {
236
+ const table = {};
237
+ let bad = false;
238
+ for (const [key, raw] of Object.entries(perDrafts)) {
239
+ const trimmed = raw.trim();
240
+ if (trimmed === '')
241
+ continue;
242
+ const value = Number(trimmed);
243
+ if (!Number.isFinite(value) || value < 0) {
244
+ bad = true;
245
+ break;
246
+ }
247
+ if (key.length > 0)
248
+ table[key] = Math.floor(value);
249
+ }
250
+ return { table, bad };
251
+ })();
252
+ const invalid = !Number.isFinite(seconds) || seconds < 0 || !Number.isFinite(count) || count < 1 || !Number.isFinite(defaultCount) || defaultCount < 0 || perTable.bad;
253
+ const stage = (setter) => (event) => {
254
+ setter(event.target.value);
255
+ setDirty(true);
256
+ setFailed(false);
257
+ };
258
+ const onSave = async () => {
259
+ if (invalid || saving)
260
+ return;
261
+ setSaving(true);
262
+ setFailed(false);
263
+ const nextWaitMs = Math.round(seconds * 1000);
264
+ const nextDepth = Math.floor(count);
265
+ const nextDefault = Math.floor(defaultCount);
266
+ const patch = { expectedRevision: config.revision };
267
+ if (nextWaitMs !== config.maxWaitMs)
268
+ patch.maxWaitMs = nextWaitMs;
269
+ if (nextDepth !== config.maxQueueDepth)
270
+ patch.maxQueueDepth = nextDepth;
271
+ if (nextDefault !== effectiveDefault)
272
+ patch.defaultConcurrency = nextDefault;
273
+ if (JSON.stringify(perTable.table) !== JSON.stringify(effectivePer))
274
+ patch.perProviderConcurrency = perTable.table;
275
+ const result = await actions.setQueue(patch);
276
+ setSaving(false);
277
+ if (result.ok)
278
+ setDirty(false);
279
+ else
280
+ setFailed(result.error ?? '保存失败,请重试。');
281
+ };
282
+ const onDiscard = () => {
283
+ setWaitSec(String(Math.round(config.maxWaitMs / 1000)));
284
+ setDepth(String(config.maxQueueDepth));
285
+ setDefaultConc(String(effectiveDefault));
286
+ setPerDrafts(Object.fromEntries(Object.entries(effectivePer).map(([key, value]) => [key, String(value)])));
287
+ setDirty(false);
288
+ setFailed(false);
289
+ };
290
+ const onResetDefaults = async () => {
291
+ if (saving)
292
+ return;
293
+ setSaving(true);
294
+ setFailed(false);
295
+ const ok = await actions.resetQueue(config.revision);
296
+ setSaving(false);
297
+ if (ok) {
298
+ setDirty(false);
299
+ setFailed(false);
300
+ }
301
+ else
302
+ setFailed('保存失败,请重试。');
303
+ };
304
+ const field = (input) => React.createElement('div', { style: pluginFieldStyle }, React.createElement('label', { htmlFor: input.id, style: pluginLabelStyle }, input.label), React.createElement('input', {
305
+ id: input.id,
306
+ type: 'number',
307
+ style: pluginInputStyle,
308
+ min: input.min,
309
+ ...(input.step === undefined ? {} : { step: input.step }),
310
+ disabled: saving,
311
+ value: input.value,
312
+ onChange: input.onEdit,
313
+ }), React.createElement('p', { style: pluginHintStyle }, invalid ? input.invalidHint : input.hint));
314
+ const header = React.createElement('button', {
315
+ type: 'button',
316
+ style: pluginHeaderStyle,
317
+ 'aria-expanded': open,
318
+ 'aria-label': (open ? '收起设置: ' : '展开设置: ') + 'LLM 排队控制',
319
+ onClick: () => setOpen(!open),
320
+ }, React.createElement('span', { style: pluginHeadTextStyle }, React.createElement('span', { style: pluginNameStyle }, 'LLM 排队控制'), React.createElement('span', { style: pluginDescriptionStyle }, '按 provider 的并发上限、准入排队、冷却等待与重试预算。')), config.overridden && !dirty ? React.createElement('span', { style: pluginHintStyle }, '已自定义') : null, React.createElement('svg', {
321
+ width: 14,
322
+ height: 14,
323
+ viewBox: '0 0 14 14',
324
+ fill: 'none',
325
+ xmlns: 'http://www.w3.org/2000/svg',
326
+ style: { color: 'var(--dsw-alias-label-tertiary, inherit)', flex: 'none', transition: 'transform .16s', transform: open ? 'rotate(180deg)' : 'none' },
327
+ }, React.createElement('path', { d: 'M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z', fill: 'currentColor' })));
328
+ const stagePer = (provider) => (event) => {
329
+ const value = event.target.value;
330
+ setPerDrafts((prev) => ({ ...prev, [provider]: value }));
331
+ setDirty(true);
332
+ setFailed(false);
333
+ };
334
+ const directory = (props.providers ?? []).filter((entry) => entry.provider.length > 0);
335
+ const rowIds = directory.map((entry) => entry.provider);
336
+ for (const key of [...Object.keys(effectivePer), ...Object.keys(perDrafts)]) {
337
+ if (key.length > 0 && !rowIds.includes(key))
338
+ rowIds.push(key);
339
+ }
340
+ const displayNameOf = (id) => directory.find((entry) => entry.provider === id)?.displayName ?? id;
341
+ // True effective cap per row (free-route heuristic included), so the
342
+ // placeholder never lies about what a blank cell inherits.
343
+ const lookupTable = { default: effectiveDefault, ...effectivePer };
344
+ const inheritHint = (id) => {
345
+ const effective = concurrencyFor(lookupTable, id);
346
+ return Number.isFinite(effective) ? `默认 ${effective}` : '不限制(默认)';
347
+ };
348
+ const concurrencySection = React.createElement('div', { style: pluginFieldStyle }, React.createElement('span', { style: pluginLabelStyle }, '各提供方并发上限'), React.createElement('p', { style: pluginHintStyle }, '留空继承默认;填 0 为该提供方不限制。'), rowIds.length === 0
349
+ ? React.createElement('p', { style: pluginHintStyle }, '暂无已知提供方,先设置默认并发即可。')
350
+ : rowIds.map((id) => React.createElement('div', { key: id, style: { ...rowStyle, paddingLeft: 12 } }, React.createElement('span', { style: nameStyle }, displayNameOf(id)), React.createElement('input', {
351
+ type: 'number',
352
+ style: { ...pluginInputStyle, width: 88, height: 28, flex: 'none' },
353
+ min: 0,
354
+ step: 1,
355
+ disabled: saving,
356
+ value: perDrafts[id] ?? '',
357
+ placeholder: inheritHint(id),
358
+ 'aria-label': `${displayNameOf(id)} 并发上限`,
359
+ onChange: stagePer(id),
360
+ }))));
361
+ const body = React.createElement('div', { style: pluginBodyStyle }, legacyHost
362
+ ? React.createElement('p', { style: pluginFailedStyle }, '检测到 Host 为旧版本:并发设置保存会失败,请重载插件后再试;排队超时与队列深度仍可保存。')
363
+ : null, field({ id: 'llm-ctl-queue-max-wait', label: '排队超时(秒)', hint: '单请求排队耐心,与愿意采纳的 provider 冷却共用一个预算。', invalidHint: '请输入 ≥ 0 的秒数。', value: waitSec, min: 0, onEdit: stage(setWaitSec) }), field({ id: 'llm-ctl-queue-depth', label: '队列深度', hint: '同一时刻允许排队的最大请求数。', invalidHint: '请输入 ≥ 1 的整数。', value: depth, min: 1, step: 1, onEdit: stage(setDepth) }), field({ id: 'llm-ctl-queue-concurrency', label: '默认并发上限', hint: '每个提供方同时进行的请求数,超限的排队等待;0 表示不限制(默认)。', invalidHint: '请输入 ≥ 0 的整数(0 = 不限制)。', value: defaultConc, min: 0, step: 1, onEdit: stage(setDefaultConc) }), concurrencySection, React.createElement('div', { style: pluginFooterStyle }, failed !== false ? React.createElement('p', { style: pluginFailedStyle }, failed) : null, config.overridden
364
+ ? React.createElement('button', { type: 'button', style: pluginActionStyle, disabled: saving, onClick: () => void onResetDefaults() }, '恢复默认')
365
+ : null, React.createElement('button', { type: 'button', style: pluginActionStyle, disabled: !dirty || invalid || saving, onClick: onDiscard }, '放弃修改'), React.createElement('button', { type: 'button', style: { ...pluginActionStyle, background: 'var(--dsw-alias-label-primary, currentColor)', color: 'var(--dsw-alias-bg-layer-3, inherit)' }, disabled: !dirty || invalid || saving, onClick: () => void onSave() }, saving ? '保存中…' : '保存')));
366
+ return React.createElement('li', { style: pluginCardStyle }, header, open ? body : null);
367
+ }