dsh-connect-workbuddy 4.4.4 → 4.4.6
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 +61 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +216 -18
- package/lib/{host-heartbeat-BwbDWvzw.js → host-heartbeat-B2jrUDhg.js} +31 -6
- package/lib/index.d.ts +184 -33
- package/lib/index.js +379 -22
- 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-B2jrUDhg.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,8 @@ function probeUpdatesOf(rows) {
|
|
|
2567
2830
|
outcome: row.result.outcome,
|
|
2568
2831
|
atMs,
|
|
2569
2832
|
source: "test-batch",
|
|
2833
|
+
modelId: row.result.modelId,
|
|
2834
|
+
...row.accountName === "" ? {} : { accountName: row.accountName },
|
|
2570
2835
|
...row.result.retryAtMs === void 0 ? {} : { retryAtMs: row.result.retryAtMs },
|
|
2571
2836
|
...row.result.message === void 0 ? {} : { message: row.result.message }
|
|
2572
2837
|
};
|
|
@@ -2809,6 +3074,30 @@ function pickFreeModel(catalog) {
|
|
|
2809
3074
|
* That is the safe direction: no credits are spent while the roster is unknown,
|
|
2810
3075
|
* and the card offers to switch back to automatic.
|
|
2811
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
|
+
}
|
|
2812
3101
|
function resolveTargetModel(catalog, preferredId) {
|
|
2813
3102
|
if (preferredId !== void 0 && preferredId !== "") return catalog.some((model) => model.id === preferredId) ? {
|
|
2814
3103
|
modelId: preferredId,
|
|
@@ -3317,7 +3606,7 @@ const POOL_STORE_PREFIX = ".workbuddy-pool";
|
|
|
3317
3606
|
function isProbe(value) {
|
|
3318
3607
|
if (typeof value !== "object" || value === null) return false;
|
|
3319
3608
|
const candidate = value;
|
|
3320
|
-
return typeof candidate["outcome"] === "string" && typeof candidate["atMs"] === "number" && (candidate["retryAtMs"] === void 0 || typeof candidate["retryAtMs"] === "number") && (candidate["message"] === void 0 || typeof candidate["message"] === "string") && (candidate["source"] === void 0 || typeof candidate["source"] === "string");
|
|
3609
|
+
return typeof candidate["outcome"] === "string" && typeof candidate["atMs"] === "number" && (candidate["retryAtMs"] === void 0 || typeof candidate["retryAtMs"] === "number") && (candidate["message"] === void 0 || typeof candidate["message"] === "string") && (candidate["accountName"] === void 0 || typeof candidate["accountName"] === "string") && (candidate["source"] === void 0 || typeof candidate["source"] === "string");
|
|
3321
3610
|
}
|
|
3322
3611
|
/**
|
|
3323
3612
|
* One region's path for measured pool facts.
|
|
@@ -3366,7 +3655,8 @@ async function readPoolProbes(region) {
|
|
|
3366
3655
|
atMs: probe.atMs,
|
|
3367
3656
|
...knownProbeSource(probe.source) === void 0 ? {} : { source: knownProbeSource(probe.source) },
|
|
3368
3657
|
...probe.retryAtMs === void 0 ? {} : { retryAtMs: probe.retryAtMs },
|
|
3369
|
-
...probe.message === void 0 ? {} : { message: probe.message }
|
|
3658
|
+
...probe.message === void 0 ? {} : { message: probe.message },
|
|
3659
|
+
...probe.accountName === void 0 ? {} : { accountName: probe.accountName }
|
|
3370
3660
|
};
|
|
3371
3661
|
}
|
|
3372
3662
|
return probes;
|
|
@@ -3414,10 +3704,14 @@ function knownProbeSource(value) {
|
|
|
3414
3704
|
*/
|
|
3415
3705
|
function mergePoolProbe(existing, update) {
|
|
3416
3706
|
if (existing === void 0) return update;
|
|
3417
|
-
|
|
3418
|
-
if (existing.retryAtMs === void 0 || existing.retryAtMs <= update.atMs) return update;
|
|
3419
|
-
return {
|
|
3707
|
+
const merged = update.accountName === void 0 && existing.accountName !== void 0 ? {
|
|
3420
3708
|
...update,
|
|
3709
|
+
accountName: existing.accountName
|
|
3710
|
+
} : update;
|
|
3711
|
+
if (update.retryAtMs !== void 0) return merged;
|
|
3712
|
+
if (existing.retryAtMs === void 0 || existing.retryAtMs <= update.atMs) return merged;
|
|
3713
|
+
return {
|
|
3714
|
+
...merged,
|
|
3421
3715
|
retryAtMs: existing.retryAtMs
|
|
3422
3716
|
};
|
|
3423
3717
|
}
|
|
@@ -3794,7 +4088,6 @@ function toCredits(answer) {
|
|
|
3794
4088
|
remain: pack.remain,
|
|
3795
4089
|
size: pack.size,
|
|
3796
4090
|
monthly: pack.monthly,
|
|
3797
|
-
...pack.cycleRemain === void 0 ? {} : { cycleRemain: pack.cycleRemain },
|
|
3798
4091
|
...pack.refreshAtMs === void 0 ? {} : { cycleRefreshMs: pack.refreshAtMs },
|
|
3799
4092
|
...pack.expiresAtMs === void 0 ? {} : { expiresAtMs: pack.expiresAtMs }
|
|
3800
4093
|
})),
|
|
@@ -4009,20 +4302,32 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4009
4302
|
const preferences = pool.preferences(region);
|
|
4010
4303
|
const members = await pool.members(region);
|
|
4011
4304
|
const ranked = rankPool(members, Date.now());
|
|
4012
|
-
const
|
|
4305
|
+
const catalog = pool.catalog?.(region) ?? [];
|
|
4306
|
+
const target = resolveTargetModel(catalog, preferences.targetModelId);
|
|
4013
4307
|
const currentAccountId = await pool.currentAccountId?.(region);
|
|
4014
4308
|
const byId = new Map(members.map((member) => [member.account.id, member]));
|
|
4015
4309
|
const rankedIds = new Set(ranked.map((row) => row.account.id));
|
|
4016
4310
|
const others = await pool.otherAccounts?.(region) ?? [];
|
|
4017
4311
|
const checkedIn = await pool.checkedInToday?.(region).catch(() => ({})) ?? {};
|
|
4018
|
-
const
|
|
4312
|
+
const rows = [...ranked.map((row) => ({
|
|
4019
4313
|
account: row.account,
|
|
4020
4314
|
member: true,
|
|
4021
4315
|
ranked: row
|
|
4022
4316
|
})), ...others.filter((account) => !rankedIds.has(account.id)).map((account) => ({
|
|
4023
4317
|
account,
|
|
4024
4318
|
member: false
|
|
4025
|
-
}))]
|
|
4319
|
+
}))];
|
|
4320
|
+
/**
|
|
4321
|
+
* Names for saved members the roster above does NOT contain (issue #60).
|
|
4322
|
+
*
|
|
4323
|
+
* Read for the SAVED ids only, and only from the measurement record: those
|
|
4324
|
+
* are exactly the accounts the card cannot name from `accounts`, because
|
|
4325
|
+
* nothing about them is left on this machine except the fact that they were
|
|
4326
|
+
* once measured. Best-effort — a failed read names nothing, and the card
|
|
4327
|
+
* falls back to an id prefix rather than inventing a label.
|
|
4328
|
+
*/
|
|
4329
|
+
const memberNames = await pool.memberNames?.(region).catch(() => void 0) ?? void 0;
|
|
4330
|
+
const accounts = rows.map(({ account, member, ranked: row }) => {
|
|
4026
4331
|
const measured = byId.get(account.id);
|
|
4027
4332
|
return {
|
|
4028
4333
|
accountId: account.id,
|
|
@@ -4037,8 +4342,17 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4037
4342
|
atMs: measured.probe.atMs,
|
|
4038
4343
|
...measured.probe.source === void 0 ? {} : { source: measured.probe.source },
|
|
4039
4344
|
...measured.probe.retryAtMs === void 0 ? {} : { retryAtMs: measured.probe.retryAtMs },
|
|
4040
|
-
...measured.probe.message === void 0 ? {} : { message: measured.probe.message }
|
|
4345
|
+
...measured.probe.message === void 0 ? {} : { message: measured.probe.message },
|
|
4346
|
+
...measured.probe.modelId === void 0 ? {} : { modelId: measured.probe.modelId }
|
|
4041
4347
|
} },
|
|
4348
|
+
...(() => {
|
|
4349
|
+
const perRow = resolveTargetModel(catalog, targetModelPreference(preferences, account.id));
|
|
4350
|
+
return {
|
|
4351
|
+
...perRow.modelId === void 0 ? {} : { testModelId: perRow.modelId },
|
|
4352
|
+
testModelSource: perRow.source,
|
|
4353
|
+
...perRow.staleModelId === void 0 ? {} : { staleTestModelId: perRow.staleModelId }
|
|
4354
|
+
};
|
|
4355
|
+
})(),
|
|
4042
4356
|
...row?.excludedBy === void 0 ? {} : { excludedBy: row.excludedBy },
|
|
4043
4357
|
current: account.id === currentAccountId,
|
|
4044
4358
|
member,
|
|
@@ -4051,9 +4365,11 @@ async function workBuddyWebPool(deps, region) {
|
|
|
4051
4365
|
...target.modelId === void 0 ? {} : { targetModelId: target.modelId },
|
|
4052
4366
|
...target.staleModelId === void 0 ? {} : { staleTargetModelId: target.staleModelId },
|
|
4053
4367
|
targetModelSource: target.source,
|
|
4368
|
+
targetModelIdByAccount: preferences.targetModelIdByAccount ?? {},
|
|
4054
4369
|
memberAccountIds: preferences.memberAccountIds,
|
|
4055
4370
|
...preferences.probeInputTokens === void 0 ? {} : { probeInputTokens: preferences.probeInputTokens },
|
|
4056
4371
|
effectiveMemberAccountIds: effectiveMembersOf(accounts.map((account) => account.accountId), new Set(preferences.memberAccountIds)),
|
|
4372
|
+
...memberNames === void 0 ? {} : { memberNames },
|
|
4057
4373
|
accounts,
|
|
4058
4374
|
catalog: deps.displayModels(region).map((model) => ({
|
|
4059
4375
|
id: model.id,
|
|
@@ -4293,7 +4609,20 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4293
4609
|
reason: "pool-unavailable",
|
|
4294
4610
|
error: "pool test unavailable"
|
|
4295
4611
|
});
|
|
4296
|
-
const
|
|
4612
|
+
const preferences = pool.preferences(region);
|
|
4613
|
+
const catalog = pool.catalog?.(region) ?? [];
|
|
4614
|
+
/**
|
|
4615
|
+
* The model ONE member is tested on (issue #61).
|
|
4616
|
+
*
|
|
4617
|
+
* Per account, because a test model is a per-account choice: `6004` is
|
|
4618
|
+
* an account × model condition, so a batch that tested every member on
|
|
4619
|
+
* one model would report "可用" for an account whose own model is the
|
|
4620
|
+
* one that is limited. Resolution still runs through
|
|
4621
|
+
* `resolveTargetModel`, so a stale override is reported, never
|
|
4622
|
+
* silently swapped.
|
|
4623
|
+
*/
|
|
4624
|
+
const targetFor = (accountId) => resolveTargetModel(catalog, targetModelPreference(preferences, accountId));
|
|
4625
|
+
const target = resolveTargetModel(catalog, targetModelPreference(preferences, void 0));
|
|
4297
4626
|
if (target.modelId === void 0) return json(res, 409, {
|
|
4298
4627
|
action: "test",
|
|
4299
4628
|
modelId: void 0,
|
|
@@ -4315,7 +4644,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
4315
4644
|
modelId: target.modelId
|
|
4316
4645
|
});
|
|
4317
4646
|
try {
|
|
4318
|
-
await pool.test(region,
|
|
4647
|
+
await pool.test(region, targetFor, (row) => {
|
|
4319
4648
|
writeLine({ row });
|
|
4320
4649
|
});
|
|
4321
4650
|
writeLine({ done: true });
|
|
@@ -4653,6 +4982,7 @@ const modelConfig = z.object({
|
|
|
4653
4982
|
const poolConfig = z.object({
|
|
4654
4983
|
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."),
|
|
4655
4984
|
targetModelId: z.string().default("").description("Model id to test; empty means pick a zero-multiplier model from this region's catalog"),
|
|
4985
|
+
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."),
|
|
4656
4986
|
/**
|
|
4657
4987
|
* Sizing of the test probe, as a token count the user picks from a fixed set.
|
|
4658
4988
|
*
|
|
@@ -4910,6 +5240,9 @@ function apply(ctx, config) {
|
|
|
4910
5240
|
* traffic once the provider is registered, which happens after this whole
|
|
4911
5241
|
* setup completes.
|
|
4912
5242
|
*/
|
|
5243
|
+
onRequestBody: (body) => {
|
|
5244
|
+
recordBodyReport(region, body, { onError: (error) => ctx.logger.warn("dsh-connect-workbuddy: body report failed", error) });
|
|
5245
|
+
},
|
|
4913
5246
|
prepareAccount: () => applyPoolSelection(region),
|
|
4914
5247
|
failoverAccount: (triedAccountIds) => failoverAccountFor(region, triedAccountIds),
|
|
4915
5248
|
onAccountFailure: (accountId, failure) => {
|
|
@@ -5140,6 +5473,27 @@ function apply(ctx, config) {
|
|
|
5140
5473
|
return effectiveMembersOf(saved, new Set(accounts.map((account) => account.id)));
|
|
5141
5474
|
},
|
|
5142
5475
|
/**
|
|
5476
|
+
* Last measured display name per saved member id (issue #60).
|
|
5477
|
+
*
|
|
5478
|
+
* Answers the one question the card cannot answer itself: a saved member
|
|
5479
|
+
* whose sign-in is gone has no row in the roster, so the name the pool
|
|
5480
|
+
* once saw is the only thing that can identify it. Read from the runtime
|
|
5481
|
+
* probe store — no upstream call, and no credential touched.
|
|
5482
|
+
*
|
|
5483
|
+
* Best-effort, and deliberately NOT filtered to ghosts here: the route
|
|
5484
|
+
* knows which ids are unlisted (it built the roster) and names only
|
|
5485
|
+
* those, so this stays a plain lookup with one meaning.
|
|
5486
|
+
*/
|
|
5487
|
+
memberNames: async (region) => {
|
|
5488
|
+
const probes = await readPoolProbes(region);
|
|
5489
|
+
const names = {};
|
|
5490
|
+
for (const [accountId, probe] of Object.entries(probes)) {
|
|
5491
|
+
if (probe.accountName === void 0 || probe.accountName === "") continue;
|
|
5492
|
+
names[accountId] = probe.accountName;
|
|
5493
|
+
}
|
|
5494
|
+
return names;
|
|
5495
|
+
},
|
|
5496
|
+
/**
|
|
5143
5497
|
* Local sign-ins the pool does NOT cover.
|
|
5144
5498
|
*
|
|
5145
5499
|
* Sent so the card can offer them to check in. Membership is an explicit
|
|
@@ -5222,7 +5576,7 @@ function apply(ctx, config) {
|
|
|
5222
5576
|
* rather than pretending a test ran.
|
|
5223
5577
|
*/
|
|
5224
5578
|
testOne: async (region, accountId) => {
|
|
5225
|
-
const target = regionTargetModel(region);
|
|
5579
|
+
const target = regionTargetModel(region, accountId);
|
|
5226
5580
|
if (target === void 0) return void 0;
|
|
5227
5581
|
const store = stacks[region].store;
|
|
5228
5582
|
const credential = await store.credentialFor(accountId).catch(() => void 0);
|
|
@@ -5497,6 +5851,7 @@ function apply(ctx, config) {
|
|
|
5497
5851
|
outcome,
|
|
5498
5852
|
atMs: Date.now(),
|
|
5499
5853
|
source: "live-request",
|
|
5854
|
+
...failure.modelId === void 0 ? {} : { modelId: failure.modelId },
|
|
5500
5855
|
...retryAtMs === void 0 ? {} : { retryAtMs },
|
|
5501
5856
|
...message === "" ? {} : { message }
|
|
5502
5857
|
} });
|
|
@@ -5587,11 +5942,12 @@ function apply(ctx, config) {
|
|
|
5587
5942
|
/**
|
|
5588
5943
|
* The model a per-row test would use right now: the SAME resolution the
|
|
5589
5944
|
* batch route performs (`resolveTargetModel` over the region's displayed
|
|
5590
|
-
* catalog and the saved preference
|
|
5591
|
-
*
|
|
5945
|
+
* catalog and the saved preference — including a per-account override,
|
|
5946
|
+
* issue #61), so a row test and a batch test never measure different models.
|
|
5947
|
+
* Undefined mirrors the route's 409 states.
|
|
5592
5948
|
*/
|
|
5593
|
-
const regionTargetModel = (region) => {
|
|
5594
|
-
return resolveTargetModel(displayModels(current(), region), poolPreferencesOf(current(), region)
|
|
5949
|
+
const regionTargetModel = (region, accountId) => {
|
|
5950
|
+
return resolveTargetModel(displayModels(current(), region), targetModelPreference(poolPreferencesOf(current(), region), accountId)).modelId;
|
|
5595
5951
|
};
|
|
5596
5952
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
5597
5953
|
const poolPreferencesOf = (config, region) => {
|
|
@@ -5600,6 +5956,7 @@ function apply(ctx, config) {
|
|
|
5600
5956
|
return {
|
|
5601
5957
|
enabled: pool?.enabled === true,
|
|
5602
5958
|
targetModelId: pool?.targetModelId ?? "",
|
|
5959
|
+
targetModelIdByAccount: { ...pool?.targetModelIdByAccount },
|
|
5603
5960
|
...probeInputTokens === void 0 ? {} : { probeInputTokens },
|
|
5604
5961
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
5605
5962
|
};
|
|
@@ -5774,4 +6131,4 @@ function apply(ctx, config) {
|
|
|
5774
6131
|
});
|
|
5775
6132
|
}
|
|
5776
6133
|
//#endregion
|
|
5777
|
-
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 };
|
|
6134
|
+
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.4.
|
|
5
|
+
"version": "4.4.6",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|