dsh-workbuddy-xdpool 1.7.2 → 1.7.4
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/CHANGELOG.md +82 -0
- package/lib/bin.js +410 -72
- package/lib/client.js +89 -15
- package/lib/index.d.ts +1033 -926
- package/lib/index.js +482 -86
- package/package.json +1 -1
package/lib/bin.js
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
import { createDecipheriv, createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
3
|
import { copyFile, mkdir, readFile, readdir, rename, writeFile, writeFile as writeFile$1 } from "node:fs/promises";
|
|
3
4
|
import { homedir, platform } from "node:os";
|
|
4
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
6
|
import { execFile, execFileSync } from "node:child_process";
|
|
6
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import z from "@deepseek-ai/schemastery";
|
|
8
8
|
import "@earendil-works/pi-ai";
|
|
9
9
|
import "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
10
10
|
import "@deepseek-ai/dsh-llm";
|
|
11
11
|
import "@deepseek-ai/dsh-llm-pi-ai";
|
|
12
12
|
//#region src/upstream.ts
|
|
13
|
+
/**
|
|
14
|
+
* Backoff between catalog-fetch attempts.
|
|
15
|
+
*
|
|
16
|
+
* Three retries after the first try, so a startup network hiccup does not cost
|
|
17
|
+
* the user their model list for the whole session. The window covers the case
|
|
18
|
+
* that actually bit: several independent components reported `fetch failed`
|
|
19
|
+
* within the same second while the machine was still bringing its network up,
|
|
20
|
+
* and the very next request succeeded 0.7s later.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately short and bounded. This runs during plugin startup, so a long
|
|
23
|
+
* ladder would delay the provider appearing at all; the total added latency
|
|
24
|
+
* here is ~12s in the worst case, and only when the network is genuinely down.
|
|
25
|
+
*/
|
|
26
|
+
const CATALOG_RETRY_BACKOFF_MS = [
|
|
27
|
+
1e3,
|
|
28
|
+
3e3,
|
|
29
|
+
8e3
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Sleep helper for the retry ladder.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately NOT unref'd. An unref'd timer does not keep the event loop
|
|
35
|
+
* alive, so awaiting one can hang forever when nothing else is pending — the
|
|
36
|
+
* retry would simply never resume. That is exactly what the first version of
|
|
37
|
+
* this did, and it wedged the very first end-to-end run: `fetchModels` never
|
|
38
|
+
* settled. The wait is at most a few seconds and only happens on failure, so
|
|
39
|
+
* holding the loop open is the correct trade.
|
|
40
|
+
*/
|
|
41
|
+
function delay(ms) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
setTimeout(resolve, ms);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
13
46
|
/** CN chat base per dingminhua's on-machine probe (HTTP 200 for chat/models). */
|
|
14
47
|
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
15
48
|
/** Billing/console base — `www.codebuddy.cn` is the billing origin. */
|
|
@@ -31,6 +64,13 @@ const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
|
|
|
31
64
|
const GLOBAL_CONFIG_PATH = "/v3/config";
|
|
32
65
|
const JSON_TIMEOUT_MS = 3e4;
|
|
33
66
|
const ERROR_BODY_LIMIT = 4096;
|
|
67
|
+
/** True for a request the CALLER aborted, which must never be retried. */
|
|
68
|
+
function isAbortError(error) {
|
|
69
|
+
if (typeof error !== "object" || error === null) return false;
|
|
70
|
+
if (error.name === "AbortError") return true;
|
|
71
|
+
const cause = error.cause;
|
|
72
|
+
return typeof cause === "object" && cause !== null && cause.name === "AbortError";
|
|
73
|
+
}
|
|
34
74
|
/** Cap on the reassembled compaction reply, guarding against a runaway stream. */
|
|
35
75
|
const COMPLETION_TEXT_LIMIT = 65536;
|
|
36
76
|
/** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
|
|
@@ -638,9 +678,20 @@ function parseTask(value) {
|
|
|
638
678
|
var WorkBuddyUpstreamClient = class {
|
|
639
679
|
fetchImpl;
|
|
640
680
|
clientVersion;
|
|
681
|
+
/** Backoff ladder for `fetchModels`; empty means a single attempt. */
|
|
682
|
+
catalogRetryBackoffMs;
|
|
683
|
+
/**
|
|
684
|
+
* Optional logger for retry notices.
|
|
685
|
+
*
|
|
686
|
+
* Set by the host so a retry is visible in the log with its attempt count —
|
|
687
|
+
* without it, a retry that eventually succeeds is invisible, and an operator
|
|
688
|
+
* debugging "why was the catalog slow" has nothing to look at.
|
|
689
|
+
*/
|
|
690
|
+
logger;
|
|
641
691
|
constructor(options = {}) {
|
|
642
692
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
643
693
|
this.clientVersion = options.clientVersion ?? "2.0.4";
|
|
694
|
+
this.catalogRetryBackoffMs = options.catalogRetryBackoffMs ?? CATALOG_RETRY_BACKOFF_MS;
|
|
644
695
|
}
|
|
645
696
|
/**
|
|
646
697
|
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
@@ -797,6 +848,30 @@ var WorkBuddyUpstreamClient = class {
|
|
|
797
848
|
* below is common to the two branches.
|
|
798
849
|
*/
|
|
799
850
|
async fetchModels(credential, signal) {
|
|
851
|
+
const attempts = this.catalogRetryBackoffMs.length + 1;
|
|
852
|
+
let lastError;
|
|
853
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) try {
|
|
854
|
+
return await this.fetchModelsOnce(credential, signal);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
lastError = error;
|
|
857
|
+
if (isAbortError(error)) throw error;
|
|
858
|
+
const wait = this.catalogRetryBackoffMs[attempt];
|
|
859
|
+
if (wait === void 0) break;
|
|
860
|
+
this.logger?.warn?.(`dsh-workbuddy-xdpool: model catalog fetch failed (attempt ${attempt + 1}/${attempts}), retrying in ${wait}ms`, error);
|
|
861
|
+
await delay(wait);
|
|
862
|
+
}
|
|
863
|
+
throw lastError;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* One catalog attempt, without retries.
|
|
867
|
+
*
|
|
868
|
+
* Split out so {@link fetchModels} can retry it: the failure this guards
|
|
869
|
+
* against is a startup network hiccup, where several independent components
|
|
870
|
+
* see `fetch failed` inside the same second and the very next request
|
|
871
|
+
* succeeds — exactly the case a single attempt turns into "the user's model
|
|
872
|
+
* list is missing half its entries for the rest of the session".
|
|
873
|
+
*/
|
|
874
|
+
async fetchModelsOnce(credential, signal) {
|
|
800
875
|
const global = regionOf(credential.domain) === "global";
|
|
801
876
|
const url = global ? `${globalBase(credential)}${GLOBAL_CONFIG_PATH}` : `${chatBase(credential)}${MODELS_CATALOG_PATH}`;
|
|
802
877
|
const headers = global ? {
|
|
@@ -1815,42 +1890,155 @@ function macosBundleExecutable(bundle) {
|
|
|
1815
1890
|
return join(bundle, "Contents", "MacOS", name);
|
|
1816
1891
|
}
|
|
1817
1892
|
/**
|
|
1893
|
+
* Encodings a Windows console child process may emit in, most likely first.
|
|
1894
|
+
*
|
|
1895
|
+
* `reg.exe` writes with the ACTIVE CODE PAGE, never UTF-8: a Chinese Windows
|
|
1896
|
+
* (code page 936) returns a path containing 「腾讯」 as GBK bytes. Decoding
|
|
1897
|
+
* those as UTF-8 mangles them into `��Ѷ`, every `existsSync` then fails, and the
|
|
1898
|
+
* plugin concludes no desktop app is installed while one is running fine.
|
|
1899
|
+
*
|
|
1900
|
+
* The list is ordered and each entry is TRIED rather than guessed: the first
|
|
1901
|
+
* decoding that yields usable text wins, and the decision is validated against
|
|
1902
|
+
* the filesystem rather than by sniffing bytes.
|
|
1903
|
+
*/
|
|
1904
|
+
const WINDOWS_CONSOLE_ENCODINGS = [
|
|
1905
|
+
"utf-8",
|
|
1906
|
+
"gbk",
|
|
1907
|
+
"big5",
|
|
1908
|
+
"shift_jis",
|
|
1909
|
+
"euc-kr",
|
|
1910
|
+
"windows-1252"
|
|
1911
|
+
];
|
|
1912
|
+
/**
|
|
1913
|
+
* Decode bytes from a Windows console child process.
|
|
1914
|
+
*
|
|
1915
|
+
* `accept` decides whether a decoding is usable — the callers pass "this text
|
|
1916
|
+
* contains a path that actually exists". Bytes that are already UTF-8 pass on
|
|
1917
|
+
* the first attempt, so the healthy case pays nothing.
|
|
1918
|
+
*/
|
|
1919
|
+
function decodeWindowsConsole(buf, accept) {
|
|
1920
|
+
for (const label of WINDOWS_CONSOLE_ENCODINGS) {
|
|
1921
|
+
let text;
|
|
1922
|
+
try {
|
|
1923
|
+
text = new TextDecoder(label, { fatal: false }).decode(buf);
|
|
1924
|
+
} catch {
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
if (accept(text)) return text;
|
|
1928
|
+
}
|
|
1929
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(buf);
|
|
1930
|
+
}
|
|
1931
|
+
/** Strip the `"<path>",<index>` decoration DisplayIcon carries. */
|
|
1932
|
+
function cleanIconPath(raw) {
|
|
1933
|
+
return raw.replace(/^"/u, "").replace(/",-?\d+$/u, "").replace(/,-?\d+$/u, "").trim();
|
|
1934
|
+
}
|
|
1935
|
+
/** Registry locations the installer records its own product under. */
|
|
1936
|
+
const UNINSTALL_KEY_ROOTS = [
|
|
1937
|
+
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
1938
|
+
"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
1939
|
+
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
|
|
1940
|
+
];
|
|
1941
|
+
/**
|
|
1818
1942
|
* Windows install locations recorded by the app's own uninstaller.
|
|
1819
1943
|
*
|
|
1820
1944
|
* The registry is the authoritative answer: it survives a non-default drive, a
|
|
1821
1945
|
* renamed folder and a differently-named executable, none of which any fixed
|
|
1822
1946
|
* path list can predict. Real machines put the app at `D:\workbuddy\WorkBuddy.exe`
|
|
1823
|
-
* and `D:\
|
|
1824
|
-
* `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see
|
|
1825
|
-
* plugin reported "the desktop app could not provide the key" for an app that was
|
|
1826
|
-
* installed and running.
|
|
1947
|
+
* and `D:\ruanjian\anzhuang\腾讯\WorkBuddy\WorkBuddy.exe` — layouts a
|
|
1948
|
+
* `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see.
|
|
1827
1949
|
*
|
|
1828
1950
|
* `DisplayIcon` is the field that actually carries the path (observed as
|
|
1829
1951
|
* `D:\workbuddy\WorkBuddy.exe,0`); `InstallLocation` is usually empty for these
|
|
1830
1952
|
* installers, so both are read and either may contribute.
|
|
1831
1953
|
*
|
|
1954
|
+
* Two readers, because neither alone is enough:
|
|
1955
|
+
*
|
|
1956
|
+
* - **PowerShell first.** `Get-ItemProperty` talks to the registry API, so no
|
|
1957
|
+
* console code page is involved and non-ASCII paths come back intact. This
|
|
1958
|
+
* is the encoding-proof path.
|
|
1959
|
+
* - **`reg.exe` as a fallback**, for a machine where PowerShell is unavailable
|
|
1960
|
+
* (locked-down or stripped images). Its raw bytes are decoded through
|
|
1961
|
+
* {@link decodeWindowsConsole}, which is what the original code got wrong.
|
|
1962
|
+
*
|
|
1832
1963
|
* Returns [] on any failure — a missing registry key is the normal case on
|
|
1833
1964
|
* non-Windows, not an error.
|
|
1834
1965
|
*/
|
|
1835
1966
|
function windowsRegistryAppPaths() {
|
|
1836
1967
|
if (process.platform !== "win32") return [];
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1968
|
+
const viaPowerShell = windowsRegistryAppPathsViaPowerShell();
|
|
1969
|
+
if (viaPowerShell.length > 0) return viaPowerShell;
|
|
1970
|
+
return windowsRegistryAppPathsViaReg();
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Registry read through PowerShell, immune to the console code page.
|
|
1974
|
+
*
|
|
1975
|
+
* `[Console]::OutputEncoding` is set to UTF-8 so the bytes Node receives are
|
|
1976
|
+
* UTF-8 regardless of the machine's code page. Values are printed one per line
|
|
1977
|
+
* and matched by the caller, so a Chinese path survives the round trip.
|
|
1978
|
+
*
|
|
1979
|
+
* Best-effort: any failure (no PowerShell, no keys, a policy restriction)
|
|
1980
|
+
* returns [], and the `reg.exe` reader takes over.
|
|
1981
|
+
*/
|
|
1982
|
+
function windowsRegistryAppPathsViaPowerShell() {
|
|
1983
|
+
const script = [
|
|
1984
|
+
"$ErrorActionPreference='SilentlyContinue'",
|
|
1985
|
+
"try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}",
|
|
1986
|
+
"$roots=@('HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*')",
|
|
1987
|
+
"foreach($r in $roots){",
|
|
1988
|
+
" Get-ItemProperty -Path $r | ForEach-Object {",
|
|
1989
|
+
" $dn=[string]$_.DisplayName; $di=[string]$_.DisplayIcon; $il=[string]$_.InstallLocation",
|
|
1990
|
+
" if($dn -match 'WorkBuddy|CodeBuddy' -or $di -match 'WorkBuddy|CodeBuddy'){",
|
|
1991
|
+
" if($di -ne ''){ Write-Output $di }",
|
|
1992
|
+
" if($il -ne ''){ Write-Output $il }",
|
|
1993
|
+
" }",
|
|
1994
|
+
" }",
|
|
1995
|
+
"}"
|
|
1996
|
+
].join("\n");
|
|
1997
|
+
let stdout;
|
|
1998
|
+
try {
|
|
1999
|
+
stdout = execFileSync("powershell", [
|
|
2000
|
+
"-NoProfile",
|
|
2001
|
+
"-NonInteractive",
|
|
2002
|
+
"-ExecutionPolicy",
|
|
2003
|
+
"Bypass",
|
|
2004
|
+
"-Command",
|
|
2005
|
+
script
|
|
2006
|
+
], {
|
|
2007
|
+
encoding: "utf8",
|
|
2008
|
+
timeout: 2e4,
|
|
2009
|
+
windowsHide: true,
|
|
2010
|
+
maxBuffer: 8388608
|
|
2011
|
+
});
|
|
2012
|
+
} catch {
|
|
2013
|
+
return [];
|
|
2014
|
+
}
|
|
1842
2015
|
const out = [];
|
|
1843
|
-
for (const
|
|
1844
|
-
|
|
2016
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
2017
|
+
const cleaned = cleanIconPath(line.trim());
|
|
2018
|
+
if (cleaned !== "") out.push(cleaned);
|
|
2019
|
+
}
|
|
2020
|
+
return out;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Registry read through `reg.exe`, with console-code-page detection.
|
|
2024
|
+
*
|
|
2025
|
+
* Kept as the fallback for machines without PowerShell. The decode step is the
|
|
2026
|
+
* fix: the output is captured as BYTES and decoded with whichever encoding
|
|
2027
|
+
* yields a path that exists, instead of assuming UTF-8 and quietly producing
|
|
2028
|
+
* mojibake that no existence check can ever match.
|
|
2029
|
+
*/
|
|
2030
|
+
function windowsRegistryAppPathsViaReg() {
|
|
2031
|
+
const out = [];
|
|
2032
|
+
for (const root of UNINSTALL_KEY_ROOTS) {
|
|
2033
|
+
let listingBuf;
|
|
1845
2034
|
try {
|
|
1846
|
-
|
|
2035
|
+
listingBuf = execFileSync("reg", [
|
|
1847
2036
|
"query",
|
|
1848
2037
|
root,
|
|
1849
2038
|
"/s",
|
|
1850
2039
|
"/v",
|
|
1851
2040
|
"DisplayName"
|
|
1852
2041
|
], {
|
|
1853
|
-
encoding: "utf8",
|
|
1854
2042
|
timeout: 1e4,
|
|
1855
2043
|
windowsHide: true,
|
|
1856
2044
|
maxBuffer: 8388608
|
|
@@ -1858,26 +2046,35 @@ function windowsRegistryAppPaths() {
|
|
|
1858
2046
|
} catch {
|
|
1859
2047
|
continue;
|
|
1860
2048
|
}
|
|
1861
|
-
const keys =
|
|
2049
|
+
const keys = decodeWindowsConsole(listingBuf, (text) => /WorkBuddy|CodeBuddy/iu.test(text)).split(/\r?\n(?=HKEY_)/u).filter((block) => /WorkBuddy|CodeBuddy/iu.test(block));
|
|
1862
2050
|
for (const key of keys) {
|
|
1863
2051
|
const keyPath = /^(HKEY_[^\r\n]+)/u.exec(key)?.[1]?.trim();
|
|
1864
2052
|
if (keyPath === void 0) continue;
|
|
1865
|
-
for (const name of ["DisplayIcon", "InstallLocation"])
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2053
|
+
for (const name of ["DisplayIcon", "InstallLocation"]) {
|
|
2054
|
+
let valueBuf;
|
|
2055
|
+
try {
|
|
2056
|
+
valueBuf = execFileSync("reg", [
|
|
2057
|
+
"query",
|
|
2058
|
+
keyPath,
|
|
2059
|
+
"/v",
|
|
2060
|
+
name
|
|
2061
|
+
], {
|
|
2062
|
+
timeout: 5e3,
|
|
2063
|
+
windowsHide: true
|
|
2064
|
+
});
|
|
2065
|
+
} catch {
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
const value = decodeWindowsConsole(valueBuf, (text) => {
|
|
2069
|
+
const candidate = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(text)?.[1]?.trim();
|
|
2070
|
+
if (candidate === void 0) return false;
|
|
2071
|
+
const path = cleanIconPath(candidate);
|
|
2072
|
+
return path !== "" && existsSync(path);
|
|
1875
2073
|
});
|
|
1876
2074
|
const raw = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(value)?.[1]?.trim();
|
|
1877
2075
|
if (raw === void 0 || raw === "") continue;
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
} catch {}
|
|
2076
|
+
out.push(cleanIconPath(raw));
|
|
2077
|
+
}
|
|
1881
2078
|
}
|
|
1882
2079
|
}
|
|
1883
2080
|
return out;
|
|
@@ -2146,7 +2343,7 @@ const ENCRYPTED_CREDENTIAL_CODE = "ENCRYPTED_CREDENTIAL";
|
|
|
2146
2343
|
var WorkBuddyEncryptedCredentialError = class extends Error {
|
|
2147
2344
|
code = ENCRYPTED_CREDENTIAL_CODE;
|
|
2148
2345
|
constructor(sourcePath) {
|
|
2149
|
-
super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed.`);
|
|
2346
|
+
super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed — if one of them looks like mojibake (e.g. "??" where a Chinese folder name should be), the registry value could not be decoded on this machine; report that line and use the override above in the meantime.`);
|
|
2150
2347
|
this.name = "WorkBuddyEncryptedCredentialError";
|
|
2151
2348
|
}
|
|
2152
2349
|
};
|
|
@@ -2879,38 +3076,97 @@ ${new Date(until).toISOString()}
|
|
|
2879
3076
|
* Static fallback used before the first live catalog fetch, and whenever the
|
|
2880
3077
|
* upstream cannot be reached.
|
|
2881
3078
|
*
|
|
2882
|
-
*
|
|
2883
|
-
*
|
|
2884
|
-
*
|
|
2885
|
-
*
|
|
2886
|
-
*
|
|
2887
|
-
*
|
|
3079
|
+
* This table is the DOMESTIC (CN) roster, and it is kept in step with what
|
|
3080
|
+
* `copilot.tencent.com` actually advertises. It matters more than a "just in
|
|
3081
|
+
* case" list usually would: a failed startup fetch falls back to it, and the
|
|
3082
|
+
* user sees that as "half my models were deleted" — `deepseek-v4.1-flash`
|
|
3083
|
+
* and friends simply vanishing from the picker, with no visible explanation.
|
|
3084
|
+
*
|
|
3085
|
+
* Every row below was captured from the live endpoint, INCLUDING the window
|
|
3086
|
+
* sizes: the previous version carried a 32K window for `hy3` when the gateway
|
|
3087
|
+
* says 192K, and listed `kimi-k3` under an id the gateway no longer uses
|
|
3088
|
+
* (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
|
|
3089
|
+
* authoritative while being wrong.
|
|
2888
3090
|
*
|
|
2889
3091
|
* `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
|
|
2890
3092
|
* which is what turns on the free badge.
|
|
2891
3093
|
*/
|
|
2892
3094
|
const FALLBACK_WORKBUDDY_MODELS = [
|
|
3095
|
+
{
|
|
3096
|
+
id: "auto",
|
|
3097
|
+
name: "Auto",
|
|
3098
|
+
contextWindow: 256e3,
|
|
3099
|
+
maxOutputTokens: 32e3,
|
|
3100
|
+
supportsImages: true
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
id: "hy4-preview",
|
|
3104
|
+
name: "Hy4 preview",
|
|
3105
|
+
contextWindow: 1e6,
|
|
3106
|
+
maxOutputTokens: 64e3,
|
|
3107
|
+
supportsImages: true,
|
|
3108
|
+
multiplier: .29
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
id: "hy3",
|
|
3112
|
+
name: "Hy3",
|
|
3113
|
+
contextWindow: 192e3,
|
|
3114
|
+
maxOutputTokens: 64e3,
|
|
3115
|
+
supportsImages: true,
|
|
3116
|
+
multiplier: 0
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
id: "hy3-x",
|
|
3120
|
+
name: "Hy3",
|
|
3121
|
+
contextWindow: 192e3,
|
|
3122
|
+
maxOutputTokens: 64e3,
|
|
3123
|
+
supportsImages: true,
|
|
3124
|
+
multiplier: .05
|
|
3125
|
+
},
|
|
3126
|
+
{
|
|
3127
|
+
id: "deepseek-v4.1-flash",
|
|
3128
|
+
name: "Deepseek-V4.1-Flash",
|
|
3129
|
+
contextWindow: 1e6,
|
|
3130
|
+
maxOutputTokens: 128e3,
|
|
3131
|
+
supportsImages: true,
|
|
3132
|
+
multiplier: .11
|
|
3133
|
+
},
|
|
3134
|
+
{
|
|
3135
|
+
id: "deepseek-v4-pro",
|
|
3136
|
+
name: "Deepseek-V4-Pro",
|
|
3137
|
+
contextWindow: 1e6,
|
|
3138
|
+
maxOutputTokens: 128e3,
|
|
3139
|
+
supportsImages: true,
|
|
3140
|
+
multiplier: .51
|
|
3141
|
+
},
|
|
3142
|
+
{
|
|
3143
|
+
id: "deepseek-v4-flash",
|
|
3144
|
+
name: "DeepSeek-V4-Flash",
|
|
3145
|
+
contextWindow: 1e6,
|
|
3146
|
+
maxOutputTokens: 128e3,
|
|
3147
|
+
supportsImages: true
|
|
3148
|
+
},
|
|
2893
3149
|
{
|
|
2894
3150
|
id: "glm-5.3",
|
|
2895
3151
|
name: "GLM-5.3",
|
|
2896
|
-
contextWindow:
|
|
2897
|
-
maxOutputTokens:
|
|
3152
|
+
contextWindow: 1e6,
|
|
3153
|
+
maxOutputTokens: 64e3,
|
|
2898
3154
|
supportsImages: true,
|
|
2899
3155
|
multiplier: .79
|
|
2900
3156
|
},
|
|
2901
3157
|
{
|
|
2902
3158
|
id: "glm-5.3-flash",
|
|
2903
3159
|
name: "GLM-5.3-Flash",
|
|
2904
|
-
contextWindow:
|
|
2905
|
-
maxOutputTokens:
|
|
3160
|
+
contextWindow: 1e6,
|
|
3161
|
+
maxOutputTokens: 131072,
|
|
2906
3162
|
supportsImages: true,
|
|
2907
3163
|
multiplier: .06
|
|
2908
3164
|
},
|
|
2909
3165
|
{
|
|
2910
3166
|
id: "glm-5.2",
|
|
2911
3167
|
name: "GLM-5.2",
|
|
2912
|
-
contextWindow:
|
|
2913
|
-
maxOutputTokens:
|
|
3168
|
+
contextWindow: 1e6,
|
|
3169
|
+
maxOutputTokens: 64e3,
|
|
2914
3170
|
supportsImages: true,
|
|
2915
3171
|
multiplier: .79
|
|
2916
3172
|
},
|
|
@@ -2918,53 +3174,57 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
2918
3174
|
id: "glm-5.1",
|
|
2919
3175
|
name: "GLM-5.1",
|
|
2920
3176
|
contextWindow: 2e5,
|
|
2921
|
-
maxOutputTokens:
|
|
2922
|
-
supportsImages: false
|
|
2923
|
-
|
|
2924
|
-
{
|
|
2925
|
-
id: "deepseek-v4-pro",
|
|
2926
|
-
name: "DeepSeek-V4-Pro",
|
|
2927
|
-
contextWindow: 2e5,
|
|
2928
|
-
maxOutputTokens: 128e3,
|
|
2929
|
-
supportsImages: true
|
|
3177
|
+
maxOutputTokens: 48e3,
|
|
3178
|
+
supportsImages: false,
|
|
3179
|
+
multiplier: .79
|
|
2930
3180
|
},
|
|
2931
3181
|
{
|
|
2932
|
-
id: "
|
|
2933
|
-
name: "
|
|
3182
|
+
id: "glm-5v-turbo",
|
|
3183
|
+
name: "GLM-5v-Turbo",
|
|
2934
3184
|
contextWindow: 2e5,
|
|
2935
|
-
maxOutputTokens:
|
|
2936
|
-
supportsImages: true
|
|
3185
|
+
maxOutputTokens: 64e3,
|
|
3186
|
+
supportsImages: true,
|
|
3187
|
+
multiplier: .71
|
|
2937
3188
|
},
|
|
2938
3189
|
{
|
|
2939
|
-
id: "kimi-k3",
|
|
3190
|
+
id: "kimi-k3-1",
|
|
2940
3191
|
name: "Kimi-K3",
|
|
2941
|
-
contextWindow:
|
|
2942
|
-
maxOutputTokens:
|
|
3192
|
+
contextWindow: 1e6,
|
|
3193
|
+
maxOutputTokens: 32e3,
|
|
2943
3194
|
supportsImages: true,
|
|
2944
3195
|
multiplier: 1.62
|
|
2945
3196
|
},
|
|
2946
3197
|
{
|
|
2947
|
-
id: "
|
|
2948
|
-
name: "
|
|
2949
|
-
contextWindow:
|
|
2950
|
-
maxOutputTokens:
|
|
2951
|
-
supportsImages: true
|
|
3198
|
+
id: "kimi-k2.8-preview",
|
|
3199
|
+
name: "Kimi-K2.8-Preview",
|
|
3200
|
+
contextWindow: 1e6,
|
|
3201
|
+
maxOutputTokens: 64e3,
|
|
3202
|
+
supportsImages: true,
|
|
3203
|
+
multiplier: .77
|
|
2952
3204
|
},
|
|
2953
3205
|
{
|
|
2954
|
-
id: "
|
|
2955
|
-
name: "
|
|
2956
|
-
contextWindow:
|
|
2957
|
-
maxOutputTokens:
|
|
3206
|
+
id: "kimi-k2.7",
|
|
3207
|
+
name: "Kimi-K2.7-Code",
|
|
3208
|
+
contextWindow: 256e3,
|
|
3209
|
+
maxOutputTokens: 32e3,
|
|
2958
3210
|
supportsImages: true,
|
|
2959
|
-
multiplier:
|
|
3211
|
+
multiplier: .57
|
|
2960
3212
|
},
|
|
2961
3213
|
{
|
|
2962
|
-
id: "
|
|
2963
|
-
name: "
|
|
2964
|
-
contextWindow:
|
|
2965
|
-
maxOutputTokens:
|
|
3214
|
+
id: "kimi-k2.6",
|
|
3215
|
+
name: "Kimi-K2.6",
|
|
3216
|
+
contextWindow: 256e3,
|
|
3217
|
+
maxOutputTokens: 32e3,
|
|
2966
3218
|
supportsImages: true,
|
|
2967
|
-
multiplier: .
|
|
3219
|
+
multiplier: .52
|
|
3220
|
+
},
|
|
3221
|
+
{
|
|
3222
|
+
id: "minimax-m3",
|
|
3223
|
+
name: "MiniMax-M3",
|
|
3224
|
+
contextWindow: 512e3,
|
|
3225
|
+
maxOutputTokens: 64e3,
|
|
3226
|
+
supportsImages: true,
|
|
3227
|
+
multiplier: .25
|
|
2968
3228
|
}
|
|
2969
3229
|
];
|
|
2970
3230
|
/** Live catalog with a static fallback behind it. */
|
|
@@ -2973,6 +3233,19 @@ var WorkBuddyCatalog = class {
|
|
|
2973
3233
|
listeners = /* @__PURE__ */ new Set();
|
|
2974
3234
|
/** User's model selection. Empty object = follow the catalog unfiltered. */
|
|
2975
3235
|
selection = {};
|
|
3236
|
+
/**
|
|
3237
|
+
* Whether `models` came from the gateway or from the static table.
|
|
3238
|
+
*
|
|
3239
|
+
* Tracked so the card can SAY which it is showing. A failed fetch used to be
|
|
3240
|
+
* invisible on screen: the picker simply held fewer models than before, which
|
|
3241
|
+
* the user reasonably read as "the plugin deleted my models" rather than "the
|
|
3242
|
+
* network hiccuped at startup".
|
|
3243
|
+
*/
|
|
3244
|
+
source = "fallback";
|
|
3245
|
+
/** When the live list last landed. */
|
|
3246
|
+
lastUpdatedAt;
|
|
3247
|
+
/** Why the last fetch failed, for display. */
|
|
3248
|
+
lastError;
|
|
2976
3249
|
current() {
|
|
2977
3250
|
return this.models;
|
|
2978
3251
|
}
|
|
@@ -3007,6 +3280,7 @@ var WorkBuddyCatalog = class {
|
|
|
3007
3280
|
/** Restore the static fallback, e.g. when the upstream stops answering. */
|
|
3008
3281
|
reset() {
|
|
3009
3282
|
this.models = FALLBACK_WORKBUDDY_MODELS;
|
|
3283
|
+
this.source = "fallback";
|
|
3010
3284
|
this.notify();
|
|
3011
3285
|
}
|
|
3012
3286
|
/** Replace the user's selection; the adapter rebuilds from `visible()`. */
|
|
@@ -3018,6 +3292,31 @@ var WorkBuddyCatalog = class {
|
|
|
3018
3292
|
currentSelection() {
|
|
3019
3293
|
return this.selection;
|
|
3020
3294
|
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Whether this catalog is serving live data or the built-in table.
|
|
3297
|
+
*
|
|
3298
|
+
* `fallback` is not an error state, but it IS a degraded one: the user is
|
|
3299
|
+
* looking at a shorter roster than the gateway offers, so the card says so
|
|
3300
|
+
* and offers a retry instead of letting them wonder where the models went.
|
|
3301
|
+
*/
|
|
3302
|
+
currentSource() {
|
|
3303
|
+
return this.source;
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3306
|
+
* When the live list last landed, if it ever did.
|
|
3307
|
+
*
|
|
3308
|
+
* Named `catalogUpdatedAt()` rather than `updatedAt()` because the class
|
|
3309
|
+
* already had an `updatedAt` member; two members of the same name is a
|
|
3310
|
+
* compile error, and the awkwardness is a useful signal that the concept is
|
|
3311
|
+
* "when THIS catalog was refreshed", not a generic timestamp.
|
|
3312
|
+
*/
|
|
3313
|
+
catalogUpdatedAt() {
|
|
3314
|
+
return this.lastUpdatedAt;
|
|
3315
|
+
}
|
|
3316
|
+
/** Why the last fetch failed, if it did. */
|
|
3317
|
+
lastFetchError() {
|
|
3318
|
+
return this.lastError;
|
|
3319
|
+
}
|
|
3021
3320
|
onChange(listener) {
|
|
3022
3321
|
this.listeners.add(listener);
|
|
3023
3322
|
return () => this.listeners.delete(listener);
|
|
@@ -3028,6 +3327,20 @@ var WorkBuddyCatalog = class {
|
|
|
3028
3327
|
/** Replace the catalog from the live upstream list; keeps the fallback if empty. */
|
|
3029
3328
|
updateFromUpstream(models) {
|
|
3030
3329
|
this.update(catalogFromUpstream(models));
|
|
3330
|
+
this.source = "live";
|
|
3331
|
+
this.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3332
|
+
this.lastError = void 0;
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Record that a fetch attempt failed, leaving the current list in place.
|
|
3336
|
+
*
|
|
3337
|
+
* The list is deliberately NOT reset here: a refresh that fails should keep
|
|
3338
|
+
* whatever working catalog is already loaded, rather than demoting a healthy
|
|
3339
|
+
* session to the static table because one retry ran out.
|
|
3340
|
+
*/
|
|
3341
|
+
noteFetchFailure(message) {
|
|
3342
|
+
this.lastError = message;
|
|
3343
|
+
this.notify();
|
|
3031
3344
|
}
|
|
3032
3345
|
notify() {
|
|
3033
3346
|
for (const listener of this.listeners) listener();
|
|
@@ -4920,6 +5233,31 @@ async function commandDoctor() {
|
|
|
4920
5233
|
imported = (await readdir2(accountDir())).filter((name) => name.endsWith(".json"));
|
|
4921
5234
|
} catch {}
|
|
4922
5235
|
lines.push(imported.length === 0 ? " (none)" : imported.map((name) => ` ✓ ${name}`).join("\n"));
|
|
5236
|
+
lines.push("");
|
|
5237
|
+
lines.push("WorkBuddy desktop app:");
|
|
5238
|
+
const override = process.env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim();
|
|
5239
|
+
if (override !== void 0 && override !== "") lines.push(` env ${WORKBUDDY_APP_EXECUTABLE_ENV} = ${override}`);
|
|
5240
|
+
let candidates = [];
|
|
5241
|
+
try {
|
|
5242
|
+
candidates = workbuddyAppExecutableCandidates();
|
|
5243
|
+
} catch (error) {
|
|
5244
|
+
lines.push(` ✗ candidate probe threw: ${error instanceof Error ? error.message : String(error)}`);
|
|
5245
|
+
}
|
|
5246
|
+
let firstExisting;
|
|
5247
|
+
for (const candidate of candidates) {
|
|
5248
|
+
const exists = existsSync(candidate);
|
|
5249
|
+
if (exists && firstExisting === void 0) firstExisting = candidate;
|
|
5250
|
+
lines.push(` ${exists ? "✓" : "·"} ${candidate}`);
|
|
5251
|
+
}
|
|
5252
|
+
lines.push(` probed ${candidates.length} candidate path(s)`);
|
|
5253
|
+
if (firstExisting === void 0) {
|
|
5254
|
+
healthy = false;
|
|
5255
|
+
lines.push(" ✗ no WorkBuddy desktop executable found at any probed path.");
|
|
5256
|
+
lines.push(" If the app IS installed, set WORKBUDDY_APP_EXECUTABLE to its full .exe path");
|
|
5257
|
+
lines.push(" (or put that line in $DSH_HOME/.env) and restart DSH. A path shown above that");
|
|
5258
|
+
lines.push(" looks like mojibake (e.g. \"???\" for a Chinese folder) indicates the registry");
|
|
5259
|
+
lines.push(" value could not be decoded on this machine — please report it with the line.");
|
|
5260
|
+
} else lines.push(` ✓ using ${firstExisting}`);
|
|
4923
5261
|
console.log(lines.join("\n"));
|
|
4924
5262
|
return healthy ? 0 : 1;
|
|
4925
5263
|
}
|