@wxip/dsh-sub2api 0.2.2

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 ADDED
@@ -0,0 +1,1107 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@wxip/dsh-sub2api",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ "use strict";
8
+ //#region rolldown:runtime
9
+ var __create = Object.create;
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __getProtoOf = Object.getPrototypeOf;
14
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
17
+ key = keys[i];
18
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
19
+ get: ((k) => from[k]).bind(null, key),
20
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
+ });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
26
+ value: mod,
27
+ enumerable: true
28
+ }) : target, mod));
29
+
30
+ //#endregion
31
+ const react = __toESM(require("react"));
32
+ const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
33
+
34
+ //#region src/client/icons.tsx
35
+ const paths = {
36
+ openai: "M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z",
37
+ claude: "M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z",
38
+ grok: "M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"
39
+ };
40
+ function ProviderIcon({ name: name$1, size = 18, style }) {
41
+ return (0, react_jsx_runtime.jsx)("svg", {
42
+ width: size,
43
+ height: size,
44
+ viewBox: "0 0 24 24",
45
+ fill: "currentColor",
46
+ style: {
47
+ flex: "none",
48
+ lineHeight: 1,
49
+ ...style
50
+ },
51
+ "aria-hidden": "true",
52
+ children: (0, react_jsx_runtime.jsx)("path", {
53
+ d: paths[name$1],
54
+ fillRule: "evenodd"
55
+ })
56
+ });
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/client/settings.tsx
61
+ const BASE = "/plugins/dsh-sub2api";
62
+ const MODELS_DEV_API = "https://models.dev/api.json";
63
+ /** Model-row controls matching the official Models page. */
64
+ function IconTrash() {
65
+ return (0, react_jsx_runtime.jsx)("svg", {
66
+ width: "18",
67
+ height: "18",
68
+ viewBox: "0 0 20 20",
69
+ fill: "none",
70
+ "aria-hidden": true,
71
+ children: (0, react_jsx_runtime.jsx)("path", {
72
+ d: "M3.5 5.25h13M8 5.25V3.5h4v1.75M5.25 5.25l.8 10.1c.05.65.6 1.15 1.25 1.15h5.4c.65 0 1.2-.5 1.25-1.15l.8-10.1M8.25 8v5.75M11.75 8v5.75",
73
+ stroke: "currentColor",
74
+ strokeWidth: "1.5",
75
+ strokeLinecap: "round",
76
+ strokeLinejoin: "round"
77
+ })
78
+ });
79
+ }
80
+ function IconChevron({ expanded }) {
81
+ return (0, react_jsx_runtime.jsx)("svg", {
82
+ width: "20",
83
+ height: "20",
84
+ viewBox: "0 0 20 20",
85
+ fill: "none",
86
+ "aria-hidden": true,
87
+ style: {
88
+ transform: expanded ? "rotate(90deg)" : undefined,
89
+ transition: "transform 120ms ease"
90
+ },
91
+ children: (0, react_jsx_runtime.jsx)("path", {
92
+ d: "m7.5 4.75 5.25 5.25-5.25 5.25",
93
+ stroke: "currentColor",
94
+ strokeWidth: "1.6",
95
+ strokeLinecap: "round",
96
+ strokeLinejoin: "round"
97
+ })
98
+ });
99
+ }
100
+ const PROVIDERS = [
101
+ {
102
+ key: "openai",
103
+ label: "OpenAI",
104
+ icon: "openai",
105
+ placeholder: "sk-…",
106
+ modelsDevProvider: "openai"
107
+ },
108
+ {
109
+ key: "claude",
110
+ label: "Claude",
111
+ icon: "claude",
112
+ placeholder: "sk-ant-…",
113
+ modelsDevProvider: "anthropic"
114
+ },
115
+ {
116
+ key: "grok",
117
+ label: "Grok",
118
+ icon: "grok",
119
+ placeholder: "xai-…",
120
+ modelsDevProvider: "xai"
121
+ }
122
+ ];
123
+ const CSS_ID = "dsh-sub2api/settings.css";
124
+ const css = `
125
+ .s2a_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}
126
+ .s2a_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}
127
+ .s2a_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:20px}
128
+ .s2a_notice{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}
129
+ .s2a_field{flex-direction:column;gap:5px;display:flex}
130
+ .s2a_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}
131
+ .s2a_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;line-height:22px}
132
+ .s2a_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}
133
+ .s2a_input::placeholder{color:var(--dsw-alias-label-dimmed)}
134
+ .s2a_rows{flex-direction:column;gap:10px;margin:4px 0 0;padding:0;list-style:none;display:flex}
135
+ .s2a_rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:12px;padding:14px 16px;display:flex}
136
+ .s2a_rowHead{align-items:center;gap:10px;display:flex}
137
+ .s2a_rowIdentity{flex:1;align-items:center;gap:8px;min-width:0;display:inline-flex}
138
+ .s2a_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}
139
+ .s2a_rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
140
+ .s2a_editor{background:var(--dsw-alias-bg-module-platform);border-radius:8px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}
141
+ .s2a_rowActions,.s2a_modelActions{align-items:center;gap:4px;margin-left:auto;display:inline-flex}
142
+ .s2a_btn,.s2a_primary{box-sizing:border-box;height:32px;font:inherit;cursor:pointer;border:none;justify-content:center;align-items:center;gap:4px;font-size:13px;line-height:20px;display:inline-flex}
143
+ .s2a_btn,.s2a_primary{border-radius:16px;padding:0 14px}
144
+ .s2a_primary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}
145
+ .s2a_primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
146
+ .s2a_btn{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:transparent}
147
+ .s2a_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
148
+ .s2a_btn:disabled,.s2a_primary:disabled{opacity:.4;cursor:default}
149
+ .s2a_btn:focus-visible,.s2a_primary:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}
150
+ /* Square, label-free icon affordance matching the official Models page: the
151
+ row's inputs carry the meaning, the trash glyph announces deletion. */
152
+ .s2a_iconBtn{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;padding:0}
153
+ .s2a_iconBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
154
+ .s2a_iconBtn:disabled{opacity:.4;cursor:default}
155
+ .s2a_iconBtn:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}
156
+ .s2a_trash:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}
157
+ .s2a_models{flex-direction:column;gap:10px;display:flex}
158
+ .s2a_modelItem{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}
159
+ .s2a_modelSummary{grid-template-columns:minmax(0,1fr) minmax(0,.72fr) 44px 32px;align-items:center;gap:10px;padding:9px 10px 9px 12px;display:grid}
160
+ .s2a_modelSummary .s2a_input{height:38px;border-radius:7px;padding:0 12px;font-size:14px}
161
+ .s2a_expandBtn{width:44px;height:38px;border-radius:7px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
162
+ .s2a_modelDetails{border-top:1px solid var(--dsw-alias-border-l2);grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px 12px;padding:10px 12px 12px;display:grid}
163
+ .s2a_modelDetails .s2a_fieldLabel{font-size:12px;font-weight:400}
164
+ .s2a_reasoningField{grid-column:1/-1}
165
+ .s2a_modelEmpty{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-tertiary);margin:0;padding:16px 10px;text-align:center;font-size:12px;line-height:18px}
166
+ .s2a_modelFooter{align-items:center;justify-content:space-between;gap:8px;display:flex}
167
+ .s2a_modelSource{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
168
+ .s2a_modelSource a{color:inherit;text-decoration:underline;text-underline-offset:2px}
169
+ .s2a_actions{position:sticky;bottom:0;z-index:5;align-items:center;justify-content:flex-end;gap:8px;margin-top:4px;padding:12px 0;background:var(--dsw-alias-bg-layer-1,#fff);border-top:1px solid var(--dsw-alias-border-l2,#ddd);display:flex}
170
+ .s2a_actions::after{content:"";position:absolute;top:100%;left:0;right:0;height:var(--s2a-footer-inset,24px);background:inherit}
171
+ .s2a_toast{position:fixed;top:24px;right:24px;z-index:10000;display:flex;align-items:flex-start;gap:12px;box-sizing:border-box;max-width:min(480px,calc(100vw - 32px));max-height:40vh;overflow:auto;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 6px 24px #0002}.s2a_toast .s2a_status{overflow-wrap:anywhere;flex:1}.s2a_toast button{flex-shrink:0}
172
+ .s2a_status{margin:0;font-size:12px;line-height:18px;white-space:pre-wrap;color:var(--dsw-alias-label-secondary)}
173
+ .s2a_statusOk{color:var(--dsw-alias-state-success-primary)}
174
+ .s2a_statusErr{color:var(--dsw-alias-state-error-primary)}
175
+ @media(max-width:620px){
176
+ .s2a_section,.s2a_rowIdentity{min-width:0}.s2a_rowCard{padding:10px 8px}.s2a_editor{padding:8px 0}
177
+ .s2a_rowHead{align-items:flex-start;flex-wrap:wrap}.s2a_rowIdentity{flex-wrap:wrap}.s2a_rowTag{box-sizing:border-box;width:100%;max-width:100%;min-width:0;flex:1 1 100%;white-space:normal;overflow-wrap:anywhere}.s2a_rowActions{width:100%;margin-left:0;flex-direction:column;align-items:stretch}.s2a_rowActions .s2a_btn{width:100%}
178
+ .s2a_modelSummary{grid-template-columns:minmax(0,1fr) 44px 32px;gap:8px;padding:8px}.s2a_modelSummary>div:nth-child(2){grid-column:1/2;grid-row:2}.s2a_modelSummary>.s2a_expandBtn{grid-column:2;grid-row:1/3}.s2a_modelSummary>.s2a_trash{grid-column:3;grid-row:1/3}
179
+ .s2a_modelDetails{grid-template-columns:minmax(0,1fr)}.s2a_reasoningField{grid-column:auto}
180
+ .s2a_modelFooter{align-items:stretch;flex-direction:column}.s2a_modelActions{width:100%;margin-left:0;align-items:stretch;flex-direction:column}.s2a_modelActions .s2a_btn{width:100%;padding:0 8px}
181
+ }
182
+ `;
183
+ function ensureCss() {
184
+ if (typeof document === "undefined") return;
185
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(CSS_ID)}]`)) return;
186
+ const tag = document.createElement("style");
187
+ tag.dataset.plugin = "dsh-sub2api";
188
+ tag.dataset.pluginCss = CSS_ID;
189
+ tag.textContent = css;
190
+ document.head.appendChild(tag);
191
+ }
192
+ const DEFAULT_REASONING_LEVELS = [
193
+ "low",
194
+ "medium",
195
+ "high"
196
+ ];
197
+ let nextRowId = 1;
198
+ let modelsDevRequest;
199
+ function modelRow(model = { id: "" }) {
200
+ if (typeof model === "string") {
201
+ const [id = "", name$1 = "", contextWindow = ""] = model.split("|");
202
+ return {
203
+ rowId: nextRowId++,
204
+ id: id.trim(),
205
+ name: name$1.trim(),
206
+ contextWindow: contextWindow.trim(),
207
+ maxTokens: "",
208
+ input: "",
209
+ reasoning: "",
210
+ effortLevels: ""
211
+ };
212
+ }
213
+ const reasoningEfforts = model.reasoningEfforts;
214
+ return {
215
+ rowId: nextRowId++,
216
+ id: model.id,
217
+ name: model.name ?? "",
218
+ contextWindow: model.contextWindow !== undefined ? String(model.contextWindow) : "",
219
+ maxTokens: model.maxTokens !== undefined ? String(model.maxTokens) : "",
220
+ input: model.input === undefined || model.input.length === 0 ? "" : model.input.includes("image") ? "text-image" : "text",
221
+ reasoning: reasoningEfforts === undefined ? "" : reasoningEfforts.length === 0 ? "off" : "on",
222
+ effortLevels: reasoningEfforts !== undefined && reasoningEfforts.length > 0 ? reasoningEfforts.join(", ") : ""
223
+ };
224
+ }
225
+ function loadModelsDev() {
226
+ modelsDevRequest ??= fetch(MODELS_DEV_API).then(async (response) => {
227
+ if (!response.ok) throw new Error(`models.dev HTTP ${response.status}`);
228
+ return await response.json();
229
+ }).catch((error) => {
230
+ modelsDevRequest = undefined;
231
+ throw error;
232
+ });
233
+ return modelsDevRequest;
234
+ }
235
+ function canonicalModelsDevProvider(id) {
236
+ const normalized = id.toLowerCase();
237
+ if (/^(gpt|o[134]|codex)/.test(normalized)) return "openai";
238
+ if (normalized.startsWith("claude")) return "anthropic";
239
+ if (normalized.startsWith("grok")) return "xai";
240
+ if (normalized.startsWith("gemini")) return "google";
241
+ if (normalized.startsWith("deepseek")) return "deepseek";
242
+ return undefined;
243
+ }
244
+ function officialModel(catalog, def, id) {
245
+ const preferredProviders = [canonicalModelsDevProvider(id), def.modelsDevProvider].filter((provider, index, all) => provider !== undefined && all.indexOf(provider) === index);
246
+ for (const provider of preferredProviders) {
247
+ const models = catalog[provider]?.models;
248
+ const match = models?.[id] ?? (models !== undefined ? Object.values(models).find((model) => model.id === id) : undefined);
249
+ if (match !== undefined) return match;
250
+ }
251
+ for (const provider of Object.values(catalog)) {
252
+ const models = provider.models;
253
+ const match = models?.[id] ?? (models !== undefined ? Object.values(models).find((model) => model.id === id) : undefined);
254
+ if (match !== undefined) return match;
255
+ }
256
+ return undefined;
257
+ }
258
+ /** The concrete reasoning-effort levels a model advertises, or undefined. */
259
+ function officialEffortValues(official) {
260
+ const effort = official.reasoning_options?.find((option) => option.type === "effort");
261
+ const values = effort?.values;
262
+ if (values === undefined || values.length === 0) return undefined;
263
+ return values.filter((value) => typeof value === "string" && value.length > 0);
264
+ }
265
+ /**
266
+
267
+ * Image-input modality derived from models.dev: an explicit modality list
268
+
269
+ * wins, then the `attachment` flag. Absent means the entry says nothing, and
270
+
271
+ * the row keeps "auto" (the adapter infers from the model id).
272
+
273
+ */
274
+ function officialInput(official) {
275
+ const input = official.modalities?.input;
276
+ if (Array.isArray(input) && input.includes("image")) return "text-image";
277
+ if (typeof official.attachment === "boolean") return official.attachment ? "text-image" : "text";
278
+ return undefined;
279
+ }
280
+ function applyOfficialDefaults(rows, def, catalog) {
281
+ let filled = 0;
282
+ const next = rows.map((row) => {
283
+ const official = officialModel(catalog, def, row.id.trim());
284
+ if (official === undefined) return row;
285
+ const name$1 = row.name.trim().length === 0 && typeof official.name === "string" ? official.name : row.name;
286
+ const officialContext = official.limit?.context;
287
+ const contextWindow = row.contextWindow.length === 0 && Number.isSafeInteger(officialContext) && (officialContext ?? 0) > 0 ? String(officialContext) : row.contextWindow;
288
+ const officialOutput = official.limit?.output;
289
+ const maxTokens = row.maxTokens.length === 0 && Number.isSafeInteger(officialOutput) && (officialOutput ?? 0) > 0 ? String(officialOutput) : row.maxTokens;
290
+ const input = !row.inputEdited && row.input.length === 0 && officialInput(official) !== undefined ? officialInput(official) : row.input;
291
+ const officialEfforts = officialEffortValues(official);
292
+ let reasoning = row.reasoning;
293
+ let effortLevels = row.effortLevels;
294
+ if (!row.reasoningEdited && reasoning.length === 0) {
295
+ if (officialEfforts !== undefined && officialEfforts.length > 0) {
296
+ reasoning = "on";
297
+ effortLevels = officialEfforts.join(", ");
298
+ } else if (typeof official.reasoning === "boolean") {
299
+ reasoning = official.reasoning ? "on" : "off";
300
+ effortLevels = official.reasoning ? DEFAULT_REASONING_LEVELS.join(", ") : "";
301
+ }
302
+ }
303
+ if (name$1 !== row.name || contextWindow !== row.contextWindow || maxTokens !== row.maxTokens || input !== row.input || reasoning !== row.reasoning || effortLevels !== row.effortLevels) filled++;
304
+ return {
305
+ ...row,
306
+ name: name$1,
307
+ contextWindow,
308
+ maxTokens,
309
+ input,
310
+ reasoning,
311
+ effortLevels
312
+ };
313
+ });
314
+ return {
315
+ rows: next,
316
+ filled
317
+ };
318
+ }
319
+ async function api(path, init) {
320
+ const response = await fetch(path, {
321
+ ...init,
322
+ headers: {
323
+ "content-type": "application/json",
324
+ ...init?.headers
325
+ }
326
+ });
327
+ const payload = await response.json().catch(() => ({}));
328
+ if (!response.ok) {
329
+ const message = typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
330
+ throw new Error(message);
331
+ }
332
+ return payload;
333
+ }
334
+ function emptyProvider() {
335
+ return {
336
+ key: "",
337
+ keyConfigured: false,
338
+ models: []
339
+ };
340
+ }
341
+ function emptyToolRef() {
342
+ return {
343
+ provider: "",
344
+ model: ""
345
+ };
346
+ }
347
+ function emptyTools() {
348
+ return { generate: emptyToolRef() };
349
+ }
350
+ function toolRefFromConfig(value) {
351
+ return {
352
+ provider: PROVIDERS.some((def) => def.key === value?.provider) ? value.provider : "",
353
+ model: PROVIDERS.some((def) => def.key === value?.provider) ? value.model : ""
354
+ };
355
+ }
356
+ function toolOptions(providers) {
357
+ const options = [];
358
+ for (const def of PROVIDERS) {
359
+ const provider = providers[def.key];
360
+ if (provider === undefined) continue;
361
+ for (const row of provider.models) {
362
+ const id = row.id.trim();
363
+ if (id.length === 0) continue;
364
+ const name$1 = row.name.trim();
365
+ options.push({
366
+ value: `${def.key}:${id}`,
367
+ label: `${def.label} / ${name$1.length > 0 ? `${name$1} (${id})` : id}`,
368
+ provider: def.key,
369
+ model: id
370
+ });
371
+ }
372
+ }
373
+ return options;
374
+ }
375
+ function serializeToolRef(ref) {
376
+ const provider = ref.provider.trim();
377
+ const model = ref.model.trim();
378
+ if (provider.length === 0 || model.length === 0) return undefined;
379
+ return {
380
+ provider,
381
+ model
382
+ };
383
+ }
384
+ function Sub2ApiSettings() {
385
+ const sectionRef = (0, react.useRef)(null);
386
+ (0, react.useEffect)(() => {
387
+ const section = sectionRef.current;
388
+ if (!section) return;
389
+ let scroller = section.parentElement;
390
+ while (scroller && !/auto|scroll/.test(getComputedStyle(scroller).overflowY)) scroller = scroller.parentElement;
391
+ if (!scroller) return;
392
+ const update = () => section.style.setProperty("--s2a-footer-inset", getComputedStyle(scroller).paddingBottom);
393
+ update();
394
+ const observer = new ResizeObserver(update);
395
+ observer.observe(scroller);
396
+ return () => observer.disconnect();
397
+ }, []);
398
+ const [baseURL, setBaseURL] = (0, react.useState)("");
399
+ const [providers, setProviders] = (0, react.useState)({});
400
+ const [tools, setTools] = (0, react.useState)(emptyTools());
401
+ const [structuredConfig, setStructuredConfig] = (0, react.useState)(false);
402
+ const [expandedModels, setExpandedModels] = (0, react.useState)(() => new Set());
403
+ const [busy, setBusy] = (0, react.useState)("");
404
+ const [message, setMessage] = (0, react.useState)("");
405
+ const [error, setError] = (0, react.useState)("");
406
+ (0, react.useEffect)(() => {
407
+ if (!message && !error) return;
408
+ const timer = window.setTimeout(() => {
409
+ setMessage("");
410
+ setError("");
411
+ }, error ? 1e4 : 6e3);
412
+ return () => window.clearTimeout(timer);
413
+ }, [message, error]);
414
+ (0, react.useEffect)(() => {
415
+ ensureCss();
416
+ api(`${BASE}/config`).then(async (cfg) => {
417
+ setBaseURL(cfg.baseURL ?? "");
418
+ setStructuredConfig(cfg.catalogFormat === "structured-v1");
419
+ const map = {};
420
+ for (const def of PROVIDERS) {
421
+ const provider = cfg.providers?.[def.key];
422
+ map[def.key] = {
423
+ key: "",
424
+ keyConfigured: provider?.keyConfigured ?? false,
425
+ models: (provider?.models ?? []).map(modelRow)
426
+ };
427
+ }
428
+ setProviders(map);
429
+ setTools({ generate: toolRefFromConfig(cfg.tools?.generate) });
430
+ try {
431
+ const catalog = await loadModelsDev();
432
+ setProviders((current) => {
433
+ const enriched = { ...current };
434
+ for (const def of PROVIDERS) {
435
+ const provider = current[def.key];
436
+ if (provider !== undefined) enriched[def.key] = {
437
+ ...provider,
438
+ models: applyOfficialDefaults(provider.models, def, catalog).rows
439
+ };
440
+ }
441
+ return enriched;
442
+ });
443
+ } catch {}
444
+ }).catch((e) => setError(String(e instanceof Error ? e.message : e)));
445
+ }, []);
446
+ const updateProvider = (0, react.useCallback)((key, patch) => {
447
+ setProviders((previous) => ({
448
+ ...previous,
449
+ [key]: {
450
+ ...previous[key] ?? emptyProvider(),
451
+ ...patch
452
+ }
453
+ }));
454
+ }, []);
455
+ const updateModel = (providerKey, rowId, patch) => {
456
+ setProviders((previous) => {
457
+ const provider = previous[providerKey] ?? emptyProvider();
458
+ return {
459
+ ...previous,
460
+ [providerKey]: {
461
+ ...provider,
462
+ models: provider.models.map((row) => row.rowId === rowId ? {
463
+ ...row,
464
+ ...patch
465
+ } : row)
466
+ }
467
+ };
468
+ });
469
+ };
470
+ const toggleModel = (rowId) => {
471
+ setExpandedModels((current) => {
472
+ const next = new Set(current);
473
+ if (next.has(rowId)) next.delete(rowId);
474
+ else next.add(rowId);
475
+ return next;
476
+ });
477
+ };
478
+ const fillModel = async (def, rowId) => {
479
+ try {
480
+ const catalog = await loadModelsDev();
481
+ setProviders((previous) => {
482
+ const provider = previous[def.key];
483
+ if (provider === undefined) return previous;
484
+ const models = provider.models.map((row) => row.rowId === rowId ? applyOfficialDefaults([row], def, catalog).rows[0] ?? row : row);
485
+ return {
486
+ ...previous,
487
+ [def.key]: {
488
+ ...provider,
489
+ models
490
+ }
491
+ };
492
+ });
493
+ } catch {}
494
+ };
495
+ const fillProvider = async (def) => {
496
+ setBusy(`metadata-${def.key}`);
497
+ setError("");
498
+ setMessage("");
499
+ try {
500
+ const catalog = await loadModelsDev();
501
+ setProviders((previous) => {
502
+ const provider = previous[def.key] ?? emptyProvider();
503
+ return {
504
+ ...previous,
505
+ [def.key]: {
506
+ ...provider,
507
+ models: applyOfficialDefaults(provider.models, def, catalog).rows
508
+ }
509
+ };
510
+ });
511
+ setMessage(`${def.label} 已补全空白字段,保留手动设置`);
512
+ } catch (e) {
513
+ setError(`无法读取 models.dev:${String(e instanceof Error ? e.message : e)}`);
514
+ } finally {
515
+ setBusy("");
516
+ }
517
+ };
518
+ const save = async () => {
519
+ setBusy("save");
520
+ setError("");
521
+ setMessage("");
522
+ try {
523
+ if (!structuredConfig) throw new Error("服务端仍在运行旧版插件,请重启 DSH Web 后再保存结构化模型配置");
524
+ const payload = {
525
+ baseURL,
526
+ providers: {},
527
+ tools: {}
528
+ };
529
+ for (const def of PROVIDERS) {
530
+ const provider = providers[def.key] ?? emptyProvider();
531
+ const nonEmptyRows = provider.models.filter((row) => row.id.trim().length > 0 || row.name.trim().length > 0 || row.contextWindow.length > 0 || row.maxTokens.length > 0 || row.reasoning.length > 0);
532
+ const seen = new Set();
533
+ const models = nonEmptyRows.map((row) => {
534
+ const id = row.id.trim();
535
+ if (id.length === 0) throw new Error(`${def.label} 存在未填写 ID 的模型`);
536
+ if (seen.has(id)) throw new Error(`${def.label} 模型 ID 重复:${id}`);
537
+ seen.add(id);
538
+ const name$1 = row.name.trim();
539
+ const contextWindow = row.contextWindow.length > 0 ? Number(row.contextWindow) : undefined;
540
+ if (contextWindow !== undefined && (!Number.isSafeInteger(contextWindow) || contextWindow < 1)) throw new Error(`${def.label} ${id} 的 Context Window 必须是正整数`);
541
+ const maxTokens = row.maxTokens.trim().length > 0 ? Number(row.maxTokens) : undefined;
542
+ if (maxTokens !== undefined && (!Number.isSafeInteger(maxTokens) || maxTokens < 1)) throw new Error(`${def.label} ${id} 的 Max Tokens 必须是正整数`);
543
+ const reasoningEfforts = row.reasoning === "off" ? [] : row.reasoning === "on" ? [...new Set(row.effortLevels.split(/[,,/\s]+/).filter(Boolean))] : undefined;
544
+ if (row.reasoning === "on" && !reasoningEfforts?.length) throw new Error(`${def.label} ${id} 请填写至少一个思考强度`);
545
+ if (reasoningEfforts?.some((level) => ![
546
+ "none",
547
+ "off",
548
+ "minimal",
549
+ "low",
550
+ "medium",
551
+ "high",
552
+ "xhigh",
553
+ "max"
554
+ ].includes(level))) throw new Error(`${def.label} ${id} 思考强度支持 none、off、minimal、low、medium、high、xhigh、max`);
555
+ const input = row.input === "text-image" ? ["text", "image"] : row.input === "text" ? ["text"] : undefined;
556
+ return {
557
+ id,
558
+ ...name$1.length > 0 ? { name: name$1 } : {},
559
+ ...contextWindow !== undefined ? { contextWindow } : {},
560
+ ...maxTokens !== undefined ? { maxTokens } : {},
561
+ ...input !== undefined ? { input } : {},
562
+ ...reasoningEfforts !== undefined ? { reasoningEfforts } : {}
563
+ };
564
+ });
565
+ payload.providers[def.key] = {
566
+ apiKey: provider.key,
567
+ models
568
+ };
569
+ }
570
+ const generate = serializeToolRef(tools.generate);
571
+ if (generate !== undefined) payload.tools.generate = generate;
572
+ const res = await api(`${BASE}/config`, {
573
+ method: "POST",
574
+ body: JSON.stringify(payload)
575
+ });
576
+ const routes = res.routes !== undefined && res.routes.length > 0 ? res.routes.join(", ") : "无(未填 key)";
577
+ setMessage(`已保存。激活路由: ${routes}`);
578
+ } catch (e) {
579
+ setError(String(e instanceof Error ? e.message : e));
580
+ } finally {
581
+ setBusy("");
582
+ }
583
+ };
584
+ const discover = async (def) => {
585
+ const key = providers[def.key]?.key ?? "";
586
+ setBusy(`discover-${def.key}`);
587
+ setError("");
588
+ setMessage("");
589
+ updateProvider(def.key, { models: [] });
590
+ try {
591
+ const res = await api(`${BASE}/discover`, {
592
+ method: "POST",
593
+ body: JSON.stringify({
594
+ baseURL,
595
+ apiKey: key,
596
+ provider: def.key
597
+ })
598
+ });
599
+ let rows = (res.models ?? []).map(modelRow);
600
+ try {
601
+ rows = applyOfficialDefaults(rows, def, await loadModelsDev()).rows;
602
+ } catch {}
603
+ updateProvider(def.key, { models: rows });
604
+ setMessage(`${def.label} 发现 ${rows.length} 个模型`);
605
+ } catch (e) {
606
+ setError(String(e instanceof Error ? e.message : e));
607
+ } finally {
608
+ setBusy("");
609
+ }
610
+ };
611
+ const checkUsage = async (def) => {
612
+ const key = providers[def.key]?.key ?? "";
613
+ setBusy(`usage-${def.key}`);
614
+ setError("");
615
+ setMessage("");
616
+ try {
617
+ const res = await api(`${BASE}/usage`, {
618
+ method: "POST",
619
+ body: JSON.stringify({
620
+ baseURL,
621
+ apiKey: key,
622
+ provider: def.key
623
+ })
624
+ });
625
+ setMessage(`${def.label} 用量: ${res.summary ?? ""}`);
626
+ } catch (e) {
627
+ setError(String(e instanceof Error ? e.message : e));
628
+ } finally {
629
+ setBusy("");
630
+ }
631
+ };
632
+ const checkStatus = async () => {
633
+ setBusy("status");
634
+ setError("");
635
+ setMessage("");
636
+ try {
637
+ const res = await api(`${BASE}/status`);
638
+ setMessage(`已注册路由: ${res.routes.length > 0 ? res.routes.join(", ") : "无"};模型: ${JSON.stringify(res.models)}`);
639
+ } catch (e) {
640
+ setError(String(e instanceof Error ? e.message : e));
641
+ } finally {
642
+ setBusy("");
643
+ }
644
+ };
645
+ return (0, react_jsx_runtime.jsxs)("div", {
646
+ className: "s2a_section",
647
+ ref: sectionRef,
648
+ children: [
649
+ (0, react_jsx_runtime.jsx)("h2", {
650
+ className: "s2a_title",
651
+ children: "Sub2API 模型接入"
652
+ }),
653
+ (0, react_jsx_runtime.jsx)("p", {
654
+ className: "s2a_intro",
655
+ children: "统一端点 + 多 key:所有供应商共享一个 baseURL,每个 key 在 sub2api 后台绑定一个分组,分组决定平台(OpenAI / Claude / Grok)与可用模型。"
656
+ }),
657
+ (0, react_jsx_runtime.jsx)("p", {
658
+ className: "s2a_notice",
659
+ children: "提示:先在 sub2api 后台创建各平台的分组并生成 API key,再填入下方。"
660
+ }),
661
+ (0, react_jsx_runtime.jsxs)("div", {
662
+ className: "s2a_field",
663
+ style: { marginTop: 2 },
664
+ children: [(0, react_jsx_runtime.jsx)("label", {
665
+ className: "s2a_fieldLabel",
666
+ children: "Sub2API Base URL"
667
+ }), (0, react_jsx_runtime.jsx)("input", {
668
+ className: "s2a_input",
669
+ value: baseURL,
670
+ placeholder: "http://localhost:8080",
671
+ onChange: (event) => setBaseURL(event.target.value)
672
+ })]
673
+ }),
674
+ (0, react_jsx_runtime.jsxs)("div", {
675
+ className: "s2a_rowCard",
676
+ children: [(0, react_jsx_runtime.jsx)("div", {
677
+ className: "s2a_rowHead",
678
+ children: (0, react_jsx_runtime.jsxs)("div", {
679
+ className: "s2a_rowIdentity",
680
+ children: [(0, react_jsx_runtime.jsx)("span", {
681
+ className: "s2a_rowName",
682
+ children: "图片生成工具"
683
+ }), (0, react_jsx_runtime.jsx)("span", {
684
+ className: "s2a_rowTag",
685
+ children: "generate_image"
686
+ })]
687
+ })
688
+ }), (0, react_jsx_runtime.jsxs)("div", {
689
+ className: "s2a_editor",
690
+ children: [(0, react_jsx_runtime.jsx)("p", {
691
+ className: "s2a_intro",
692
+ children: "指定生成图片使用的模型,生成结果会保存到工作区。"
693
+ }), ["generate"].map((kind) => {
694
+ const label = "生图模型";
695
+ const ref = tools[kind];
696
+ const options = toolOptions(providers);
697
+ const selected = ref.provider.length > 0 && ref.model.length > 0 ? `${ref.provider}:${ref.model}` : "";
698
+ const known = options.some((option) => option.value === selected);
699
+ return (0, react_jsx_runtime.jsxs)("div", {
700
+ className: "s2a_field",
701
+ children: [(0, react_jsx_runtime.jsx)("label", {
702
+ className: "s2a_fieldLabel",
703
+ children: label
704
+ }), (0, react_jsx_runtime.jsxs)("select", {
705
+ className: "s2a_input",
706
+ value: selected,
707
+ "aria-label": label,
708
+ onChange: (event) => {
709
+ const next = options.find((option) => option.value === event.target.value);
710
+ setTools((current) => ({
711
+ ...current,
712
+ [kind]: next === undefined ? emptyToolRef() : {
713
+ provider: next.provider,
714
+ model: next.model
715
+ }
716
+ }));
717
+ },
718
+ children: [
719
+ (0, react_jsx_runtime.jsx)("option", {
720
+ value: "",
721
+ children: "未指定"
722
+ }),
723
+ !known && selected.length > 0 && (0, react_jsx_runtime.jsx)("option", {
724
+ value: selected,
725
+ children: `${ref.provider} / ${ref.model}(不在当前目录)`
726
+ }),
727
+ options.map((option) => (0, react_jsx_runtime.jsx)("option", {
728
+ value: option.value,
729
+ children: option.label
730
+ }, option.value))
731
+ ]
732
+ })]
733
+ }, kind);
734
+ })]
735
+ })]
736
+ }),
737
+ (0, react_jsx_runtime.jsx)("ul", {
738
+ className: "s2a_rows",
739
+ children: PROVIDERS.map((def) => {
740
+ const provider = providers[def.key] ?? emptyProvider();
741
+ return (0, react_jsx_runtime.jsxs)("li", {
742
+ className: "s2a_rowCard",
743
+ children: [(0, react_jsx_runtime.jsxs)("div", {
744
+ className: "s2a_rowHead",
745
+ children: [(0, react_jsx_runtime.jsxs)("div", {
746
+ className: "s2a_rowIdentity",
747
+ children: [
748
+ (0, react_jsx_runtime.jsx)(ProviderIcon, {
749
+ name: def.icon,
750
+ size: 18
751
+ }),
752
+ (0, react_jsx_runtime.jsx)("span", {
753
+ className: "s2a_rowName",
754
+ children: def.label
755
+ }),
756
+ (0, react_jsx_runtime.jsxs)("span", {
757
+ className: "s2a_rowTag",
758
+ children: ["sub2api-", def.key]
759
+ })
760
+ ]
761
+ }), (0, react_jsx_runtime.jsxs)("div", {
762
+ className: "s2a_rowActions",
763
+ children: [(0, react_jsx_runtime.jsx)("button", {
764
+ className: "s2a_btn",
765
+ disabled: busy.length > 0,
766
+ onClick: () => discover(def),
767
+ children: busy === `discover-${def.key}` ? "…" : "获取模型"
768
+ }), (0, react_jsx_runtime.jsx)("button", {
769
+ className: "s2a_btn",
770
+ disabled: busy.length > 0,
771
+ onClick: () => checkUsage(def),
772
+ children: busy === `usage-${def.key}` ? "…" : "查看用量"
773
+ })]
774
+ })]
775
+ }), (0, react_jsx_runtime.jsxs)("div", {
776
+ className: "s2a_editor",
777
+ children: [(0, react_jsx_runtime.jsxs)("div", {
778
+ className: "s2a_field",
779
+ children: [(0, react_jsx_runtime.jsxs)("label", {
780
+ className: "s2a_fieldLabel",
781
+ children: [
782
+ def.label,
783
+ " API Key",
784
+ provider.keyConfigured || provider.key.length > 0 ? " ✓" : ""
785
+ ]
786
+ }), (0, react_jsx_runtime.jsx)("input", {
787
+ className: "s2a_input",
788
+ type: "password",
789
+ value: provider.key,
790
+ placeholder: provider.keyConfigured ? `${def.placeholder}(已配置,留空保持不变)` : def.placeholder,
791
+ onChange: (event) => updateProvider(def.key, { key: event.target.value })
792
+ })]
793
+ }), (0, react_jsx_runtime.jsxs)("div", {
794
+ className: "s2a_field",
795
+ children: [
796
+ (0, react_jsx_runtime.jsx)("label", {
797
+ className: "s2a_fieldLabel",
798
+ children: "模型列表"
799
+ }),
800
+ (0, react_jsx_runtime.jsx)("div", {
801
+ className: "s2a_models",
802
+ children: provider.models.length === 0 ? (0, react_jsx_runtime.jsx)("p", {
803
+ className: "s2a_modelEmpty",
804
+ children: "暂无模型"
805
+ }) : provider.models.map((row) => {
806
+ const expanded = expandedModels.has(row.rowId);
807
+ const detailsId = `s2a-model-${row.rowId}-details`;
808
+ return (0, react_jsx_runtime.jsxs)("div", {
809
+ className: "s2a_modelItem",
810
+ children: [(0, react_jsx_runtime.jsxs)("div", {
811
+ className: "s2a_modelSummary",
812
+ children: [
813
+ (0, react_jsx_runtime.jsx)("div", { children: (0, react_jsx_runtime.jsx)("input", {
814
+ className: "s2a_input",
815
+ value: row.id,
816
+ placeholder: "模型 ID",
817
+ "aria-label": `${def.label} 模型 ID`,
818
+ onChange: (event) => updateModel(def.key, row.rowId, { id: event.target.value }),
819
+ onBlur: () => fillModel(def, row.rowId)
820
+ }) }),
821
+ (0, react_jsx_runtime.jsx)("div", { children: (0, react_jsx_runtime.jsx)("input", {
822
+ className: "s2a_input",
823
+ value: row.name,
824
+ placeholder: "名称",
825
+ "aria-label": `${def.label} 模型名称`,
826
+ onChange: (event) => updateModel(def.key, row.rowId, { name: event.target.value })
827
+ }) }),
828
+ (0, react_jsx_runtime.jsx)("button", {
829
+ type: "button",
830
+ className: "s2a_iconBtn s2a_expandBtn",
831
+ title: expanded ? "收起模型详情" : "展开模型详情",
832
+ "aria-label": expanded ? `收起 ${row.id || "模型"} 详情` : `展开 ${row.id || "模型"} 详情`,
833
+ "aria-expanded": expanded,
834
+ "aria-controls": detailsId,
835
+ onClick: () => toggleModel(row.rowId),
836
+ children: (0, react_jsx_runtime.jsx)(IconChevron, { expanded })
837
+ }),
838
+ (0, react_jsx_runtime.jsx)("button", {
839
+ type: "button",
840
+ className: "s2a_iconBtn s2a_trash",
841
+ title: "删除模型",
842
+ "aria-label": `删除 ${row.id || "模型"}`,
843
+ onClick: () => updateProvider(def.key, { models: provider.models.filter((item) => item.rowId !== row.rowId) }),
844
+ children: (0, react_jsx_runtime.jsx)(IconTrash, {})
845
+ })
846
+ ]
847
+ }), expanded && (0, react_jsx_runtime.jsxs)("div", {
848
+ id: detailsId,
849
+ className: "s2a_modelDetails",
850
+ children: [
851
+ (0, react_jsx_runtime.jsxs)("div", {
852
+ className: "s2a_field",
853
+ children: [(0, react_jsx_runtime.jsx)("label", {
854
+ className: "s2a_fieldLabel",
855
+ children: "上下文窗口"
856
+ }), (0, react_jsx_runtime.jsx)("input", {
857
+ className: "s2a_input",
858
+ type: "number",
859
+ min: "1",
860
+ step: "1",
861
+ value: row.contextWindow,
862
+ placeholder: "自动填充",
863
+ "aria-label": `${def.label} 上下文窗口`,
864
+ onChange: (event) => updateModel(def.key, row.rowId, { contextWindow: event.target.value })
865
+ })]
866
+ }),
867
+ (0, react_jsx_runtime.jsxs)("div", {
868
+ className: "s2a_field",
869
+ children: [(0, react_jsx_runtime.jsx)("label", {
870
+ className: "s2a_fieldLabel",
871
+ children: "最大输出 token"
872
+ }), (0, react_jsx_runtime.jsx)("input", {
873
+ className: "s2a_input",
874
+ type: "number",
875
+ min: "1",
876
+ step: "1",
877
+ value: row.maxTokens,
878
+ placeholder: "自动填充",
879
+ "aria-label": `${def.label} 最大输出 token`,
880
+ onChange: (event) => updateModel(def.key, row.rowId, { maxTokens: event.target.value })
881
+ })]
882
+ }),
883
+ (0, react_jsx_runtime.jsxs)("div", {
884
+ className: "s2a_field",
885
+ children: [(0, react_jsx_runtime.jsx)("label", {
886
+ className: "s2a_fieldLabel",
887
+ children: "图片输入"
888
+ }), (0, react_jsx_runtime.jsxs)("select", {
889
+ className: "s2a_input",
890
+ "aria-label": `${def.label} ${row.id} 图片输入`,
891
+ value: row.input,
892
+ onChange: (event) => updateModel(def.key, row.rowId, {
893
+ input: event.target.value,
894
+ inputEdited: true
895
+ }),
896
+ children: [
897
+ (0, react_jsx_runtime.jsx)("option", {
898
+ value: "",
899
+ children: "自动(按模型推断)"
900
+ }),
901
+ (0, react_jsx_runtime.jsx)("option", {
902
+ value: "text",
903
+ children: "仅文本"
904
+ }),
905
+ (0, react_jsx_runtime.jsx)("option", {
906
+ value: "text-image",
907
+ children: "文本 + 图片"
908
+ })
909
+ ]
910
+ })]
911
+ }),
912
+ (0, react_jsx_runtime.jsxs)("div", {
913
+ className: "s2a_field s2a_reasoningField",
914
+ children: [
915
+ (0, react_jsx_runtime.jsx)("label", {
916
+ className: "s2a_fieldLabel",
917
+ children: "思考强度"
918
+ }),
919
+ (0, react_jsx_runtime.jsxs)("select", {
920
+ className: "s2a_input",
921
+ "aria-label": `${def.label} ${row.id} 思考模式`,
922
+ value: row.reasoning,
923
+ onChange: (event) => updateModel(def.key, row.rowId, {
924
+ reasoning: event.target.value,
925
+ reasoningEdited: true
926
+ }),
927
+ children: [
928
+ (0, react_jsx_runtime.jsx)("option", {
929
+ value: "",
930
+ children: "自动"
931
+ }),
932
+ (0, react_jsx_runtime.jsx)("option", {
933
+ value: "off",
934
+ children: "不支持"
935
+ }),
936
+ (0, react_jsx_runtime.jsx)("option", {
937
+ value: "on",
938
+ children: "手动输入档位"
939
+ })
940
+ ]
941
+ }),
942
+ row.reasoning === "on" && (0, react_jsx_runtime.jsx)("input", {
943
+ className: "s2a_input",
944
+ value: row.effortLevels,
945
+ "aria-label": `${def.label} ${row.id} 思考强度档位`,
946
+ placeholder: "例如 none, low, high, max",
947
+ onChange: (event) => updateModel(def.key, row.rowId, {
948
+ effortLevels: event.target.value,
949
+ reasoningEdited: true
950
+ })
951
+ }),
952
+ (0, react_jsx_runtime.jsx)("span", {
953
+ className: "s2a_modelSource",
954
+ children: "多个档位用逗号分隔。手动设置不会被补全数据覆盖。"
955
+ })
956
+ ]
957
+ })
958
+ ]
959
+ })]
960
+ }, row.rowId);
961
+ })
962
+ }),
963
+ (0, react_jsx_runtime.jsxs)("div", {
964
+ className: "s2a_modelFooter",
965
+ children: [(0, react_jsx_runtime.jsxs)("span", {
966
+ className: "s2a_modelSource",
967
+ children: [
968
+ "默认值来自 ",
969
+ (0, react_jsx_runtime.jsx)("a", {
970
+ href: "https://models.dev/",
971
+ target: "_blank",
972
+ rel: "noreferrer",
973
+ children: "models.dev"
974
+ }),
975
+ ",未匹配时可手动填写"
976
+ ]
977
+ }), (0, react_jsx_runtime.jsxs)("div", {
978
+ className: "s2a_modelActions",
979
+ children: [(0, react_jsx_runtime.jsx)("button", {
980
+ className: "s2a_btn",
981
+ disabled: busy.length > 0 || provider.models.length === 0,
982
+ onClick: () => fillProvider(def),
983
+ children: busy === `metadata-${def.key}` ? "…" : "补全数据"
984
+ }), (0, react_jsx_runtime.jsx)("button", {
985
+ className: "s2a_btn",
986
+ disabled: busy.length > 0,
987
+ onClick: () => updateProvider(def.key, { models: [...provider.models, modelRow()] }),
988
+ children: "添加模型"
989
+ })]
990
+ })]
991
+ })
992
+ ]
993
+ })]
994
+ })]
995
+ }, def.key);
996
+ })
997
+ }),
998
+ (0, react_jsx_runtime.jsxs)("div", {
999
+ className: "s2a_actions",
1000
+ children: [(0, react_jsx_runtime.jsx)("button", {
1001
+ className: "s2a_primary",
1002
+ disabled: busy.length > 0,
1003
+ onClick: save,
1004
+ children: busy === "save" ? "保存中…" : "保存配置"
1005
+ }), (0, react_jsx_runtime.jsx)("button", {
1006
+ className: "s2a_btn",
1007
+ disabled: busy.length > 0,
1008
+ onClick: checkStatus,
1009
+ children: busy === "status" ? "…" : "查看状态"
1010
+ })]
1011
+ }),
1012
+ (message || error) && (0, react_jsx_runtime.jsxs)("div", {
1013
+ className: "s2a_toast",
1014
+ role: "status",
1015
+ "aria-live": "polite",
1016
+ children: [(0, react_jsx_runtime.jsx)("p", {
1017
+ className: `s2a_status ${error ? "s2a_statusErr" : "s2a_statusOk"}`,
1018
+ children: error || message
1019
+ }), (0, react_jsx_runtime.jsx)("button", {
1020
+ className: "s2a_iconBtn",
1021
+ "aria-label": "关闭提示",
1022
+ onClick: () => {
1023
+ setMessage("");
1024
+ setError("");
1025
+ },
1026
+ children: "×"
1027
+ })]
1028
+ })
1029
+ ]
1030
+ });
1031
+ }
1032
+
1033
+ //#endregion
1034
+ //#region src/client/toolview.tsx
1035
+ const ROOT = {
1036
+ display: "grid",
1037
+ gap: "8px",
1038
+ padding: "8px 10px"
1039
+ };
1040
+ const IMAGE = {
1041
+ maxWidth: "100%",
1042
+ maxHeight: 420,
1043
+ objectFit: "contain",
1044
+ borderRadius: 8,
1045
+ background: "var(--dsw-alias-bg-module-platform, #f2f2f2)"
1046
+ };
1047
+ const META = {
1048
+ fontSize: 11,
1049
+ lineHeight: 1.5,
1050
+ color: "var(--dsw-alias-label-secondary, #6b6b6b)",
1051
+ whiteSpace: "pre-wrap",
1052
+ wordBreak: "break-word"
1053
+ };
1054
+ /** Attachment-served URL for one image content block. */
1055
+ function attachmentUrl(image) {
1056
+ const ref = JSON.stringify(image.attachment);
1057
+ return `/plugins/dsh-sub2api/attachment?ref=${encodeURIComponent(btoa(ref))}`;
1058
+ }
1059
+ function GenerateImageToolview(props) {
1060
+ const { block } = props;
1061
+ const content = "content" in block ? block.content : [];
1062
+ const image = content.find((b) => b.type === "image" && b.attachment !== undefined);
1063
+ const text = content.find((b) => b.type === "text");
1064
+ if (image === undefined && text === undefined) return (0, react_jsx_runtime.jsx)("div", {
1065
+ style: {
1066
+ ...META,
1067
+ padding: "8px 10px"
1068
+ },
1069
+ children: "生成图片…"
1070
+ });
1071
+ return (0, react_jsx_runtime.jsxs)("div", {
1072
+ style: ROOT,
1073
+ children: [image !== undefined && (0, react_jsx_runtime.jsx)("img", {
1074
+ src: attachmentUrl(image),
1075
+ alt: image.attachment.name ?? "generated image",
1076
+ style: IMAGE
1077
+ }), text !== undefined && (0, react_jsx_runtime.jsx)("div", {
1078
+ style: META,
1079
+ children: text.text
1080
+ })]
1081
+ });
1082
+ }
1083
+
1084
+ //#endregion
1085
+ //#region src/client/index.tsx
1086
+ const name = "dsh-sub2api-client";
1087
+ const inject = ["slots"];
1088
+ function apply(ctx) {
1089
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1090
+ name: "settings.section",
1091
+ id: "sub2api-models",
1092
+ order: 12,
1093
+ label: () => "Sub2API 模型"
1094
+ }, Sub2ApiSettings));
1095
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1096
+ name: "tool.call.toolview",
1097
+ key: "generate_image"
1098
+ }, GenerateImageToolview));
1099
+ }
1100
+
1101
+ //#endregion
1102
+ exports.apply = apply
1103
+ exports.inject = inject
1104
+ exports.name = name
1105
+ return module.exports;
1106
+ }
1107
+ });