dsh-connect-workbuddy 4.4.5 → 4.5.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 +43 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +273 -12
- package/lib/{host-heartbeat-Bj7JvSYJ.js → host-heartbeat-DvcOd-3C.js} +20 -2
- package/lib/index.d.ts +159 -3
- package/lib/index.js +352 -14
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-DvcOd-3C.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
@@ -1776,6 +1776,7 @@ function createWorkBuddyShim(options) {
|
|
|
1776
1776
|
const failoverAccount = options.failoverAccount;
|
|
1777
1777
|
const prepareAccount = options.prepareAccount;
|
|
1778
1778
|
const onAccountFailure = options.onAccountFailure;
|
|
1779
|
+
const onRequestBody = options.onRequestBody;
|
|
1779
1780
|
const SHARED_SECRET = randomBytes(32).toString("base64url");
|
|
1780
1781
|
/** Constant-time bearer check; absent or mismatched bearers are rejected. */
|
|
1781
1782
|
function bearerOk(req) {
|
|
@@ -1886,6 +1887,12 @@ function createWorkBuddyShim(options) {
|
|
|
1886
1887
|
}
|
|
1887
1888
|
const raw = (await readBody(req)).toString("utf8");
|
|
1888
1889
|
const prepared = prepareChatBody(raw);
|
|
1890
|
+
const requestModelId = modelIdOfBody(prepared);
|
|
1891
|
+
if (onRequestBody !== void 0) try {
|
|
1892
|
+
onRequestBody(prepared);
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
logger?.warn("dsh-connect-workbuddy: body report failed", error);
|
|
1895
|
+
}
|
|
1889
1896
|
const controller = new AbortController();
|
|
1890
1897
|
res.on("close", () => controller.abort());
|
|
1891
1898
|
let result = await client.chatStream(credential, prepared, controller.signal);
|
|
@@ -1929,7 +1936,8 @@ function createWorkBuddyShim(options) {
|
|
|
1929
1936
|
onAccountFailure(entry.accountId, {
|
|
1930
1937
|
status: entry.failure.status,
|
|
1931
1938
|
kind: entry.failure.kind,
|
|
1932
|
-
message: entry.failure.message
|
|
1939
|
+
message: entry.failure.message,
|
|
1940
|
+
...requestModelId === void 0 ? {} : { modelId: requestModelId }
|
|
1933
1941
|
});
|
|
1934
1942
|
} catch (error) {
|
|
1935
1943
|
logger?.warn("dsh-connect-workbuddy: recording an account failure failed", error);
|
|
@@ -2042,6 +2050,246 @@ function createWorkBuddyShim(options) {
|
|
|
2042
2050
|
};
|
|
2043
2051
|
}
|
|
2044
2052
|
//#endregion
|
|
2053
|
+
//#region src/body-report.ts
|
|
2054
|
+
/**
|
|
2055
|
+
* Body-composition report: WHAT was sent upstream, in counts (issue #59).
|
|
2056
|
+
*
|
|
2057
|
+
* WHY THIS EXISTS. Issue #59's reporter measured the SAME conversation charged
|
|
2058
|
+
* ~708K tokens by the CN gateway and ~1.374M by the international one (1.94×),
|
|
2059
|
+
* which turns a region switch into an instant `code 11115 prompt is too long`.
|
|
2060
|
+
* Two worlds explain that, and they have opposite owners:
|
|
2061
|
+
*
|
|
2062
|
+
* - the gateways count identical bytes differently (upstream's business); or
|
|
2063
|
+
* - the two requests did not carry the same messages (ours to fix).
|
|
2064
|
+
*
|
|
2065
|
+
* The second world was left standing by two independent measurements. The
|
|
2066
|
+
* reporter ran this project's `--kind`/`--language` probe on BOTH regions and
|
|
2067
|
+
* got the same tokens-per-1000-chars on byte-identical prose, code and JSON
|
|
2068
|
+
* (ratios 0.98–1.06 across six cells), so identical bytes really do bill
|
|
2069
|
+
* identically. And a code trace showed historical `reasoning` never reaches the
|
|
2070
|
+
* wire at all: pi-ai only forwards `thinking` blocks as text when
|
|
2071
|
+
* `compat.requiresThinkingAsText` is set — this plugin never sets it — and the
|
|
2072
|
+
* `reasoning_details` route needs a signature the stream parser never writes.
|
|
2073
|
+
* With the counting difference excluded and reasoning excluded, the remaining
|
|
2074
|
+
* candidate is that the two bodies differ in the first place.
|
|
2075
|
+
*
|
|
2076
|
+
* This module exists to settle exactly that, and it can do it WITHOUT either
|
|
2077
|
+
* region having credit: the body is assembled locally, before the upstream ever
|
|
2078
|
+
* gets a chance to answer `14018 Credits exhausted`. Capture one composition per
|
|
2079
|
+
* region in the same session and diff them.
|
|
2080
|
+
*
|
|
2081
|
+
* It records COUNTS ONLY — never a byte of message text, no account name, no
|
|
2082
|
+
* token. That is a hard rule, not a default: the point is to compare shape
|
|
2083
|
+
* (`messages.length`, per-block type, CJK vs non-CJK characters, total bytes),
|
|
2084
|
+
* and every one of those is answerable arithmetically. `tests/issue59-body-report.spec.ts`
|
|
2085
|
+
* asserts the report cannot leak a fixture's text.
|
|
2086
|
+
*/
|
|
2087
|
+
/**
|
|
2088
|
+
* Environment variable that enables the report.
|
|
2089
|
+
*
|
|
2090
|
+
* Unset or blank means OFF — a diagnostic that writes files must be opt-in.
|
|
2091
|
+
* `1`/`true` writes to `<DSH home>/body-reports/`; anything else is read as a
|
|
2092
|
+
* directory path, so a user can point it at an existing folder (and, on
|
|
2093
|
+
* Windows, one with spaces).
|
|
2094
|
+
*/
|
|
2095
|
+
const WORKBUDDY_BODY_REPORT_ENV = "WORKBUDDY_BODY_REPORT";
|
|
2096
|
+
/**
|
|
2097
|
+
* Character ranges that tokenize (and therefore bill) like CJK.
|
|
2098
|
+
*
|
|
2099
|
+
* Deliberately generous: the caller's question is "how much of this is the ~3×
|
|
2100
|
+
* kind", and the measured spread is 0.669 vs 0.224 tokens/char, so a character
|
|
2101
|
+
* class that is borderline costs far less than mis-attributing a whole bucket.
|
|
2102
|
+
* Ranges follow Unicode blocks, plus the compatibility forms and the two
|
|
2103
|
+
* extension planes where rare Han characters live.
|
|
2104
|
+
*/
|
|
2105
|
+
const CJK_RANGES = [
|
|
2106
|
+
[11904, 12031],
|
|
2107
|
+
[12288, 12351],
|
|
2108
|
+
[12352, 12543],
|
|
2109
|
+
[13312, 19903],
|
|
2110
|
+
[19968, 40959],
|
|
2111
|
+
[63744, 64255],
|
|
2112
|
+
[65072, 65103],
|
|
2113
|
+
[65280, 65519],
|
|
2114
|
+
[131072, 195103]
|
|
2115
|
+
];
|
|
2116
|
+
/** Whether one code point bills like CJK. */
|
|
2117
|
+
function isCjkCodePoint(code) {
|
|
2118
|
+
return CJK_RANGES.some(([low, high]) => code >= low && code <= high);
|
|
2119
|
+
}
|
|
2120
|
+
/** Count characters (code points) and how many of them are CJK. */
|
|
2121
|
+
function countChars(text) {
|
|
2122
|
+
let chars = 0;
|
|
2123
|
+
let cjkChars = 0;
|
|
2124
|
+
for (const character of text) {
|
|
2125
|
+
chars += 1;
|
|
2126
|
+
const code = character.codePointAt(0);
|
|
2127
|
+
if (code !== void 0 && isCjkCodePoint(code)) cjkChars += 1;
|
|
2128
|
+
}
|
|
2129
|
+
return {
|
|
2130
|
+
chars,
|
|
2131
|
+
cjkChars
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
/** Build one block report from already-extracted text. */
|
|
2135
|
+
function block(type, text) {
|
|
2136
|
+
return {
|
|
2137
|
+
type,
|
|
2138
|
+
...countChars(text)
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
/** Whether a value is a plain object (not null, not an array). */
|
|
2142
|
+
function isRecord(value) {
|
|
2143
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Describe one message's blocks.
|
|
2147
|
+
*
|
|
2148
|
+
* The role decides what a content string IS, and that is not cosmetic: a
|
|
2149
|
+
* `tool` message's payload is a tool result, so counting it as `text` would hide
|
|
2150
|
+
* the very bucket a long agentic session is mostly made of.
|
|
2151
|
+
*/
|
|
2152
|
+
function describeMessage(message) {
|
|
2153
|
+
const role = typeof message["role"] === "string" ? message["role"] : "unknown";
|
|
2154
|
+
const blocks = [];
|
|
2155
|
+
const content = message["content"];
|
|
2156
|
+
/**
|
|
2157
|
+
* The block type a plain string carries, by the ROLE carrying it.
|
|
2158
|
+
*
|
|
2159
|
+
* A `tool` message's string is a tool RESULT and a `system` one is the system
|
|
2160
|
+
* prompt; calling both `text` would fold the biggest bucket of a long agentic
|
|
2161
|
+
* session into the smallest one, which is the opposite of what a composition
|
|
2162
|
+
* report is for.
|
|
2163
|
+
*/
|
|
2164
|
+
const stringType = role === "tool" ? "tool-result" : role === "system" || role === "developer" ? "system" : "text";
|
|
2165
|
+
if (typeof content === "string") {
|
|
2166
|
+
if (content !== "") blocks.push(block(stringType, content));
|
|
2167
|
+
} else if (Array.isArray(content)) for (const part of content) {
|
|
2168
|
+
if (!isRecord(part)) continue;
|
|
2169
|
+
const type = typeof part["type"] === "string" ? part["type"] : "unknown";
|
|
2170
|
+
if (type === "text") {
|
|
2171
|
+
const text = part["text"];
|
|
2172
|
+
if (typeof text === "string" && text !== "") blocks.push(block(stringType, text));
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
blocks.push({
|
|
2176
|
+
type: type === "image_url" ? "image" : type,
|
|
2177
|
+
chars: 0,
|
|
2178
|
+
cjkChars: 0
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
const toolCalls = message["tool_calls"];
|
|
2182
|
+
if (Array.isArray(toolCalls)) for (const call of toolCalls) {
|
|
2183
|
+
if (!isRecord(call)) continue;
|
|
2184
|
+
const fn = call["function"];
|
|
2185
|
+
const name = isRecord(fn) && typeof fn["name"] === "string" ? fn["name"] : "";
|
|
2186
|
+
const args = isRecord(fn) && typeof fn["arguments"] === "string" ? fn["arguments"] : "";
|
|
2187
|
+
blocks.push(block("tool-call", `${name}${args}`));
|
|
2188
|
+
}
|
|
2189
|
+
for (const field of [
|
|
2190
|
+
"reasoning_content",
|
|
2191
|
+
"reasoning",
|
|
2192
|
+
"reasoning_text"
|
|
2193
|
+
]) {
|
|
2194
|
+
const value = message[field];
|
|
2195
|
+
if (typeof value === "string" && value !== "") blocks.push(block("reasoning", value));
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
role,
|
|
2199
|
+
chars: blocks.reduce((sum, entry) => sum + entry.chars, 0),
|
|
2200
|
+
cjkChars: blocks.reduce((sum, entry) => sum + entry.cjkChars, 0),
|
|
2201
|
+
blocks
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Describe one serialized request body.
|
|
2206
|
+
*
|
|
2207
|
+
* Returns `undefined` when the body is not a JSON object — a diagnostic must
|
|
2208
|
+
* never be the reason a chat request fails, so every caller treats `undefined`
|
|
2209
|
+
* as "nothing to report" rather than an error.
|
|
2210
|
+
*/
|
|
2211
|
+
function describeBody(bodyJson, region, nowMs) {
|
|
2212
|
+
let parsed;
|
|
2213
|
+
try {
|
|
2214
|
+
parsed = JSON.parse(bodyJson);
|
|
2215
|
+
} catch {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (!isRecord(parsed)) return void 0;
|
|
2219
|
+
const rawMessages = parsed["messages"];
|
|
2220
|
+
const messages = Array.isArray(rawMessages) ? rawMessages.filter(isRecord).map(describeMessage) : [];
|
|
2221
|
+
const rawTools = parsed["tools"];
|
|
2222
|
+
const tools = Array.isArray(rawTools) ? block("tools", rawTools.map((tool) => isRecord(tool) ? JSON.stringify(tool) : "").join("")) : {
|
|
2223
|
+
type: "tools",
|
|
2224
|
+
chars: 0,
|
|
2225
|
+
cjkChars: 0
|
|
2226
|
+
};
|
|
2227
|
+
const chars = messages.reduce((sum, message) => sum + message.chars, 0) + tools.chars;
|
|
2228
|
+
const cjkChars = messages.reduce((sum, message) => sum + message.cjkChars, 0) + tools.cjkChars;
|
|
2229
|
+
return {
|
|
2230
|
+
at: new Date(nowMs).toISOString(),
|
|
2231
|
+
region,
|
|
2232
|
+
...typeof parsed["model"] === "string" ? { model: parsed["model"] } : {},
|
|
2233
|
+
bodyBytes: Buffer.byteLength(bodyJson, "utf8"),
|
|
2234
|
+
messageCount: messages.length,
|
|
2235
|
+
chars,
|
|
2236
|
+
cjkChars,
|
|
2237
|
+
tools,
|
|
2238
|
+
messages
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Resolve the directory the report writes to, or `undefined` when disabled.
|
|
2243
|
+
*
|
|
2244
|
+
* Exported for the same reason the other env-var resolvers in this plugin are:
|
|
2245
|
+
* the behaviour is worth a unit test, and the test must not have to set a real
|
|
2246
|
+
* process-wide variable to get one.
|
|
2247
|
+
*/
|
|
2248
|
+
function bodyReportDirectory(env = process.env) {
|
|
2249
|
+
const configured = env[WORKBUDDY_BODY_REPORT_ENV];
|
|
2250
|
+
if (configured === void 0) return void 0;
|
|
2251
|
+
const trimmed = configured.trim();
|
|
2252
|
+
if (trimmed === "") return void 0;
|
|
2253
|
+
if (trimmed === "1" || trimmed.toLowerCase() === "true") return join(resolveDshHome(), "body-reports");
|
|
2254
|
+
return trimmed;
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Append one composition to `<dir>/<region>.jsonl`.
|
|
2258
|
+
*
|
|
2259
|
+
* One file per region is the whole point: the reporter captures a session, then
|
|
2260
|
+
* diffs `cn.jsonl`'s last line against `global.jsonl`'s. JSON Lines rather than
|
|
2261
|
+
* one file per request because a single turn makes many upstream calls (one per
|
|
2262
|
+
* tool step) and the LAST one is the interesting one.
|
|
2263
|
+
*
|
|
2264
|
+
* Read-modify-write is avoided on purpose — a long session would rewrite a
|
|
2265
|
+
* growing file on every step. The lock keeps two concurrent requests from
|
|
2266
|
+
* interleaving a line.
|
|
2267
|
+
*/
|
|
2268
|
+
async function appendBodyReport(directory, report) {
|
|
2269
|
+
const file = join(directory, `${report.region}.jsonl`);
|
|
2270
|
+
await mkdir(directory, { recursive: true });
|
|
2271
|
+
const line = `${JSON.stringify(report)}\n`;
|
|
2272
|
+
await withFileLock(file, () => appendFile(file, line, "utf8"));
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Record one outgoing body, if the report is enabled.
|
|
2276
|
+
*
|
|
2277
|
+
* NEVER THROWS and never awaits long: this is called on the request path, and a
|
|
2278
|
+
* chat request must not fail (or stall) because a diagnostic could not write a
|
|
2279
|
+
* file. A failure is reported through `onError` so the caller can log it.
|
|
2280
|
+
*/
|
|
2281
|
+
async function recordBodyReport(region, bodyJson, options = {}) {
|
|
2282
|
+
const directory = bodyReportDirectory(options.env);
|
|
2283
|
+
if (directory === void 0) return;
|
|
2284
|
+
try {
|
|
2285
|
+
const report = describeBody(bodyJson, region, options.nowMs ?? Date.now());
|
|
2286
|
+
if (report === void 0) return;
|
|
2287
|
+
await appendBodyReport(directory, report);
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
options.onError?.(error);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
//#endregion
|
|
2045
2293
|
//#region src/probe.ts
|
|
2046
2294
|
/**
|
|
2047
2295
|
* How long to bench a MODEL that answered 14003 with no stated time.
|
|
@@ -2504,7 +2752,22 @@ async function testAllAccounts(targets, modelId, deps, onRow) {
|
|
|
2504
2752
|
const rows = [];
|
|
2505
2753
|
for (const [index, target] of targets.entries()) {
|
|
2506
2754
|
if (index > 0) await (deps.wait ?? defaultWait)(400);
|
|
2507
|
-
const
|
|
2755
|
+
const wanted = typeof modelId === "function" ? modelId(target.accountId) : { modelId };
|
|
2756
|
+
if (wanted.modelId === void 0) {
|
|
2757
|
+
const row = {
|
|
2758
|
+
accountId: target.accountId,
|
|
2759
|
+
accountName: target.accountName,
|
|
2760
|
+
result: {
|
|
2761
|
+
modelId: wanted.staleModelId ?? "",
|
|
2762
|
+
outcome: "unavailable",
|
|
2763
|
+
message: wanted.staleModelId === void 0 ? "this region offers no model to test" : `the test model "${wanted.staleModelId}" is no longer offered in this region`
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
rows.push(row);
|
|
2767
|
+
onRow?.(row);
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
const row = await testOne(target, wanted.modelId, deps);
|
|
2508
2771
|
rows.push(row);
|
|
2509
2772
|
onRow?.(row);
|
|
2510
2773
|
}
|
|
@@ -2567,6 +2830,7 @@ function probeUpdatesOf(rows) {
|
|
|
2567
2830
|
outcome: row.result.outcome,
|
|
2568
2831
|
atMs,
|
|
2569
2832
|
source: "test-batch",
|
|
2833
|
+
modelId: row.result.modelId,
|
|
2570
2834
|
...row.accountName === "" ? {} : { accountName: row.accountName },
|
|
2571
2835
|
...row.result.retryAtMs === void 0 ? {} : { retryAtMs: row.result.retryAtMs },
|
|
2572
2836
|
...row.result.message === void 0 ? {} : { message: row.result.message }
|
|
@@ -2810,6 +3074,30 @@ function pickFreeModel(catalog) {
|
|
|
2810
3074
|
* That is the safe direction: no credits are spent while the roster is unknown,
|
|
2811
3075
|
* and the card offers to switch back to automatic.
|
|
2812
3076
|
*/
|
|
3077
|
+
/**
|
|
3078
|
+
* The model ONE account's test should target (issue #61).
|
|
3079
|
+
*
|
|
3080
|
+
* The region-wide `targetModelId` is one value, but `6004` is an ACCOUNT × MODEL
|
|
3081
|
+
* condition: the reporter measured two accounts where `hy4-preview-f` answered
|
|
3082
|
+
* `ok` and `step-5-preview` returned `429 / 6004` at the same moment. Testing
|
|
3083
|
+
* only the region's model therefore cannot answer "can this account serve the
|
|
3084
|
+
* model I actually use", which is the question the pool's test button is read as
|
|
3085
|
+
* answering.
|
|
3086
|
+
*
|
|
3087
|
+
* A per-account override is the smallest thing that makes the question
|
|
3088
|
+
* answerable, and it is additive: an account with no entry keeps using
|
|
3089
|
+
* `targetModelId`, so every existing profile behaves identically.
|
|
3090
|
+
*
|
|
3091
|
+
* Returns a PREFERENCE, not a resolved target — `resolveTargetModel` still
|
|
3092
|
+
* validates it against the catalog, so an override pointing at a model this
|
|
3093
|
+
* region no longer offers is reported as stale rather than silently swapped for
|
|
3094
|
+
* one the user did not choose.
|
|
3095
|
+
*/
|
|
3096
|
+
function targetModelPreference(preferences, accountId) {
|
|
3097
|
+
const perAccount = accountId === void 0 ? void 0 : preferences.targetModelIdByAccount?.[accountId];
|
|
3098
|
+
if (perAccount !== void 0 && perAccount !== "") return perAccount;
|
|
3099
|
+
return preferences.targetModelId;
|
|
3100
|
+
}
|
|
2813
3101
|
function resolveTargetModel(catalog, preferredId) {
|
|
2814
3102
|
if (preferredId !== void 0 && preferredId !== "") return catalog.some((model) => model.id === preferredId) ? {
|
|
2815
3103
|
modelId: preferredId,
|
|
@@ -3919,6 +4207,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3919
4207
|
deps.regionUsable?.(region, accounts.length > 0);
|
|
3920
4208
|
const enabled = deps.regionEnabled(region);
|
|
3921
4209
|
const showCreditsInMainUi = deps.showCreditsInMainUi?.(region) ?? true;
|
|
4210
|
+
const creditPosition = deps.creditPosition?.(region) ?? "composer";
|
|
3922
4211
|
const authStatus = await store.status();
|
|
3923
4212
|
const selectionExplicit = store.hasExplicitSelection();
|
|
3924
4213
|
if (authStatus.state !== "signed-out" && accounts.length === 0) return {
|
|
@@ -3940,6 +4229,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3940
4229
|
selectionExplicit,
|
|
3941
4230
|
enabled,
|
|
3942
4231
|
showCreditsInMainUi,
|
|
4232
|
+
creditPosition,
|
|
3943
4233
|
...await store.selectionLost() ? { selectionLost: true } : {},
|
|
3944
4234
|
...webAccounts.length === 0 ? await searchedPaths(store) : {}
|
|
3945
4235
|
};
|
|
@@ -3955,6 +4245,7 @@ async function workBuddyWebStatus(deps, region) {
|
|
|
3955
4245
|
selectionExplicit,
|
|
3956
4246
|
enabled,
|
|
3957
4247
|
showCreditsInMainUi,
|
|
4248
|
+
creditPosition,
|
|
3958
4249
|
accounts: accounts.map(toWebAccount),
|
|
3959
4250
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
|
|
3960
4251
|
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
@@ -4014,7 +4305,8 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4014
4305
|
const preferences = pool.preferences(region);
|
|
4015
4306
|
const members = await pool.members(region);
|
|
4016
4307
|
const ranked = rankPool(members, Date.now());
|
|
4017
|
-
const
|
|
4308
|
+
const catalog = pool.catalog?.(region) ?? [];
|
|
4309
|
+
const target = resolveTargetModel(catalog, preferences.targetModelId);
|
|
4018
4310
|
const currentAccountId = await pool.currentAccountId?.(region);
|
|
4019
4311
|
const byId = new Map(members.map((member) => [member.account.id, member]));
|
|
4020
4312
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
@@ -4053,8 +4345,17 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4053
4345
|
atMs: measured.probe.atMs,
|
|
4054
4346
|
...measured.probe.source === void 0 ? {} : { source: measured.probe.source },
|
|
4055
4347
|
...measured.probe.retryAtMs === void 0 ? {} : { retryAtMs: measured.probe.retryAtMs },
|
|
4056
|
-
...measured.probe.message === void 0 ? {} : { message: measured.probe.message }
|
|
4348
|
+
...measured.probe.message === void 0 ? {} : { message: measured.probe.message },
|
|
4349
|
+
...measured.probe.modelId === void 0 ? {} : { modelId: measured.probe.modelId }
|
|
4057
4350
|
} },
|
|
4351
|
+
...(() => {
|
|
4352
|
+
const perRow = resolveTargetModel(catalog, targetModelPreference(preferences, account.id));
|
|
4353
|
+
return {
|
|
4354
|
+
...perRow.modelId === void 0 ? {} : { testModelId: perRow.modelId },
|
|
4355
|
+
testModelSource: perRow.source,
|
|
4356
|
+
...perRow.staleModelId === void 0 ? {} : { staleTestModelId: perRow.staleModelId }
|
|
4357
|
+
};
|
|
4358
|
+
})(),
|
|
4058
4359
|
...row?.excludedBy === void 0 ? {} : { excludedBy: row.excludedBy },
|
|
4059
4360
|
current: account.id === currentAccountId,
|
|
4060
4361
|
member,
|
|
@@ -4067,6 +4368,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4067
4368
|
...target.modelId === void 0 ? {} : { targetModelId: target.modelId },
|
|
4068
4369
|
...target.staleModelId === void 0 ? {} : { staleTargetModelId: target.staleModelId },
|
|
4069
4370
|
targetModelSource: target.source,
|
|
4371
|
+
targetModelIdByAccount: preferences.targetModelIdByAccount ?? {},
|
|
4070
4372
|
memberAccountIds: preferences.memberAccountIds,
|
|
4071
4373
|
...preferences.probeInputTokens === void 0 ? {} : { probeInputTokens: preferences.probeInputTokens },
|
|
4072
4374
|
effectiveMemberAccountIds: effectiveMembersOf(accounts.map((account) => account.accountId), new Set(preferences.memberAccountIds)),
|
|
@@ -4310,7 +4612,20 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4310
4612
|
reason: "pool-unavailable",
|
|
4311
4613
|
error: "pool test unavailable"
|
|
4312
4614
|
});
|
|
4313
|
-
const
|
|
4615
|
+
const preferences = pool.preferences(region);
|
|
4616
|
+
const catalog = pool.catalog?.(region) ?? [];
|
|
4617
|
+
/**
|
|
4618
|
+
* The model ONE member is tested on (issue #61).
|
|
4619
|
+
*
|
|
4620
|
+
* Per account, because a test model is a per-account choice: `6004` is
|
|
4621
|
+
* an account × model condition, so a batch that tested every member on
|
|
4622
|
+
* one model would report "可用" for an account whose own model is the
|
|
4623
|
+
* one that is limited. Resolution still runs through
|
|
4624
|
+
* `resolveTargetModel`, so a stale override is reported, never
|
|
4625
|
+
* silently swapped.
|
|
4626
|
+
*/
|
|
4627
|
+
const targetFor = (accountId) => resolveTargetModel(catalog, targetModelPreference(preferences, accountId));
|
|
4628
|
+
const target = resolveTargetModel(catalog, targetModelPreference(preferences, void 0));
|
|
4314
4629
|
if (target.modelId === void 0) return json(res, 409, {
|
|
4315
4630
|
action: "test",
|
|
4316
4631
|
modelId: void 0,
|
|
@@ -4332,7 +4647,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4332
4647
|
modelId: target.modelId
|
|
4333
4648
|
});
|
|
4334
4649
|
try {
|
|
4335
|
-
await pool.test(region,
|
|
4650
|
+
await pool.test(region, targetFor, (row) => {
|
|
4336
4651
|
writeLine({ row });
|
|
4337
4652
|
});
|
|
4338
4653
|
writeLine({ done: true });
|
|
@@ -4670,6 +4985,7 @@ const modelConfig = z.object({
|
|
|
4670
4985
|
const poolConfig = z.object({
|
|
4671
4986
|
enabled: z.boolean().default(false).description("Whether this region's account pool is active (opt-in). While on, a failed chat request is retried against the other usable pool members before the error is reported."),
|
|
4672
4987
|
targetModelId: z.string().default("").description("Model id to test; empty means pick a zero-multiplier model from this region's catalog"),
|
|
4988
|
+
targetModelIdByAccount: z.dict(z.string()).default({}).description("Per-account test model, keyed by account id; an account with no entry uses targetModelId. Use this when an account must be tested on the model you actually chat with, because 6004 is an account-by-model condition."),
|
|
4673
4989
|
/**
|
|
4674
4990
|
* Sizing of the test probe, as a token count the user picks from a fixed set.
|
|
4675
4991
|
*
|
|
@@ -4700,6 +5016,7 @@ const regionStateConfig = z.object({
|
|
|
4700
5016
|
* which is reachable exactly where the feature renders.
|
|
4701
5017
|
*/
|
|
4702
5018
|
showCreditsInMainUi: z.boolean().default(true).description("Show this region's remaining credits at the DSH sidebar foot (opt-out; the line carries its own hide button)"),
|
|
5019
|
+
creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("Where the credit readout sits: \"composer\" = the composer tool row (default), \"stats\" = the ambient row below the composer card, next to the shell's own token pills. Choose \"stats\" if another plugin occupies the composer row."),
|
|
4703
5020
|
lastCatalog: z.array(modelConfig).default([]),
|
|
4704
5021
|
enabledModelIds: z.array(z.string()).default([]),
|
|
4705
5022
|
imageModelIds: z.array(z.string()).default([]),
|
|
@@ -4927,6 +5244,9 @@ function apply(ctx, config) {
|
|
|
4927
5244
|
* traffic once the provider is registered, which happens after this whole
|
|
4928
5245
|
* setup completes.
|
|
4929
5246
|
*/
|
|
5247
|
+
onRequestBody: (body) => {
|
|
5248
|
+
recordBodyReport(region, body, { onError: (error) => ctx.logger.warn("dsh-connect-workbuddy: body report failed", error) });
|
|
5249
|
+
},
|
|
4930
5250
|
prepareAccount: () => applyPoolSelection(region),
|
|
4931
5251
|
failoverAccount: (triedAccountIds) => failoverAccountFor(region, triedAccountIds),
|
|
4932
5252
|
onAccountFailure: (accountId, failure) => {
|
|
@@ -5089,6 +5409,13 @@ function apply(ctx, config) {
|
|
|
5089
5409
|
*/
|
|
5090
5410
|
showCreditsInMainUi: (region) => regionStateOf(current(), region).showCreditsInMainUi !== false,
|
|
5091
5411
|
/**
|
|
5412
|
+
* WHERE the readout sits, resolved here rather than read from the browser
|
|
5413
|
+
* settings mirror, for the same measured reason as the flag above: on the
|
|
5414
|
+
* affected 0.1.7 deployments that mirror never picks up this plugin's
|
|
5415
|
+
* writes, and a control reading it would keep showing the old seat.
|
|
5416
|
+
*/
|
|
5417
|
+
creditPosition: (region) => creditPositionOf(current(), region),
|
|
5418
|
+
/**
|
|
5092
5419
|
* One minimal request per named model, through the region's own credential.
|
|
5093
5420
|
*
|
|
5094
5421
|
* Resolved here rather than in the route so the browser half never touches a
|
|
@@ -5260,7 +5587,7 @@ function apply(ctx, config) {
|
|
|
5260
5587
|
* rather than pretending a test ran.
|
|
5261
5588
|
*/
|
|
5262
5589
|
testOne: async (region, accountId) => {
|
|
5263
|
-
const target = regionTargetModel(region);
|
|
5590
|
+
const target = regionTargetModel(region, accountId);
|
|
5264
5591
|
if (target === void 0) return void 0;
|
|
5265
5592
|
const store = stacks[region].store;
|
|
5266
5593
|
const credential = await store.credentialFor(accountId).catch(() => void 0);
|
|
@@ -5535,6 +5862,7 @@ function apply(ctx, config) {
|
|
|
5535
5862
|
outcome,
|
|
5536
5863
|
atMs: Date.now(),
|
|
5537
5864
|
source: "live-request",
|
|
5865
|
+
...failure.modelId === void 0 ? {} : { modelId: failure.modelId },
|
|
5538
5866
|
...retryAtMs === void 0 ? {} : { retryAtMs },
|
|
5539
5867
|
...message === "" ? {} : { message }
|
|
5540
5868
|
} });
|
|
@@ -5625,19 +5953,29 @@ function apply(ctx, config) {
|
|
|
5625
5953
|
/**
|
|
5626
5954
|
* The model a per-row test would use right now: the SAME resolution the
|
|
5627
5955
|
* batch route performs (`resolveTargetModel` over the region's displayed
|
|
5628
|
-
* catalog and the saved preference
|
|
5629
|
-
*
|
|
5956
|
+
* catalog and the saved preference — including a per-account override,
|
|
5957
|
+
* issue #61), so a row test and a batch test never measure different models.
|
|
5958
|
+
* Undefined mirrors the route's 409 states.
|
|
5630
5959
|
*/
|
|
5631
|
-
const regionTargetModel = (region) => {
|
|
5632
|
-
return resolveTargetModel(displayModels(current(), region), poolPreferencesOf(current(), region)
|
|
5960
|
+
const regionTargetModel = (region, accountId) => {
|
|
5961
|
+
return resolveTargetModel(displayModels(current(), region), targetModelPreference(poolPreferencesOf(current(), region), accountId)).modelId;
|
|
5633
5962
|
};
|
|
5634
5963
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
5964
|
+
/**
|
|
5965
|
+
* WHERE this region's credit readout sits (issue #62).
|
|
5966
|
+
*
|
|
5967
|
+
* A local reader rather than a schema default so a config written before the
|
|
5968
|
+
* setting existed reads as `composer` — the seat that was the only one, so an
|
|
5969
|
+
* upgrade moves nothing on screen.
|
|
5970
|
+
*/
|
|
5971
|
+
const creditPositionOf = (config, region) => regionStateOf(config, region).creditPosition === "stats" ? "stats" : "composer";
|
|
5635
5972
|
const poolPreferencesOf = (config, region) => {
|
|
5636
5973
|
const pool = regionStateOf(config, region).pool;
|
|
5637
5974
|
const probeInputTokens = pool?.probeInputTokens;
|
|
5638
5975
|
return {
|
|
5639
5976
|
enabled: pool?.enabled === true,
|
|
5640
5977
|
targetModelId: pool?.targetModelId ?? "",
|
|
5978
|
+
targetModelIdByAccount: { ...pool?.targetModelIdByAccount },
|
|
5641
5979
|
...probeInputTokens === void 0 ? {} : { probeInputTokens },
|
|
5642
5980
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
5643
5981
|
};
|
|
@@ -5812,4 +6150,4 @@ function apply(ctx, config) {
|
|
|
5812
6150
|
});
|
|
5813
6151
|
}
|
|
5814
6152
|
//#endregion
|
|
5815
|
-
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
|
6153
|
+
export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, modelIdOfBody, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.5.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|