dsh-any-deepseek 0.0.3 → 0.0.5
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/channels.d.ts +20 -5
- package/lib/client.js +172 -128
- package/lib/config.d.ts +0 -2
- package/lib/http.d.ts +11 -1
- package/lib/index.js +35 -25
- package/lib/route.d.ts +6 -5
- package/package.json +1 -1
package/lib/channels.d.ts
CHANGED
|
@@ -29,7 +29,16 @@ export interface ChannelEntry {
|
|
|
29
29
|
model?: string;
|
|
30
30
|
/** Credential reference (environment-variable name). Required. */
|
|
31
31
|
apiKeyEnv?: string;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Retired. Kept in the schema only so an existing configuration that still carries
|
|
34
|
+
* the field does not fail validation; it is **ignored**.
|
|
35
|
+
*
|
|
36
|
+
* The design has exactly one axis for "which relay serves requests": `current`. A second
|
|
37
|
+
* on/off axis says the same thing twice and can contradict itself — a channel that is
|
|
38
|
+
* `enabled` but not `current` does nothing, and one that is `current` but disabled is a
|
|
39
|
+
* straight incoherence. Removing the axis also removes a whole class of UI state: there is no
|
|
40
|
+
* such thing as a "closed channel" to render greyed out.
|
|
41
|
+
*/
|
|
33
42
|
enabled?: boolean;
|
|
34
43
|
/** Overrides applied on top of a preset, or the whole declaration for a custom channel. */
|
|
35
44
|
contextWindow?: number;
|
|
@@ -98,11 +107,17 @@ export declare function resolveChannel(id: string, raw: unknown): {
|
|
|
98
107
|
*/
|
|
99
108
|
export declare function resolveChannels(entries: unknown): ChannelResolution;
|
|
100
109
|
/**
|
|
101
|
-
*
|
|
110
|
+
* Every resolved channel, in configuration order — the panel's list and the router's candidate set.
|
|
111
|
+
*
|
|
112
|
+
* 🔴 There is no filtering step here, deliberately. This used to be `enabledChannels()`, which
|
|
113
|
+
* dropped entries with `enabled: false`. That made two things impossible:
|
|
114
|
+
* · the panel could not show a channel it had hidden, so you could not switch it back on;
|
|
115
|
+
* · "which relay serves requests" had two answers (`enabled` AND `current`) that could disagree.
|
|
116
|
+
* The design has one axis — `current` — so the only list is "all channels", and selection is a
|
|
117
|
+
* separate fact.
|
|
102
118
|
* @param resolution - output of {@link resolveChannels}.
|
|
103
|
-
* @
|
|
104
|
-
* @returns enabled channels, configuration order preserved.
|
|
119
|
+
* @returns all resolved channels (the router picks one of them via `current`).
|
|
105
120
|
*/
|
|
106
|
-
export declare function
|
|
121
|
+
export declare function allChannels(resolution: ChannelResolution): ResolvedChannel[];
|
|
107
122
|
/** The advertised model id, re-exported so callers need one import for routing facts. */
|
|
108
123
|
export { ADVERTISED_MODEL_ID };
|
package/lib/client.js
CHANGED
|
@@ -117,7 +117,7 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
}
|
|
118
118
|
//#endregion
|
|
119
119
|
//#region \0ad-css:src/client/SettingsPage
|
|
120
|
-
const css = "/* Panel styles
|
|
120
|
+
const css = "/* Panel styles.\n *\n * 🔴 There are NO `--dsw-alias-*` design tokens on this host (measured 2026-10-21: the document\n * declares 29 custom properties and not one is a colour). This file used to reference a dozen of\n * them, every one falling back to a hardcoded LIGHT value (`rgba(0,0,0,0.12)` and friends) — so\n * on the dark theme the borders and dividers were effectively invisible.\n *\n * ⇒ Derive everything from `currentColor`, which the host sets per theme. `color-mix` gives the\n * low-contrast borders and fills that a token would have provided, and it follows the theme\n * automatically instead of assuming one palette.\n */\n\n.ad-page {\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 4px 2px 12px;\n}\n\n/* One place to change the two theme-derived values the whole panel uses. */\n.ad-page {\n --ad-line: color-mix(in srgb, currentColor 18%, transparent);\n --ad-line-strong: color-mix(in srgb, currentColor 32%, transparent);\n --ad-fill-subtle: color-mix(in srgb, currentColor 7%, transparent);\n --ad-danger: #e5534b;\n}\n\n.ad-head {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ad-title {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n}\n\n.ad-intro {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n opacity: 0.72;\n}\n\n.ad-error {\n border: 1px solid var(--ad-danger);\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n\n.ad-section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.ad-sectionTitle {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n opacity: 0.6;\n}\n\n.ad-rows {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ad-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n border: 1px solid var(--ad-line);\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.ad-row-current {\n border-color: var(--ad-line-strong);\n}\n\n.ad-rowMain {\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n\n.ad-rowTitleLine {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n font-weight: 500;\n}\n\n.ad-rowTitle {\n white-space: nowrap;\n}\n\n.ad-badge {\n font-size: 10px;\n padding: 1px 6px;\n border-radius: 999px;\n border: 1px solid currentColor;\n opacity: 0.8;\n}\n\n.ad-warn {\n color: #b7791f;\n cursor: help;\n}\n\n.ad-rowMeta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n opacity: 0.66;\n min-width: 0;\n}\n\n.ad-mono {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n word-break: break-all;\n}\n\n.ad-sep {\n opacity: 0.5;\n}\n\n.ad-rowActions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n\n.ad-btn {\n font-size: 11px;\n padding: 4px 10px;\n border-radius: 6px;\n border: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.ad-btn:disabled {\n opacity: 0.45;\n cursor: default;\n}\n\n.ad-btn-quiet {\n opacity: 0.8;\n}\n\n.ad-probe {\n font-size: 11px;\n padding-top: 2px;\n}\n\n.ad-probe-ok {\n color: #2f855a;\n}\n\n.ad-probe-bad {\n color: #c53030;\n word-break: break-word;\n}\n\n.ad-hint,\n.ad-empty,\n.ad-note {\n margin: 0;\n font-size: 11px;\n line-height: 1.6;\n opacity: 0.66;\n}\n\n.ad-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n}\n\n.ad-dot-on {\n background: #2f855a;\n}\n\n.ad-dot-off {\n border: 1px solid currentColor;\n opacity: 0.5;\n}\n\n.ad-dot-unknown {\n background: linear-gradient(90deg, currentColor 50%, transparent 50%);\n opacity: 0.5;\n}\n\n/* The channel row is a radio \"pick\" zone: the whole left side is clickable, and the radio sits\n at the start of the line so \"which one is live\" is readable down a single column. */\n.ad-rowPick {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n flex: 1;\n min-width: 0;\n cursor: pointer;\n}\n\n.ad-rowPick input[type='radio'] {\n flex: none;\n /* Optically centred against the first text line rather than the row box. */\n margin-top: 3px;\n accent-color: var(--ad-line-strong);\n cursor: pointer;\n}\n\n/* The switch is gone: selection is the only axis (see `channels.ts`). Keeping the rules would\n invite someone to bring the second axis back. */\n\n.ad-tabs {\n display: flex;\n gap: 6px;\n}\n\n.ad-tab {\n font-size: 11px;\n padding: 3px 10px;\n border-radius: 999px;\n border: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.ad-tab-on {\n border-color: var(--ad-line-strong);\n font-weight: 600;\n}\n\n.ad-tab-refresh {\n margin-left: auto;\n opacity: 0.75;\n}\n\n.ad-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n}\n\n.ad-table th,\n.ad-table td {\n padding: 5px 8px;\n border-bottom: 1px solid var(--ad-line);\n text-align: left;\n}\n\n.ad-table th {\n font-weight: 500;\n opacity: 0.6;\n}\n\n.ad-num {\n text-align: right;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n\n.ad-total td {\n font-weight: 600;\n border-bottom: none;\n}\n\n/* ── add-channel form ─────────────────────────────────────────────────────── */\n\n.ad-addForm {\n display: flex;\n flex-direction: column;\n gap: 8px;\n border: 1px dashed var(--ad-line);\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.ad-field {\n display: flex;\n flex-direction: column;\n gap: 3px;\n font-size: 11px;\n}\n\n.ad-field > span {\n opacity: 0.7;\n}\n\n.ad-input {\n font-size: 11px;\n padding: 4px 8px;\n border-radius: 6px;\n border: 1px solid var(--ad-line);\n background: transparent;\n color: inherit;\n font-family: inherit;\n}\n\n/*\n * 🔴 A native `<select>` cannot be themed by leaving the background transparent.\n *\n * The popup list is drawn by the browser, not by the closed control's box: the options inherit\n * nothing from it, so `background: transparent` made them render in the platform default (light)\n * palette against this dark theme — reported by the owner and measured on the real host.\n *\n * 🔴 SECOND correction (measured 2026-10-21): this file used `var(--dsw-alias-color-*, #fallback)`\n * throughout, and **none of those variables exist**. The host declares only 29 custom properties\n * in total and not one is a colour token, so every `var()` here was falling back — and a fallback\n * that happens to be set inside a shorthand can be silently dropped (the select's background\n * computed to `rgba(0,0,0,0)`, i.e. still transparent, which is exactly the bug).\n *\n * ⇒ Use only values this file controls, derived from `currentColor` (the host sets a light text\n * colour on both themes) plus explicit translucent layers. No host token is assumed.\n */\nselect.ad-input {\n /* Tells the engine which palette to use for the popup. This part DID work. */\n color-scheme: dark light;\n /*\n * Paint the closed box with a layer that follows the surrounding text colour, so it matches\n * whichever theme is active instead of hardcoding a dark value into a light theme.\n */\n background-color: color-mix(in srgb, currentColor 8%, transparent);\n color: inherit;\n cursor: pointer;\n padding-right: 22px;\n /* Room for the chevron drawn below, so text never runs under it. */\n appearance: none;\n background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),\n linear-gradient(135deg, currentColor 50%, transparent 50%);\n background-position: calc(100% - 12px) calc(50% + 1px), calc(100% - 8px) calc(50% + 1px);\n background-size: 4px 4px, 4px 4px;\n background-repeat: no-repeat;\n}\n\nselect.ad-input:hover:not(:disabled) {\n background-color: color-mix(in srgb, currentColor 14%, transparent);\n}\n\nselect.ad-input:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 1px;\n}\n\n/*\n * The popup itself follows `color-scheme`; these keep the option rows explicit for engines that\n * honour per-option colours. `color-scheme: dark` above is what actually stops the light-on-dark\n * list, because the popup is native chrome rather than a styled element.\n */\nselect.ad-input option {\n background-color: Canvas;\n color: CanvasText;\n}\n\n.ad-fieldError {\n margin: 0;\n font-size: 11px;\n color: #c53030;\n}\n\n/* A destructive action is quiet until hovered: it should be findable, not inviting. */\n.ad-btn-danger {\n opacity: 0.6;\n}\n\n.ad-btn-danger:hover:not(:disabled) {\n opacity: 1;\n border-color: var(--ad-danger);\n color: var(--ad-danger);\n}\n";
|
|
121
121
|
if (typeof document !== "undefined" && document.querySelector("style[data-ad-css=\"SettingsPage\"]") === null) {
|
|
122
122
|
const tag = document.createElement("style");
|
|
123
123
|
tag.setAttribute("data-ad-css", "SettingsPage");
|
|
@@ -126,17 +126,38 @@ window.__ModuleLoader__.load({
|
|
|
126
126
|
}
|
|
127
127
|
//#endregion
|
|
128
128
|
//#region src/client/SettingsPage.tsx
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
/**
|
|
130
|
+
* A channel id derived from a preset id, made unique against what already exists.
|
|
131
|
+
*
|
|
132
|
+
* The id is a configuration key, so it must match `^[a-z0-9][a-z0-9_-]*$`. Preset ids already do,
|
|
133
|
+
* so the only work is de-duplication: adding two Open Design relays is legitimate, and the second
|
|
134
|
+
* must not silently overwrite the first.
|
|
135
|
+
* @param presetId - the chosen preset.
|
|
136
|
+
* @param existing - rows already configured.
|
|
137
|
+
* @returns a free id like `open-design`, `open-design-2`, …
|
|
138
|
+
*/
|
|
139
|
+
function suggestId(presetId, existing) {
|
|
140
|
+
if (presetId === "") return "";
|
|
141
|
+
const taken = new Set(existing.map((row) => row.id));
|
|
142
|
+
if (!taken.has(presetId)) return presetId;
|
|
143
|
+
for (let n = 2; n < 1e3; n++) {
|
|
144
|
+
const candidate = `${presetId}-${n}`;
|
|
145
|
+
if (!taken.has(candidate)) return candidate;
|
|
146
|
+
}
|
|
147
|
+
return `${presetId}-${Date.now()}`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The credential reference name a preset conventionally uses.
|
|
151
|
+
*
|
|
152
|
+
* This is a CONVENTION, not a lookup: the reference is just the environment-variable name the
|
|
153
|
+
* user's key is stored under, and any name works. Pre-filling the conventional one saves typing;
|
|
154
|
+
* `OPEN_DESIGN` → `OPEN_DESIGN_API_KEY`.
|
|
155
|
+
* @param presetId - the chosen preset.
|
|
156
|
+
* @returns the suggested reference name ('' when no preset is chosen).
|
|
157
|
+
*/
|
|
158
|
+
function suggestKeyRef(presetId) {
|
|
159
|
+
if (presetId === "") return "";
|
|
160
|
+
return `${presetId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
|
|
140
161
|
}
|
|
141
162
|
/** A status dot: filled when confirmed configured, hollow when confirmed absent, half otherwise. */
|
|
142
163
|
function KeyDot(props) {
|
|
@@ -167,6 +188,16 @@ window.__ModuleLoader__.load({
|
|
|
167
188
|
const [newId, setNewId] = (0, react.useState)("");
|
|
168
189
|
const [newPreset, setNewPreset] = (0, react.useState)("");
|
|
169
190
|
const [newKeyRef, setNewKeyRef] = (0, react.useState)("");
|
|
191
|
+
/**
|
|
192
|
+
* The reference name the form last derived from a preset.
|
|
193
|
+
*
|
|
194
|
+
* Needed to tell "the user never touched this field" from "the user typed it": only a value that
|
|
195
|
+
* still equals the last suggestion may be replaced when the preset changes. Without it,
|
|
196
|
+
* switching presets after typing a custom reference would silently discard the user's input.
|
|
197
|
+
*/
|
|
198
|
+
const lastSuggestedKeyRef = (0, react.useRef)("");
|
|
199
|
+
/** Same idea for the derived channel id (see the select's onChange). */
|
|
200
|
+
const lastSuggestedId = (0, react.useRef)("");
|
|
170
201
|
const refresh = (0, react.useCallback)(async () => {
|
|
171
202
|
const [loaded, statsNow] = await Promise.all([loadChannels(), loadStats(group)]);
|
|
172
203
|
setRows(loaded.channels);
|
|
@@ -184,17 +215,9 @@ window.__ModuleLoader__.load({
|
|
|
184
215
|
});
|
|
185
216
|
}, [refresh]);
|
|
186
217
|
const current = settings.value?.current ?? "";
|
|
187
|
-
const channels = settings.value?.channels ?? {};
|
|
188
218
|
const select = (id) => {
|
|
189
219
|
write(() => setField(["current"], id));
|
|
190
220
|
};
|
|
191
|
-
const toggleChannel = (id, on) => {
|
|
192
|
-
write(() => setField([
|
|
193
|
-
"channels",
|
|
194
|
-
id,
|
|
195
|
-
"enabled"
|
|
196
|
-
], on), "切换渠道开关失败");
|
|
197
|
-
};
|
|
198
221
|
/**
|
|
199
222
|
* Run one settings write and surface a refusal.
|
|
200
223
|
*
|
|
@@ -236,7 +259,7 @@ window.__ModuleLoader__.load({
|
|
|
236
259
|
const id = newId.trim();
|
|
237
260
|
if (id === "") return null;
|
|
238
261
|
if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) return "只能用小写字母、数字、- 和 _,且不能以符号开头";
|
|
239
|
-
if (
|
|
262
|
+
if (rows.some((row) => row.id === id)) return "这个 id 已经有了";
|
|
240
263
|
return null;
|
|
241
264
|
})();
|
|
242
265
|
/**
|
|
@@ -248,15 +271,17 @@ window.__ModuleLoader__.load({
|
|
|
248
271
|
*/
|
|
249
272
|
const addChannel = () => {
|
|
250
273
|
const id = newId.trim();
|
|
251
|
-
if (id === "" || newIdProblem !== null || newKeyRef.trim() === "") return;
|
|
274
|
+
if (newPreset === "" || id === "" || newIdProblem !== null || newKeyRef.trim() === "") return;
|
|
252
275
|
write(() => setField(["channels", id], {
|
|
253
|
-
|
|
276
|
+
preset: newPreset,
|
|
254
277
|
apiKeyEnv: newKeyRef.trim()
|
|
255
278
|
}), "添加渠道失败");
|
|
256
279
|
if (current === "") write(() => setField(["current"], id), "设为当前渠道失败");
|
|
257
280
|
setNewId("");
|
|
258
281
|
setNewKeyRef("");
|
|
259
282
|
setNewPreset("");
|
|
283
|
+
lastSuggestedKeyRef.current = "";
|
|
284
|
+
lastSuggestedId.current = "";
|
|
260
285
|
setAdding(false);
|
|
261
286
|
};
|
|
262
287
|
const removeChannel = (id) => {
|
|
@@ -274,9 +299,14 @@ window.__ModuleLoader__.load({
|
|
|
274
299
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
275
300
|
className: "ad-intro",
|
|
276
301
|
children: [
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
"
|
|
302
|
+
"这里定义的是 ",
|
|
303
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "any-deepseek 这一个 provider" }),
|
|
304
|
+
" 的内部渠道, 与 DSH 别处的设置无关。本插件对外只暴露一个模型 (",
|
|
305
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
306
|
+
className: "ad-mono",
|
|
307
|
+
children: model.name || "DeepSeek V4.1 Flash"
|
|
308
|
+
}),
|
|
309
|
+
"); 下面勾选哪一个渠道,请求就只发到那一个,不会自动切换。"
|
|
280
310
|
]
|
|
281
311
|
})]
|
|
282
312
|
}),
|
|
@@ -298,95 +328,90 @@ window.__ModuleLoader__.load({
|
|
|
298
328
|
}),
|
|
299
329
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
300
330
|
className: "ad-rows",
|
|
331
|
+
role: "radiogroup",
|
|
332
|
+
"aria-label": "选择当前渠道",
|
|
301
333
|
children: rows.map((row) => {
|
|
302
|
-
const enabled = channels[row.id]?.enabled !== false;
|
|
303
334
|
const isCurrent = current === row.id;
|
|
304
335
|
const probeResult = verdict[row.id];
|
|
305
336
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
306
337
|
className: `ad-row${isCurrent ? " ad-row-current" : ""}`,
|
|
307
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
308
|
-
className: "ad-
|
|
309
|
-
children: [
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
children: row.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
338
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
339
|
+
className: "ad-rowPick",
|
|
340
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
341
|
+
type: "radio",
|
|
342
|
+
name: "ad-current-channel",
|
|
343
|
+
checked: isCurrent,
|
|
344
|
+
onChange: () => select(row.id)
|
|
345
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
346
|
+
className: "ad-rowMain",
|
|
347
|
+
children: [
|
|
348
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
349
|
+
className: "ad-rowTitleLine",
|
|
350
|
+
children: [
|
|
351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyDot, {
|
|
352
|
+
configured: row.keyConfigured,
|
|
353
|
+
known: row.keySource !== void 0 || row.keyConfigured
|
|
354
|
+
}),
|
|
355
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
356
|
+
className: "ad-rowTitle",
|
|
357
|
+
children: row.label
|
|
358
|
+
}),
|
|
359
|
+
isCurrent && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
360
|
+
className: "ad-badge",
|
|
361
|
+
children: "当前"
|
|
362
|
+
}),
|
|
363
|
+
row.warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
364
|
+
className: "ad-warn",
|
|
365
|
+
title: row.warnings.join("\n"),
|
|
366
|
+
children: "⚠"
|
|
367
|
+
})
|
|
368
|
+
]
|
|
369
|
+
}),
|
|
370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
371
|
+
className: "ad-rowMeta",
|
|
372
|
+
children: [
|
|
373
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
374
|
+
className: "ad-mono",
|
|
375
|
+
children: row.model
|
|
376
|
+
}),
|
|
377
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
378
|
+
className: "ad-sep",
|
|
379
|
+
children: "·"
|
|
380
|
+
}),
|
|
381
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: row.api }),
|
|
382
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
383
|
+
className: "ad-sep",
|
|
384
|
+
children: "·"
|
|
385
|
+
}),
|
|
386
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
387
|
+
className: "ad-mono",
|
|
388
|
+
children: row.baseURL
|
|
389
|
+
})
|
|
390
|
+
]
|
|
391
|
+
}),
|
|
392
|
+
probeResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
393
|
+
className: `ad-probe ad-probe-${probeResult.ok ? "ok" : "bad"}`,
|
|
394
|
+
children: probeResult.ok ? `通(${probeResult.ms}ms)` : `不通:${probeResult.code ?? "ERROR"}${probeResult.message === void 0 ? "" : ` — ${probeResult.message}`}`
|
|
395
|
+
})
|
|
396
|
+
]
|
|
397
|
+
})]
|
|
359
398
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
360
399
|
className: "ad-rowActions",
|
|
361
|
-
children: [
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
children: probing === row.id ? "测试中…" : "测试"
|
|
377
|
-
}),
|
|
378
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
379
|
-
type: "button",
|
|
380
|
-
className: "ad-btn ad-btn-danger",
|
|
381
|
-
title: "移除此渠道(不删除凭据本身)",
|
|
382
|
-
onClick: () => removeChannel(row.id),
|
|
383
|
-
children: "移除"
|
|
384
|
-
}),
|
|
385
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
|
|
386
|
-
checked: enabled,
|
|
387
|
-
onChange: (on) => toggleChannel(row.id, on)
|
|
388
|
-
})
|
|
389
|
-
]
|
|
400
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
401
|
+
type: "button",
|
|
402
|
+
className: "ad-btn ad-btn-quiet",
|
|
403
|
+
disabled: probing === row.id,
|
|
404
|
+
onClick: () => {
|
|
405
|
+
runProbe(row.id);
|
|
406
|
+
},
|
|
407
|
+
children: probing === row.id ? "测试中…" : "测试"
|
|
408
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
409
|
+
type: "button",
|
|
410
|
+
className: "ad-btn ad-btn-danger",
|
|
411
|
+
title: "移除此渠道(不删除凭据本身)",
|
|
412
|
+
onClick: () => removeChannel(row.id),
|
|
413
|
+
children: "移除"
|
|
414
|
+
})]
|
|
390
415
|
})]
|
|
391
416
|
}, row.id);
|
|
392
417
|
})
|
|
@@ -394,53 +419,68 @@ window.__ModuleLoader__.load({
|
|
|
394
419
|
adding ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
395
420
|
className: "ad-addForm",
|
|
396
421
|
children: [
|
|
397
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
398
|
-
className: "ad-field",
|
|
399
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "渠道 id" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
400
|
-
className: "ad-input",
|
|
401
|
-
value: newId,
|
|
402
|
-
placeholder: "例如 opencode-go",
|
|
403
|
-
onChange: (event) => setNewId(event.target.value)
|
|
404
|
-
})]
|
|
405
|
-
}),
|
|
406
|
-
newIdProblem !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
407
|
-
className: "ad-fieldError",
|
|
408
|
-
children: newIdProblem
|
|
409
|
-
}),
|
|
410
422
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
411
423
|
className: "ad-field",
|
|
412
424
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "规格" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
413
425
|
className: "ad-input",
|
|
414
426
|
value: newPreset,
|
|
415
|
-
onChange: (event) =>
|
|
427
|
+
onChange: (event) => {
|
|
428
|
+
const next = event.target.value;
|
|
429
|
+
setNewPreset(next);
|
|
430
|
+
const previousId = lastSuggestedId.current;
|
|
431
|
+
const previousKey = lastSuggestedKeyRef.current;
|
|
432
|
+
const nextId = suggestId(next, rows);
|
|
433
|
+
const nextKey = suggestKeyRef(next);
|
|
434
|
+
setNewId((current) => current === "" || current === previousId ? nextId : current);
|
|
435
|
+
setNewKeyRef((current) => current === "" || current === previousKey ? nextKey : current);
|
|
436
|
+
lastSuggestedId.current = nextId;
|
|
437
|
+
lastSuggestedKeyRef.current = nextKey;
|
|
438
|
+
},
|
|
416
439
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
417
440
|
value: "",
|
|
418
|
-
children: "
|
|
441
|
+
children: "选择规格…"
|
|
419
442
|
}), presets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
420
443
|
value: preset.id,
|
|
421
444
|
children: preset.displayName
|
|
422
445
|
}, preset.id))]
|
|
423
446
|
})]
|
|
424
447
|
}),
|
|
448
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
449
|
+
className: "ad-field",
|
|
450
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "渠道 id" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
451
|
+
className: "ad-input",
|
|
452
|
+
value: newId,
|
|
453
|
+
placeholder: newPreset === "" ? "先选上面的规格" : suggestId(newPreset, rows),
|
|
454
|
+
onChange: (event) => setNewId(event.target.value)
|
|
455
|
+
})]
|
|
456
|
+
}),
|
|
457
|
+
newIdProblem !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
458
|
+
className: "ad-fieldError",
|
|
459
|
+
children: newIdProblem
|
|
460
|
+
}),
|
|
425
461
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
426
462
|
className: "ad-field",
|
|
427
463
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "凭据引用名" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
428
464
|
className: "ad-input",
|
|
429
465
|
value: newKeyRef,
|
|
430
|
-
placeholder: "例如 OPENCODE_GO_API_KEY",
|
|
466
|
+
placeholder: newPreset === "" ? "例如 OPENCODE_GO_API_KEY" : suggestKeyRef(newPreset),
|
|
431
467
|
onChange: (event) => setNewKeyRef(event.target.value)
|
|
432
468
|
})]
|
|
433
469
|
}),
|
|
434
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
470
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
435
471
|
className: "ad-hint",
|
|
436
|
-
children:
|
|
472
|
+
children: [
|
|
473
|
+
"这里填的是",
|
|
474
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "引用名" }),
|
|
475
|
+
"(环境变量名),不是密钥本身。密钥请通过凭据中心设置。"
|
|
476
|
+
]
|
|
437
477
|
}),
|
|
438
478
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
439
479
|
className: "ad-rowActions",
|
|
440
480
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
441
481
|
type: "button",
|
|
442
482
|
className: "ad-btn",
|
|
443
|
-
disabled: newId.trim() === "" || newIdProblem !== null || newKeyRef.trim() === "",
|
|
483
|
+
disabled: newPreset === "" || newId.trim() === "" || newIdProblem !== null || newKeyRef.trim() === "",
|
|
444
484
|
onClick: addChannel,
|
|
445
485
|
children: "添加"
|
|
446
486
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -454,7 +494,11 @@ window.__ModuleLoader__.load({
|
|
|
454
494
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
455
495
|
type: "button",
|
|
456
496
|
className: "ad-btn",
|
|
457
|
-
onClick: () =>
|
|
497
|
+
onClick: () => {
|
|
498
|
+
setAdding(true);
|
|
499
|
+
lastSuggestedId.current = "";
|
|
500
|
+
lastSuggestedKeyRef.current = "";
|
|
501
|
+
},
|
|
458
502
|
children: "添加渠道"
|
|
459
503
|
}),
|
|
460
504
|
current === "" && rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
package/lib/config.d.ts
CHANGED
|
@@ -42,7 +42,6 @@ export interface ChannelConfigInput {
|
|
|
42
42
|
baseURL?: string;
|
|
43
43
|
model?: string;
|
|
44
44
|
apiKeyEnv?: string;
|
|
45
|
-
enabled?: boolean;
|
|
46
45
|
contextWindow?: number;
|
|
47
46
|
maxTokens?: number;
|
|
48
47
|
input?: Array<'text' | 'image'>;
|
|
@@ -63,7 +62,6 @@ export interface ChannelConfigValue {
|
|
|
63
62
|
readonly baseURL?: string;
|
|
64
63
|
readonly model?: string;
|
|
65
64
|
readonly apiKeyEnv?: string;
|
|
66
|
-
readonly enabled?: boolean;
|
|
67
65
|
readonly contextWindow?: number;
|
|
68
66
|
readonly maxTokens?: number;
|
|
69
67
|
readonly input?: ReadonlyArray<'text' | 'image'>;
|
package/lib/http.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { ResolvedChannel } from './channels.js';
|
|
|
21
21
|
export declare const ROUTE_PREFIX = "/any-deepseek";
|
|
22
22
|
/** What the routes need from the plugin. */
|
|
23
23
|
export interface RouteDeps {
|
|
24
|
-
/**
|
|
24
|
+
/** Every resolved channel, in configuration order. */
|
|
25
25
|
channels: () => readonly ResolvedChannel[];
|
|
26
26
|
/** Resolve a credential reference to its value. */
|
|
27
27
|
resolveKey: (ref: string) => Promise<string | undefined>;
|
|
@@ -40,6 +40,16 @@ export interface RouteDeps {
|
|
|
40
40
|
configured: boolean;
|
|
41
41
|
source?: string;
|
|
42
42
|
}>;
|
|
43
|
+
/**
|
|
44
|
+
* The ledger directory the plugin actually WRITES to.
|
|
45
|
+
*
|
|
46
|
+
* 🔴 This must be passed in, not recomputed. The reader used to call
|
|
47
|
+
* `ledgerDirectory(undefined)` — which silently dropped the configured `usage.path` — while the
|
|
48
|
+
* writer used `ledgerDirectory(usage.path)`. Whenever a user set a custom ledger directory the
|
|
49
|
+
* two pointed at different places, so the panel read an empty book forever and reported
|
|
50
|
+
* "no usage yet" as though no request had ever run. Read and write must resolve one directory.
|
|
51
|
+
*/
|
|
52
|
+
ledgerDir: () => string;
|
|
43
53
|
}
|
|
44
54
|
/** Structural view of the harness web server (only `register` is used). */
|
|
45
55
|
export interface WebServerLike {
|
package/lib/index.js
CHANGED
|
@@ -56,7 +56,6 @@ const ChannelConfigSchema = z.object({
|
|
|
56
56
|
baseURL: z.string().description("端点根地址"),
|
|
57
57
|
model: z.string().description("上游模型 id(各家拼写不同,必填)"),
|
|
58
58
|
apiKeyEnv: z.string().description("凭据引用名(环境变量名);真实 key 存在凭据库里,不落配置"),
|
|
59
|
-
enabled: z.boolean().default(true).description("是否启用"),
|
|
60
59
|
contextWindow: z.number().description("上下文窗口(tokens)"),
|
|
61
60
|
maxTokens: z.number().description("单次输出上限(tokens)"),
|
|
62
61
|
input: z.array(z.union([z.const("text"), z.const("image")])).description("输入模态"),
|
|
@@ -434,19 +433,19 @@ function resolveChannels(entries) {
|
|
|
434
433
|
};
|
|
435
434
|
}
|
|
436
435
|
/**
|
|
437
|
-
*
|
|
436
|
+
* Every resolved channel, in configuration order — the panel's list and the router's candidate set.
|
|
437
|
+
*
|
|
438
|
+
* 🔴 There is no filtering step here, deliberately. This used to be `enabledChannels()`, which
|
|
439
|
+
* dropped entries with `enabled: false`. That made two things impossible:
|
|
440
|
+
* · the panel could not show a channel it had hidden, so you could not switch it back on;
|
|
441
|
+
* · "which relay serves requests" had two answers (`enabled` AND `current`) that could disagree.
|
|
442
|
+
* The design has one axis — `current` — so the only list is "all channels", and selection is a
|
|
443
|
+
* separate fact.
|
|
438
444
|
* @param resolution - output of {@link resolveChannels}.
|
|
439
|
-
* @
|
|
440
|
-
* @returns enabled channels, configuration order preserved.
|
|
445
|
+
* @returns all resolved channels (the router picks one of them via `current`).
|
|
441
446
|
*/
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
const map = isPlainObject(entries) ? entries : {};
|
|
445
|
-
for (const [id, channel] of resolution.channels) {
|
|
446
|
-
const raw = map[id];
|
|
447
|
-
if (!(isPlainObject(raw) && raw.enabled === false)) out.push(channel);
|
|
448
|
-
}
|
|
449
|
-
return out;
|
|
447
|
+
function allChannels(resolution) {
|
|
448
|
+
return [...resolution.channels.values()];
|
|
450
449
|
}
|
|
451
450
|
//#endregion
|
|
452
451
|
//#region src/route.ts
|
|
@@ -456,7 +455,11 @@ function enabledChannels(resolution, entries) {
|
|
|
456
455
|
* The order of the checks matters for the message the user sees: an empty configuration is a
|
|
457
456
|
* setup problem, while an unset `current` is a choice not yet made. Reporting them separately
|
|
458
457
|
* keeps the panel's guidance accurate.
|
|
459
|
-
*
|
|
458
|
+
*
|
|
459
|
+
* 🔴 There is no "disabled" branch any more. `current` is the single axis, so a selection either
|
|
460
|
+
* names a channel that exists or names one that does not — the old `CHANNEL_DISABLED` code
|
|
461
|
+
* described a state the configuration can no longer express.
|
|
462
|
+
* @param channels - every resolved channel, in configuration order.
|
|
460
463
|
* @param current - the configured channel id.
|
|
461
464
|
* @returns the selected channel, or the reason there is none.
|
|
462
465
|
*/
|
|
@@ -465,7 +468,7 @@ function selectChannel(channels, current) {
|
|
|
465
468
|
ok: false,
|
|
466
469
|
failure: {
|
|
467
470
|
code: "NO_CHANNELS",
|
|
468
|
-
detail: "no channel is configured
|
|
471
|
+
detail: "no channel is configured; add one in Settings → Any DeepSeek"
|
|
469
472
|
}
|
|
470
473
|
};
|
|
471
474
|
const wanted = typeof current === "string" ? current.trim() : "";
|
|
@@ -473,7 +476,7 @@ function selectChannel(channels, current) {
|
|
|
473
476
|
ok: false,
|
|
474
477
|
failure: {
|
|
475
478
|
code: "NO_CHANNEL_SELECTED",
|
|
476
|
-
detail: "no channel is selected; pick one in Settings →
|
|
479
|
+
detail: "no channel is selected; pick one in Settings → Any DeepSeek"
|
|
477
480
|
}
|
|
478
481
|
};
|
|
479
482
|
const found = channels.find((channel) => channel.id === wanted);
|
|
@@ -484,8 +487,8 @@ function selectChannel(channels, current) {
|
|
|
484
487
|
return {
|
|
485
488
|
ok: false,
|
|
486
489
|
failure: {
|
|
487
|
-
code: "
|
|
488
|
-
detail: `selected channel "${wanted}"
|
|
490
|
+
code: "CHANNEL_UNKNOWN",
|
|
491
|
+
detail: `selected channel "${wanted}" does not exist; available: ${channels.map((channel) => channel.id).join(", ")}`
|
|
489
492
|
}
|
|
490
493
|
};
|
|
491
494
|
}
|
|
@@ -926,6 +929,11 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
926
929
|
}
|
|
927
930
|
async resolveModel(provider, model, _signal) {
|
|
928
931
|
const channel = this.selected();
|
|
932
|
+
const efforts = Object.keys(channel?.reasoningEfforts ?? {});
|
|
933
|
+
const reasoning = efforts.length === 0 ? {} : { reasoning: { efforts: efforts.map((id) => ({
|
|
934
|
+
id: ReasoningEffortId(id),
|
|
935
|
+
name: id
|
|
936
|
+
})) } };
|
|
929
937
|
return {
|
|
930
938
|
provider,
|
|
931
939
|
id: model,
|
|
@@ -933,10 +941,7 @@ var AnyDeepSeekAdapter = class extends LlmAdapter {
|
|
|
933
941
|
description: channel === void 0 ? void 0 : `via ${channel.label}`,
|
|
934
942
|
context: { contextWindow: channel?.contextWindow ?? 1e6 },
|
|
935
943
|
defaultMaxTokens: channel?.maxTokens,
|
|
936
|
-
...
|
|
937
|
-
id: ReasoningEffortId(id),
|
|
938
|
-
name: id
|
|
939
|
-
})) } }
|
|
944
|
+
...reasoning
|
|
940
945
|
};
|
|
941
946
|
}
|
|
942
947
|
/**
|
|
@@ -1464,7 +1469,7 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
1464
1469
|
const from = queryDate(url.searchParams.get("from"));
|
|
1465
1470
|
const to = queryDate(url.searchParams.get("to"));
|
|
1466
1471
|
const group = url.searchParams.get("group") ?? "channel";
|
|
1467
|
-
const entries = await readLedgerRange(
|
|
1472
|
+
const entries = await readLedgerRange(deps.ledgerDir(), from, to);
|
|
1468
1473
|
const rows = group === "day" ? summarize(entries, (entry) => dayKey(new Date(entry.at))) : summarize(entries, (entry) => entry.channel);
|
|
1469
1474
|
json(res, 200, {
|
|
1470
1475
|
range: {
|
|
@@ -1602,14 +1607,16 @@ function apply(ctx, config) {
|
|
|
1602
1607
|
}
|
|
1603
1608
|
};
|
|
1604
1609
|
/**
|
|
1605
|
-
*
|
|
1610
|
+
* Every configured channel, in configuration order.
|
|
1606
1611
|
*
|
|
1607
1612
|
* Read fresh on every call rather than captured at apply time: the settings document can be
|
|
1608
1613
|
* edited while the plugin runs, and `fiber.config` is updated in place by the loader.
|
|
1614
|
+
*
|
|
1615
|
+
* No filtering: the list is what the panel shows AND what the router chooses from. Which one
|
|
1616
|
+
* serves requests is answered solely by `current` — there is no second on/off axis.
|
|
1609
1617
|
*/
|
|
1610
1618
|
const channels = () => {
|
|
1611
|
-
|
|
1612
|
-
return enabledChannels(resolveChannels(map), map);
|
|
1619
|
+
return allChannels(resolveChannels(unwrapChannels((ctx.fiber?.config ?? config).channels)));
|
|
1613
1620
|
};
|
|
1614
1621
|
const current = () => {
|
|
1615
1622
|
const value = unwrap((ctx.fiber?.config ?? config).current);
|
|
@@ -1655,6 +1662,9 @@ function apply(ctx, config) {
|
|
|
1655
1662
|
} catch {
|
|
1656
1663
|
return { configured: false };
|
|
1657
1664
|
}
|
|
1665
|
+
},
|
|
1666
|
+
ledgerDir: () => {
|
|
1667
|
+
return ledgerDirectory((ctx.fiber?.config ?? config).usage?.path);
|
|
1658
1668
|
}
|
|
1659
1669
|
});
|
|
1660
1670
|
}
|
package/lib/route.d.ts
CHANGED
|
@@ -21,10 +21,7 @@ export type RouteFailure = {
|
|
|
21
21
|
code: 'NO_CHANNEL_SELECTED';
|
|
22
22
|
detail: string;
|
|
23
23
|
} | {
|
|
24
|
-
code: '
|
|
25
|
-
detail: string;
|
|
26
|
-
} | {
|
|
27
|
-
code: 'CHANNEL_DISABLED';
|
|
24
|
+
code: 'CHANNEL_UNKNOWN';
|
|
28
25
|
detail: string;
|
|
29
26
|
} | {
|
|
30
27
|
code: 'NO_CHANNELS';
|
|
@@ -44,7 +41,11 @@ export type RouteResult = {
|
|
|
44
41
|
* The order of the checks matters for the message the user sees: an empty configuration is a
|
|
45
42
|
* setup problem, while an unset `current` is a choice not yet made. Reporting them separately
|
|
46
43
|
* keeps the panel's guidance accurate.
|
|
47
|
-
*
|
|
44
|
+
*
|
|
45
|
+
* 🔴 There is no "disabled" branch any more. `current` is the single axis, so a selection either
|
|
46
|
+
* names a channel that exists or names one that does not — the old `CHANNEL_DISABLED` code
|
|
47
|
+
* described a state the configuration can no longer express.
|
|
48
|
+
* @param channels - every resolved channel, in configuration order.
|
|
48
49
|
* @param current - the configured channel id.
|
|
49
50
|
* @returns the selected channel, or the reason there is none.
|
|
50
51
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-deepseek",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "One DeepSeek V4.1 Flash provider for DeepSeek Harness: paste a key per channel, switch channels manually, token usage booked per real upstream.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|