dsh-any-deepseek 0.0.4 → 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 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
- /** Whether the panel offers this channel. Absent means enabled. */
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
- * Channels that should appear and serve requests: resolved AND not explicitly disabled.
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
- * @param entries - the same configuration map, read again for the `enabled` flags.
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 enabledChannels(resolution: ChannelResolution, entries: unknown): ResolvedChannel[];
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. Uses the harness's alias tokens where they exist, plain values otherwise, so\n the page follows the surrounding theme instead of hardcoding a palette. */\n\n.ad-page {\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 4px 2px 12px;\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(--dsw-alias-color-danger, #c0392b);\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(--dsw-alias-color-border, rgba(0, 0, 0, 0.12));\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.ad-row-current {\n border-color: var(--dsw-alias-color-accent, #3b82f6);\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(--dsw-alias-color-border, rgba(0, 0, 0, 0.16));\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.ad-switch {\n position: relative;\n display: inline-flex;\n align-items: center;\n cursor: pointer;\n}\n\n.ad-switch input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n}\n\n.ad-switchTrack {\n width: 30px;\n height: 16px;\n border-radius: 999px;\n background: var(--dsw-alias-color-border, rgba(0, 0, 0, 0.2));\n transition: background 120ms ease;\n}\n\n.ad-switch input:checked + .ad-switchTrack {\n background: var(--dsw-alias-color-accent, #3b82f6);\n}\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(--dsw-alias-color-border, rgba(0, 0, 0, 0.16));\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.ad-tab-on {\n border-color: var(--dsw-alias-color-accent, #3b82f6);\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(--dsw-alias-color-border, rgba(0, 0, 0, 0.08));\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(--dsw-alias-color-border, rgba(0, 0, 0, 0.16));\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(--dsw-alias-color-border, rgba(0, 0, 0, 0.16));\n background: transparent;\n color: inherit;\n font-family: inherit;\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(--dsw-alias-color-danger, #c0392b);\n color: var(--dsw-alias-color-danger, #c0392b);\n}\n";
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
- /** One labelled switch. */
130
- function Toggle(props) {
131
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
132
- className: "ad-switch",
133
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
134
- type: "checkbox",
135
- checked: props.checked,
136
- disabled: props.disabled,
137
- onChange: (event) => props.onChange(event.target.checked)
138
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ad-switchTrack" })]
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 (channels[id] !== void 0) return "这个 id 已经有了";
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
- ...newPreset === "" ? {} : { preset: newPreset },
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
- model.name || "DeepSeek V4.1 Flash",
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)("div", {
308
- className: "ad-rowMain",
309
- children: [
310
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
311
- className: "ad-rowTitleLine",
312
- children: [
313
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyDot, {
314
- configured: row.keyConfigured,
315
- known: row.keySource !== void 0 || row.keyConfigured
316
- }),
317
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
318
- className: "ad-rowTitle",
319
- children: row.label
320
- }),
321
- isCurrent && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
322
- className: "ad-badge",
323
- children: "当前"
324
- }),
325
- row.warnings.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
326
- className: "ad-warn",
327
- title: row.warnings.join("\n"),
328
- children: "⚠"
329
- })
330
- ]
331
- }),
332
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
333
- className: "ad-rowMeta",
334
- children: [
335
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
336
- className: "ad-mono",
337
- children: row.model
338
- }),
339
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
340
- className: "ad-sep",
341
- children: "·"
342
- }),
343
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: row.api }),
344
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
345
- className: "ad-sep",
346
- children: "·"
347
- }),
348
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
349
- className: "ad-mono",
350
- children: row.baseURL
351
- })
352
- ]
353
- }),
354
- probeResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
355
- className: `ad-probe ad-probe-${probeResult.ok ? "ok" : "bad"}`,
356
- children: probeResult.ok ? `通(${probeResult.ms}ms)` : `不通:${probeResult.code ?? "ERROR"}${probeResult.message === void 0 ? "" : ` — ${probeResult.message}`}`
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
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
363
- type: "button",
364
- className: "ad-btn",
365
- disabled: isCurrent || !enabled,
366
- onClick: () => select(row.id),
367
- children: isCurrent ? "使用中" : "设为当前"
368
- }),
369
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
370
- type: "button",
371
- className: "ad-btn ad-btn-quiet",
372
- disabled: probing === row.id || !enabled,
373
- onClick: () => {
374
- runProbe(row.id);
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) => setNewPreset(event.target.value),
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.jsx)("p", {
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: () => setAdding(true),
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
- /** Enabled, resolved channels. */
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
- * Channels that should appear and serve requests: resolved AND not explicitly disabled.
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
- * @param entries - the same configuration map, read again for the `enabled` flags.
440
- * @returns enabled channels, configuration order preserved.
445
+ * @returns all resolved channels (the router picks one of them via `current`).
441
446
  */
442
- function enabledChannels(resolution, entries) {
443
- const out = [];
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
- * @param channels - enabled, resolved channels in configuration order.
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 and enabled; add one in Settings → any-deepseek"
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 → any-deepseek"
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: "CHANNEL_DISABLED",
488
- detail: `selected channel "${wanted}" is not among the enabled channels; enabled: ${channels.map((channel) => channel.id).join(", ")}`
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
  }
@@ -1466,7 +1469,7 @@ function registerStatsRoutes(ctx, webServer, deps) {
1466
1469
  const from = queryDate(url.searchParams.get("from"));
1467
1470
  const to = queryDate(url.searchParams.get("to"));
1468
1471
  const group = url.searchParams.get("group") ?? "channel";
1469
- const entries = await readLedgerRange(ledgerDirectory(void 0), from, to);
1472
+ const entries = await readLedgerRange(deps.ledgerDir(), from, to);
1470
1473
  const rows = group === "day" ? summarize(entries, (entry) => dayKey(new Date(entry.at))) : summarize(entries, (entry) => entry.channel);
1471
1474
  json(res, 200, {
1472
1475
  range: {
@@ -1604,14 +1607,16 @@ function apply(ctx, config) {
1604
1607
  }
1605
1608
  };
1606
1609
  /**
1607
- * The current channel set.
1610
+ * Every configured channel, in configuration order.
1608
1611
  *
1609
1612
  * Read fresh on every call rather than captured at apply time: the settings document can be
1610
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.
1611
1617
  */
1612
1618
  const channels = () => {
1613
- const map = unwrapChannels((ctx.fiber?.config ?? config).channels);
1614
- return enabledChannels(resolveChannels(map), map);
1619
+ return allChannels(resolveChannels(unwrapChannels((ctx.fiber?.config ?? config).channels)));
1615
1620
  };
1616
1621
  const current = () => {
1617
1622
  const value = unwrap((ctx.fiber?.config ?? config).current);
@@ -1657,6 +1662,9 @@ function apply(ctx, config) {
1657
1662
  } catch {
1658
1663
  return { configured: false };
1659
1664
  }
1665
+ },
1666
+ ledgerDir: () => {
1667
+ return ledgerDirectory((ctx.fiber?.config ?? config).usage?.path);
1660
1668
  }
1661
1669
  });
1662
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: 'UNKNOWN_CHANNEL';
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
- * @param channels - enabled, resolved channels in configuration order.
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.4",
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": {