dsh-connect-workbuddy 3.1.0 → 3.3.0
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 +146 -0
- package/README.en.md +49 -20
- package/README.md +49 -22
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/docs/WINDOWS.md +1 -0
- package/docs/assets/account-pool-demo.html +207 -262
- package/lib/bin.js +1 -1
- package/lib/client.js +222 -686
- package/lib/{host-heartbeat-DZnPggVT.js → host-heartbeat-t93cLCoW.js} +63 -2
- package/lib/index.d.ts +81 -30
- package/lib/index.js +1327 -250
- package/package.json +1 -1
|
@@ -637,6 +637,67 @@ function normalizeToolChoice(obj) {
|
|
|
637
637
|
delete obj["tool_choice"];
|
|
638
638
|
}
|
|
639
639
|
/**
|
|
640
|
+
* What a PREPARED request body declares, as the input to DSML recovery.
|
|
641
|
+
*
|
|
642
|
+
* This exists so the recovery path can answer the one question its gates turn
|
|
643
|
+
* on — "was this tool name offered in THIS request?" — without a second parse
|
|
644
|
+
* of the body and without a second source of truth. `prepareChatBody` has
|
|
645
|
+
* already parsed it; this reads the same prepared JSON.
|
|
646
|
+
*
|
|
647
|
+
* Two facts make it cheap and exact:
|
|
648
|
+
*
|
|
649
|
+
* - `tool_choice: "none"` deletes `tools` outright in
|
|
650
|
+
* {@link normalizeToolChoice}, so "no tools declared" is directly
|
|
651
|
+
* observable here rather than a separate condition to remember;
|
|
652
|
+
* - a pinned `tool_choice` arrives as the bare function name (the object form
|
|
653
|
+
* was flattened), so anything that is not `auto`/`required`/`none` is a pin.
|
|
654
|
+
*
|
|
655
|
+
* Returns `undefined` when the body declares nothing usable — malformed JSON,
|
|
656
|
+
* no `tools` array, or an empty one. Callers must read that as "recovery is
|
|
657
|
+
* off" (gate 3), not as "no information, so guess".
|
|
658
|
+
*/
|
|
659
|
+
function declaredTools(bodyJson) {
|
|
660
|
+
let body;
|
|
661
|
+
try {
|
|
662
|
+
body = JSON.parse(bodyJson);
|
|
663
|
+
} catch {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return void 0;
|
|
667
|
+
const obj = body;
|
|
668
|
+
const tools = obj["tools"];
|
|
669
|
+
if (!Array.isArray(tools)) return void 0;
|
|
670
|
+
const names = /* @__PURE__ */ new Set();
|
|
671
|
+
const requiredParameters = /* @__PURE__ */ new Map();
|
|
672
|
+
for (const value of tools) {
|
|
673
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
674
|
+
const fn = value["function"];
|
|
675
|
+
if (typeof fn !== "object" || fn === null || Array.isArray(fn)) continue;
|
|
676
|
+
const definition = fn;
|
|
677
|
+
const name = typeof definition["name"] === "string" ? definition["name"] : "";
|
|
678
|
+
if (name === "") continue;
|
|
679
|
+
names.add(name);
|
|
680
|
+
const parameters = definition["parameters"];
|
|
681
|
+
if (typeof parameters !== "object" || parameters === null || Array.isArray(parameters)) continue;
|
|
682
|
+
const required = parameters["required"];
|
|
683
|
+
if (!Array.isArray(required)) continue;
|
|
684
|
+
const list = required.filter((entry) => typeof entry === "string" && entry !== "");
|
|
685
|
+
if (list.length > 0) requiredParameters.set(name, list);
|
|
686
|
+
}
|
|
687
|
+
if (names.size === 0) return void 0;
|
|
688
|
+
const pinned = typeof obj["tool_choice"] === "string" ? obj["tool_choice"].trim() : "";
|
|
689
|
+
const pinnedToolName = pinned !== "" && ![
|
|
690
|
+
"auto",
|
|
691
|
+
"required",
|
|
692
|
+
"none"
|
|
693
|
+
].includes(pinned.toLowerCase()) ? pinned : void 0;
|
|
694
|
+
return {
|
|
695
|
+
names,
|
|
696
|
+
requiredParameters,
|
|
697
|
+
...pinnedToolName === void 0 ? {} : { pinnedToolName }
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
640
701
|
* Gateway (openresty/APISIX) rejection of a token it no longer accepts.
|
|
641
702
|
*
|
|
642
703
|
* The business APIs answer JSON; an edge rejection answers an HTML error page
|
|
@@ -2459,7 +2520,7 @@ function generationOf(version) {
|
|
|
2459
2520
|
//#endregion
|
|
2460
2521
|
//#region src/version.ts
|
|
2461
2522
|
/** The npm package version this build was produced from. */
|
|
2462
|
-
const WORKBUDDY_CONNECT_VERSION = "3.
|
|
2523
|
+
const WORKBUDDY_CONNECT_VERSION = "3.3.0";
|
|
2463
2524
|
//#endregion
|
|
2464
2525
|
//#region src/host-heartbeat.ts
|
|
2465
2526
|
/**
|
|
@@ -2617,4 +2678,4 @@ async function clearHostHeartbeat() {
|
|
|
2617
2678
|
await rm(workbuddyHostHeartbeatPath(), { force: true });
|
|
2618
2679
|
}
|
|
2619
2680
|
//#endregion
|
|
2620
|
-
export {
|
|
2681
|
+
export { readAtRestKey as $, CREDENTIAL_REJECTED_CODE as A, prepareChatBody as B, defaultDesktopAuthPath as C, parseWorkBuddyAuth as D, legacyWorkbuddyOwnAuthPath as E, declaredTools as F, deriveAtRestKeyId as G, WORKBUDDY_APP_EXECUTABLE_ENV as H, isCredentialRejectedError as I, isEncryptedFieldWrapper as J, fetchAtRestKeyPayload as K, parseCreditMultiplier as L, WorkBuddyCredentialRejectedError as M, WorkBuddyUpstreamClient as N, workbuddyAccountId as O, classifyUpstreamError as P, openEncryptedField as Q, parseReasoning as R, defaultDesktopAuthDirs as S, isEncryptedCredentialError as T, clearAtRestKeyCache as U, regionOf as V, deriveAtRestKey as W, macosBundleExecutable as X, isWorkbuddyBundle as Y, macosNestedAppBundles as Z, WORKBUDDY_AUTH_FILE_ENV as _, readHostHeartbeat as a, authFileName as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, workbuddyAppExecutableCandidates as et, WorkBuddyCatalog as f, WORKBUDDY_AUTH_FILENAME as g, ENCRYPTED_CREDENTIAL_CODE as h, processStartTimeMs as i, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as j, workbuddyOwnAuthPath as k, piAiRuntimeInfo as l, fallbackModelsFor as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, deriveCatalog as p, findWorkbuddyAppExecutable as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyCredentialStore as v, hasEncryptedCredentialFields as w, defaultDesktopAuthCandidates as x, WorkBuddyEncryptedCredentialError as y, parseUpstreamModel as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -793,12 +793,62 @@ interface WorkBuddyShim {
|
|
|
793
793
|
/** Stop serving and destroy open connections. */
|
|
794
794
|
close(): Promise<void>;
|
|
795
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Supplies the credential to retry a failed chat request with.
|
|
798
|
+
*
|
|
799
|
+
* Called with every account id already tried for THIS request, the one that just
|
|
800
|
+
* failed LAST, and resolves to the next candidate — or `undefined` when the pool
|
|
801
|
+
* is off, empty, or exhausted, in which case the original failure is reported
|
|
802
|
+
* unchanged.
|
|
803
|
+
*
|
|
804
|
+
* The plugin owns the policy (which pool members count as usable, in what
|
|
805
|
+
* order); the shim owns only the retry loop and the wire-level rule that a
|
|
806
|
+
* retry may happen before anything has been written to the client.
|
|
807
|
+
*/
|
|
808
|
+
type WorkBuddyFailoverAccount = (triedAccountIds: readonly string[]) => Promise<WorkBuddyCredential | undefined>;
|
|
796
809
|
/** Constructor dependencies. */
|
|
797
810
|
interface WorkBuddyShimOptions {
|
|
798
811
|
store: WorkBuddyCredentialStore;
|
|
799
812
|
client: Pick<WorkBuddyUpstreamClient, 'chatStream'>;
|
|
800
813
|
catalog: WorkBuddyCatalog;
|
|
801
814
|
logger?: ShimLogger;
|
|
815
|
+
/**
|
|
816
|
+
* Pool failover for chat requests. Absent means no failover at all — the
|
|
817
|
+
* pre-pool behaviour, where the selected account serves and its failure is
|
|
818
|
+
* reported as-is.
|
|
819
|
+
*/
|
|
820
|
+
failoverAccount?: WorkBuddyFailoverAccount;
|
|
821
|
+
/**
|
|
822
|
+
* Called before the FIRST attempt of every chat request, so the plugin can
|
|
823
|
+
* point the store at the account the pool's ranking says should serve.
|
|
824
|
+
*
|
|
825
|
+
* It exists because "who serves" and "who to retry with" are the same decision
|
|
826
|
+
* made at two moments: the ranking picks the first account, and after a failure
|
|
827
|
+
* picks the next one. The shim resolves the credential itself right after this
|
|
828
|
+
* returns, so the ranking never has to produce a credential — which keeps
|
|
829
|
+
* token REFRESH in one place (`store.resolve()`) instead of duplicating it on
|
|
830
|
+
* a path that would silently send expired tokens.
|
|
831
|
+
*
|
|
832
|
+
* Absent means the store's own selection always serves.
|
|
833
|
+
*/
|
|
834
|
+
prepareAccount?: () => Promise<void>;
|
|
835
|
+
/**
|
|
836
|
+
* Called for every attempt that failed upstream, before the next one is tried.
|
|
837
|
+
*
|
|
838
|
+
* Exists so the plugin can RECORD the failure as a measurement. The ranking
|
|
839
|
+
* that picks the serving account reads the pool's STORED measurements, so
|
|
840
|
+
* without this an account that just answered 429 still reads as untested — and
|
|
841
|
+
* the next request picks it again and pays the same failed round trip before
|
|
842
|
+
* failing over.
|
|
843
|
+
*
|
|
844
|
+
* Fired without awaiting: a slow write must never delay the retry the user is
|
|
845
|
+
* waiting on, and the measurement only matters for LATER requests.
|
|
846
|
+
*/
|
|
847
|
+
onAccountFailure?: (accountId: string, failure: {
|
|
848
|
+
status: number;
|
|
849
|
+
kind: UpstreamErrorKind;
|
|
850
|
+
message: string;
|
|
851
|
+
}) => void;
|
|
802
852
|
}
|
|
803
853
|
/**
|
|
804
854
|
* Start the loopback endpoint. Requests must carry the shim's shared secret;
|
|
@@ -1907,8 +1957,13 @@ interface WorkBuddyWebPoolAccount {
|
|
|
1907
1957
|
*/
|
|
1908
1958
|
interface WorkBuddyWebPool {
|
|
1909
1959
|
enabled: boolean;
|
|
1910
|
-
|
|
1911
|
-
|
|
1960
|
+
/**
|
|
1961
|
+
* Whether this region offers the daily check-in at all.
|
|
1962
|
+
*
|
|
1963
|
+
* The card hides the action entirely when false, rather than rendering a
|
|
1964
|
+
* button whose only outcome is a refusal.
|
|
1965
|
+
*/
|
|
1966
|
+
checkinSupported: boolean;
|
|
1912
1967
|
/** The model a test would use right now; absent when none can be resolved. */
|
|
1913
1968
|
targetModelId?: string;
|
|
1914
1969
|
/**
|
|
@@ -1936,15 +1991,6 @@ interface WorkBuddyWebPool {
|
|
|
1936
1991
|
* ran on ZERO accounts and reported success.
|
|
1937
1992
|
*/
|
|
1938
1993
|
effectiveMemberAccountIds?: readonly string[];
|
|
1939
|
-
/**
|
|
1940
|
-
* The account rotation most recently moved billing TO, when it moved.
|
|
1941
|
-
*
|
|
1942
|
-
* Reported so the card can record a switch it did not perform: rotation
|
|
1943
|
-
* happens on the Host (including on a timer), and the card is otherwise
|
|
1944
|
-
* unable to tell a switch from an unchanged state — which would make
|
|
1945
|
-
* "every switch is recorded" a promise the UI could not keep.
|
|
1946
|
-
*/
|
|
1947
|
-
rotatedToAccountId?: string;
|
|
1948
1994
|
accounts: readonly WorkBuddyWebPoolAccount[];
|
|
1949
1995
|
/**
|
|
1950
1996
|
* The models a manual target can be chosen from.
|
|
@@ -2093,8 +2139,6 @@ interface WorkBuddyPoolDeps {
|
|
|
2093
2139
|
/** The region's stored preferences, resolved from the Host config. */
|
|
2094
2140
|
preferences(region: WorkBuddyRegion): {
|
|
2095
2141
|
enabled: boolean;
|
|
2096
|
-
rotateByCredits: boolean;
|
|
2097
|
-
autoTestIntervalMinutes: number;
|
|
2098
2142
|
targetModelId: string;
|
|
2099
2143
|
memberAccountIds: readonly string[];
|
|
2100
2144
|
};
|
|
@@ -2110,14 +2154,6 @@ interface WorkBuddyPoolDeps {
|
|
|
2110
2154
|
* checked) instead of passing the guard and running a batch over nothing.
|
|
2111
2155
|
*/
|
|
2112
2156
|
effectiveMemberAccountIds?(region: WorkBuddyRegion): Promise<readonly string[]>;
|
|
2113
|
-
/**
|
|
2114
|
-
* The account rotation last redirected billing to, when it did.
|
|
2115
|
-
*
|
|
2116
|
-
* Reported so the card can log a switch that happened on the Host (possibly
|
|
2117
|
-
* on a timer) — without it, "every switch leaves a record" is a promise the
|
|
2118
|
-
* browser half has no way to keep.
|
|
2119
|
-
*/
|
|
2120
|
-
rotatedToAccountId?(region: WorkBuddyRegion): Promise<string | undefined>;
|
|
2121
2157
|
/**
|
|
2122
2158
|
* Today's check-in state per account id.
|
|
2123
2159
|
*
|
|
@@ -2125,6 +2161,8 @@ interface WorkBuddyPoolDeps {
|
|
|
2125
2161
|
* entry) means "not read", which the card renders as unknown rather than as a
|
|
2126
2162
|
* definite "not checked in" — a claim that would contradict the check-in the
|
|
2127
2163
|
* user just performed.
|
|
2164
|
+
*
|
|
2165
|
+
* Only the CN region has a check-in to report; see {@link checkinSupported}.
|
|
2128
2166
|
*/
|
|
2129
2167
|
checkedInToday?(region: WorkBuddyRegion): Promise<Readonly<Record<string, boolean>>>;
|
|
2130
2168
|
/**
|
|
@@ -2137,6 +2175,16 @@ interface WorkBuddyPoolDeps {
|
|
|
2137
2175
|
}[]>;
|
|
2138
2176
|
/** The account currently billing traffic for the region, when known. */
|
|
2139
2177
|
currentAccountId?(region: WorkBuddyRegion): Promise<string | undefined>;
|
|
2178
|
+
/**
|
|
2179
|
+
* Whether this region offers the daily check-in action at all.
|
|
2180
|
+
*
|
|
2181
|
+
* The CN app rewards a daily check-in; the international region is not offered
|
|
2182
|
+
* one here, so its card must not show the button and its route must refuse the
|
|
2183
|
+
* action. A capability rather than a region name compared in the browser: the
|
|
2184
|
+
* policy belongs in the Host, and the card should not have to know which
|
|
2185
|
+
* regions happen to have the feature.
|
|
2186
|
+
*/
|
|
2187
|
+
checkinSupported?(region: WorkBuddyRegion): boolean;
|
|
2140
2188
|
/**
|
|
2141
2189
|
* Claim the daily reward for every account of one region.
|
|
2142
2190
|
*
|
|
@@ -2224,18 +2272,21 @@ interface WorkBuddyRegionState {
|
|
|
2224
2272
|
* measurement living in the same slot would be rolled back by the next save.
|
|
2225
2273
|
*/
|
|
2226
2274
|
interface WorkBuddyPoolPreferences {
|
|
2227
|
-
/** Whether the pool is active for this region. Opt-in: default false. */
|
|
2228
|
-
enabled?: boolean;
|
|
2229
2275
|
/**
|
|
2230
|
-
* Whether the
|
|
2276
|
+
* Whether the pool is active for this region — which now means the
|
|
2277
|
+
* FAILOVER switch, not a credit ranking.
|
|
2278
|
+
*
|
|
2279
|
+
* Opt-in: default false. While on, a chat request that fails upstream is
|
|
2280
|
+
* retried against the other usable members of this region's pool, in order,
|
|
2281
|
+
* until one serves it or none is left (see `src/shim.ts`). While off the
|
|
2282
|
+
* plugin behaves exactly as it did before the pool existed: the account the
|
|
2283
|
+
* user selected serves every request, and a failure is reported as-is.
|
|
2231
2284
|
*
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2234
|
-
*
|
|
2285
|
+
* It deliberately does NOT change which account serves NEXT: failover is
|
|
2286
|
+
* per-request borrowing, so the user's selection stays in effect and the
|
|
2287
|
+
* card can keep reporting who really served.
|
|
2235
2288
|
*/
|
|
2236
|
-
|
|
2237
|
-
/** Minutes between automatic tests; clamped to 5..1440. */
|
|
2238
|
-
autoTestIntervalMinutes?: number;
|
|
2289
|
+
enabled?: boolean;
|
|
2239
2290
|
/** Model id to test; `''` means auto-pick a free model from this region. */
|
|
2240
2291
|
targetModelId?: string;
|
|
2241
2292
|
/**
|