Repository navigation
Releases
v1.2.12 — the panel wears the house baseline
feat: wear the house design baseline instead of a private palette
v1.2.12 lines the ranking panel up with dsh-design-baseline, the single
stylesheet every one of my plugins now shares:
- inline the baseline block (BASELINE_CSS, THEME, wash, injectBaseline)
into factory(require). It cannot be imported: the DSH client module
system's require() never resolves file paths, only platform seeds,
materialised module ids and registered package factories. - install it once via injectBaseline(document, PANEL_ID) before the
panel's own rules; injectBaseline is idempotent (style[data-dsb]), so a
hot reload cannot stack copies. - the three score axes now read --dsb-accent-{stability,speed,intelligence}
with literal fallbacks instead of three ad-hoc colour definitions. - put dsb-root on both panel roots so the neutral mapping resolves on the
same element the host's dark overrides target (not :root). - publish gate + both README install commands -> v1.2.12; smoke adds the
"wears the house baseline" check (38 checks, all pass).
v1.2.11 — free models work before you configure anything
feat: turn on the keyless providers without a first-run setup
A fresh install had an empty api_keys table, and the router only routes to a
platform that has a row there (services/router.js, usableKeyCountsByPlatform).
So a brand-new install answered one model out of 296 and every other row said
no_key — despite seven providers that need no key at all. The user had to
discover the dashboard, add a keyless provider by hand, and only then see a
usable list.
The plugin now asks the router's own registry which platforms are keyless
(getAllProviders() → keyless === true, seven today) and, for any platform the
database has never seen, hands the server a declarative config at spawn time:
FREEAPI_CONFIG_JSON={"keys":[{"platform":"aihorde"},…]}
The server stores its no-key sentinel for each, exactly as the dashboard does
when a person adds one. Measured on a copy of a real database with api_keys
emptied: 1 available model before, 53 after. On this machine, which already had
seven sentinel rows, the plan comes back empty and nothing changes.
Deliberate choices:
- No hardcoded platform list. The registry is the source, so an upstream
provider being added or dropped is picked up without editing this plugin. - Only missing rows are declared, so a platform the user disabled stays
disabled — the seed never re-enables something a person turned off. settings.enabledModelsis untouched. An empty list keeps meaning "follow the
probes"; the seed does not quietly convert it into a 51-entry whitelist.- An operator who set
FREEAPI_CONFIG_JSONthemselves keeps it: the seed only
fills the variable when it is absent. - The database read is skipped on a lean poll, and the panel gets the state as
keyless: { already, pending }, so the settings card can say "7 keyless
providers are on" instead of leaving the feature invisible.
smoke.mjs grows one behavioural check (registry membership, a fresh database
being all-missing, the payload shape, the empty case, the operator override, and
the supervisor asking for the plan), and asserts the panel reports the state.
v1.2.10 — the bars are painted again
fix: paint the ranking bars with colours the host actually defines
The speed and intelligence bars have been invisible since 1.2.2, when the
hardcoded hex values were replaced with var() references. The replacements
pointed at --color-blue-500, --color-green-500 and --color-violet-500, which
the DSH host never defines. A dangling var() is not an error: it simply paints
nothing, so the bars kept their grey track and lost their fill.
The reason the mistake looked safe is dsh-context, which does reference those
names. It can, because it defines that palette itself in a <style> it appends
to document.head. Copying the references without copying the definitions left
every one of them dangling. The host's real palettes are --dsw-static-{hue}
for raw colour and --dsw-alias-* for semantics.
Six references are rewired to a primitive plus the dashboard's literal as a
fallback, so the panel is correct even if a future host drops the token:
state-success-primary, var(--color-green-500) -> ..., #22c55e (6 places)
label-tertiary, var(--color-neutral-500)-> ..., #7f8287
state-warn-primary, var(--color-amber-500) -> ..., #f59e0b
button-primary-fill, var(--color-blue-500) -> ..., #3b82f6
state-error-primary, var(--color-red-500) -> ..., #f43f5e
brand-primary, var(--color-blue-500) -> ..., #3b82f6
The two axes had no fallback at all and now do. The speed bar takes
--dsw-static-blue-500 with #3b82f6 behind it. The intelligence bar is a plain
#a855f7 rather than a token, because the host ships violet-400 (#a78bfa) and
violet-600 (#7c3aed) but no violet-500 — close is not the same as the colour
the :3001 table uses, and matching that table exactly is the whole point.
Two alias names were wrong for the same reason and are corrected:
--dsw-alias-border-secondary and --dsw-alias-border-1 do not exist, while
--dsw-alias-border-l2 does.
Verified by rendering the shipped stylesheet in headless Edge with the host's
real token values installed in a selector, then reading getComputedStyle.
Putting the declarations loose in a <style> is invalid CSS and drops them, so
an earlier version of this check measured only fallbacks and passed while the
bug was still there. With the tokens correctly scoped, all three bars resolve
and the speed and intelligence values are byte-identical to the dashboard's
#3b82f6 and #a855f7.
v1.2.9 — the desktop panel can reach its own API again
fix: let the desktop panel reach its own API
1.2.7 tightened sameOrigin to refuse any request without an Origin header, on
the reasoning that a browser POST to the panel always carries one. That is true
for a browser. It is false for the desktop app, and the difference is invisible
from the server side, so the release shipped a panel that could not talk to its
own backend: every request came back 403 origin rejected and the status card
sat on "not running / external".
The desktop app does not load the web UI over http. It serves it from a custom
dsh-app:// scheme and proxies the panel's requests through its own forwarder,
which deletes origin and host from the forwarded headers and re-adds only
the session cookie. So the plugin receives a POST with no Origin at all — and
even a build that keeps the header sends dsh-app://app, which no http://
comparison can ever match. Both shapes were refused.
The guard now asks where the request came from rather than only what it claims:
- no Origin, or
null-> allowed only on a loopback Host dsh-app://...-> allowed only on a loopback Host- any other Origin -> must still equal the Host
Nothing is loosened for the case the check exists for. A web page cannot reach a
loopback port without an Origin, Origin is set by the user agent and cannot be
forged, and the desktop scheme can only be served by the harness itself. The
remote-host branches are refused exactly as before.
The regression test asserts behaviour instead of grepping for a source string:
the old check was written as assert.match(index, /typeof origin !== 'string'/),
so shipping the bug kept the test green. It now calls sameOrigin with the real
header shapes — no Origin, null, dsh-app://app, dsh-app://shell, a browser
same-origin POST, plus six refusals — which would have failed on 1.2.7 and does
fail if the loopback gate is dropped.
v1.2.7 — crash / lie / leak / hang fixes + publish gate
dsh-freellmapi v1.2.7
One commit on top of v1.2.6 that closes the crash / lie / leak / hang findings and adds a publish gate. Version 1.2.6 had already been published, so this ships as 1.2.7.
Fixes
- Hang: the SSE reader now ends on
[DONE]instead of waiting for the 300 s request timeout; it also flushes theTextDecoderand handles a trailing unterminateddata:line, so a stream that ends without a finish reason is reported correctly instead of as a transport error. - Leak (process): when the bundled server's startup throws (spawn error, early exit, port taken, grace timeout), the spawned child is now killed, so a retry cannot leave an orphan holding the port.
- Crash (panel): rank scores are checked to be finite numbers before
toFixed(3); the main panel slot is registered throughctx.effectso hot reload disposes the old one. - Lie (panel): a failed save no longer clears the draft and shows "已保存 / saved"; router state and its 2.5 s polling are now a single module-level instance shared by the panel and the bundle view.
- Leak / robustness (panel API): requests with no
Origin/Hostare rejected, the route handler has a.catchso a destroyed response cannot become an unhandled rejection, and the probe loop stops once the scope is disposed. - SSRF:
baseURLpointing at link-local / metadata addresses (169.254.*,fe80::*) is refused; loopback is unchanged.
Publish gate
engines.nodeis now>=22.13.0(required bynode:sqlite).npm testandprepackboth runsmoke.mjs(36 checks), so the tarball cannot be built from a broken tree.- The install URLs in README.md / README.zh.md now point at
v1.2.7/dsh-freellmapi-1.2.7.tgzand are asserted againstpackage.json.version.
The attached dsh-freellmapi-1.2.7.tgz is prebuilt (identical file set to the published npm package, including the bundled vendor/node_modules runtime deps):
dsh plugin --profile <profile> add https://github.com/stallenlu/dsh-freellmapi/releases/download/v1.2.7/dsh-freellmapi-1.2.7.tgzv1.2.6 — nothing left running behind you
Three leaks, one release.
- The router is torn down even when autoStart is off. The supervisor cleanup used to be gated behind the autoStart branch, so a server started from the panel, a tool or ensureRouter kept running — and kept holding its port — after the fiber went away.
- A failed spawn is an error the panel can show, not a crashed host. The child never had an 'error' listener, and a failed spawn emits 'error' without emitting 'exit'; an unhandled 'error' event throws in the host. A missing or unexecutable server binary could take DSH down with it. It now fails fast instead of waiting out the 25s grace period.
- The request budget is released on every exit. The adapter only cleared its timeout and abort listener on the paths that reached the fetch, so a synchronous validation throw leaked both for the whole requestTimeoutMs.
smoke.mjs: 33 → 35 checks, including a behavioural one that a package with no bundled runtime rejects within seconds, naming the missing piece.
v1.2.5 — a stuck host can no longer freeze the panel
Four fixes on paths a normal session takes. The catalogue read and the panel's request now have timeouts, so a router that accepts the connection but never answers turns into a readable error instead of a panel where every button is dead until a reload (start keeps a longer ceiling: it waits out the supervisor's 25s grace period). Requests queue instead of being dropped, so a click that lands during a probe poll is actually sent. Ranking reads are serialised — the vendored database handle is a module-level singleton, and two overlapping reads used to swap the connection out from under each other, swallowing the failure and making the ranking vanish for a turn. Probe polls ask for a lean payload and the client merges back the scores it already has: 49.7 KB instead of 134.9 KB per poll, and an unchanged payload no longer rebuilds the tree. Also fixes the row switch, which reported aria-pressed on a role=switch (and painted its knob from that attribute), and removes a dead sizeLabel that was computed incorrectly and never read.
v1.2.4 — native ranking tab, themed panel, dark-friendly selects
Re-draws the router's own model table inside the DSH panel, and brings the panel onto the DSH design tokens.
Ranking tab
- Reads the router database read-only and reuses the vendored
getModelGroups()/getRoutingScores(), so the stability / speed / intelligence axes and the 0..1 score are the same numbers the:3001console shows. Nothing is recomputed. - Sortable columns, search, and vision / tools / context-size filters.
- Per-row enable switches, plus tick-untick bulk actions over the currently shown set.
- A missing or locked database degrades to "scores unavailable" - the model list keeps working.
Colour and surfaces
- Accents come from DSW alias tokens and
color-mix()instead of fixed hex values, so the panel follows light and dark themes. - Flat list surfaces no longer stack
bg-layer-3/bg-layer-4on top of the card's raised surface. Cards arebg-layer-1with a hairline border; rows are transparent with an inset border.
Native selects
- A native
<select>popup is drawn fromcolor-scheme, which the host never set - so on a dark theme the list opened white with a blue highlight. The panel now measures the active theme and appliescolor-schemeto its root, with aMutationObserverso a theme flip is picked up without a reload.
Console link
- "Open the full console" from the ranking tab and the status card, for provider keys, usage and routing policy.
Install
The tarball below is self-contained: vendor/node_modules (the router's runtime closure) ships inside it, because the repository does not commit node_modules. Installing straight from the git tree will produce a router that cannot start.
smoke.mjs: 26 offline checks pass.
v1.0.10 — visible sort, tick usable, and a plugin icon
v1.0.10 — visible sort, tick usable, and a plugin icon
Four things the last round of screenshots exposed, all fixed.
| Report | Fix |
|---|---|
| 排序的功能没有看到 | The sort <select> used appearance:none with no border or arrow, so it read as static text. It now draws its own chevron and reserves room for it, at the same 32px/16px geometry as the neighbouring buttons. |
| 排序按钮的配置不协调 / 可用应排前 | Name, context and availability sorts all lead with the rows the picker actually offers, then apply the chosen key. |
| 选择只有全选、全不选 / 勾选可用全部选上了 / 要先全不选才生效 | New 勾选可用 (N) button. "Usable" means a probe answered a real request — the same fact as the 实测可用 tag, not the router's flag. It replaces the listed rows rather than unioning with them, so it works directly after 全选. All three bulk buttons act only on the rows the search box left visible. |
| 给这个插件设计一个简约专用图标 | Package-root icon.svg (one entry node fanning out to three models — the router), declared as icon, exported as ./icon, and included in files so npm pack cannot drop it. The sidebar PanelIcon uses the same motif, follows currentColor, and honours the host's size. |
Verification
node smoke.mjs: all 26 offline checks pass, including new guards for the icon's packaging (top-level field,./iconexport,filesentry, 256 KiB cap, viewBox, currentColor) and for every sort/bulk-tick semantic above.node smoke.mjs --live: 287 models / 54 available,autoansweredPONG.icon.svgrendered in real Chrome: 128×128, 3,744 lit pixels — a valid filename is not the same as pixels, so this was measured.- Mini-React run against the real
ModelList, four scenarios: after 全选 → 勾选可用 leaves exactly the two probed-ok rows; after 全不选 → same; a manually ticked unprobed row is dropped; with agemmafilter the usable button is disabled and unfiltering leaves the original selection intact. - Unpacked tarball: version 1.0.10, all 10
lib/modules present (lib/probe.jsincluded — the v1.0.4fileswhitelist lesson),icon.svgpresent, both smoke suites pass from the unpacked directory.
Getting it
The client-side changes (sort, tick usable, sidebar icon) land after a Ctrl+F5 refresh. The package icon is host metadata, so it needs one DSH restart to replace the generic puzzle piece on the plugin page.
v1.0.9 — sort the model list, tick the visible rows
Superseded by v1.0.10 — visible sort, tick usable, and a real plugin icon.