dsh-connect-workbuddy 3.4.2 → 3.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/WINDOWS.md +6 -0
- package/lib/bin.js +12 -4
- package/lib/client.js +128 -7
- package/lib/{host-heartbeat-Sq5JwluI.js → host-heartbeat-DTuwptkM.js} +245 -6
- package/lib/index.d.ts +130 -12
- package/lib/index.js +215 -15
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as openEncryptedField, A as CREDENTIAL_REJECTED_CODE, B as prepareChatBody, C as defaultDesktopAuthPath, D as parseWorkBuddyAuth, E as legacyWorkbuddyOwnAuthPath, F as declaredTools, G as deriveAtRestKeyId, H as WORKBUDDY_APP_EXECUTABLE_ENV, I as isCredentialRejectedError, K as fetchAtRestKeyPayload, L as parseCreditMultiplier, M as WorkBuddyCredentialRejectedError, N as WorkBuddyUpstreamClient, O as workbuddyAccountId, P as classifyUpstreamError, Q as macosNestedAppBundles, R as parseReasoning, S as defaultDesktopAuthDirs, T as isEncryptedCredentialError, U as clearAtRestKeyCache, V as regionOf, W as deriveAtRestKey, X as isWorkbuddyBundle, Y as isEncryptedFieldWrapper, Z as macosBundleExecutable, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, b as authFileName, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as readAtRestKey, f as WorkBuddyCatalog, g as WORKBUDDY_AUTH_FILENAME, h as ENCRYPTED_CREDENTIAL_CODE, i as processStartTimeMs, j as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, m as fallbackModelsFor, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as deriveCatalog, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as workbuddyAppExecutableCandidates, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, w as hasEncryptedCredentialFields, x as defaultDesktopAuthCandidates, y as WorkBuddyEncryptedCredentialError, z as parseUpstreamModel } from "./host-heartbeat-DTuwptkM.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -1180,6 +1180,7 @@ const KIND_STATUS = {
|
|
|
1180
1180
|
hard_credit: 402,
|
|
1181
1181
|
soft_rate: 429,
|
|
1182
1182
|
session_dead: 401,
|
|
1183
|
+
policy_reject: 403,
|
|
1183
1184
|
not_found: 502,
|
|
1184
1185
|
server: 502,
|
|
1185
1186
|
client: 400
|
|
@@ -1212,6 +1213,16 @@ const KIND_STATUS = {
|
|
|
1212
1213
|
* Every other kind already lands outside the retry set by its status alone
|
|
1213
1214
|
* (`hard_credit` 402 → quota, `session_dead` 401 → auth, 502/400 → other), so
|
|
1214
1215
|
* none of them is listed: a translation nobody needs is a lie waiting to drift.
|
|
1216
|
+
*
|
|
1217
|
+
* `policy_reject` is deliberately NOT listed either, for a sharper reason than
|
|
1218
|
+
* the others: the obvious candidate translation would be the upstream's own
|
|
1219
|
+
* numeric code (11140), but that is not a class the host knows, so it would take
|
|
1220
|
+
* the host's generic path and add nothing over `policy_reject`. It needs no
|
|
1221
|
+
* translation to stay outside the retry set (403), and it must NOT be translated
|
|
1222
|
+
* INTO the set — a content-policy refusal is deterministic, so retrying it in
|
|
1223
|
+
* place would spend the retry budget on a request the server will refuse again,
|
|
1224
|
+
* and cross-provider failover cannot help a policy decision either. The code the
|
|
1225
|
+
* host receives therefore names the class honestly.
|
|
1215
1226
|
*/
|
|
1216
1227
|
const KIND_HOST_CODE = { soft_rate: "quota_exceeded" };
|
|
1217
1228
|
/**
|
|
@@ -1331,10 +1342,49 @@ var RecoveryStream = class {
|
|
|
1331
1342
|
*/
|
|
1332
1343
|
nativeCallsSeen = false;
|
|
1333
1344
|
buffer;
|
|
1334
|
-
|
|
1345
|
+
/**
|
|
1346
|
+
* A notice to place at the head of the answer, once, before any prose.
|
|
1347
|
+
*
|
|
1348
|
+
* Set when the request reached this stream only after the pool failed over, so
|
|
1349
|
+
* the user learns WHY the answer came from a different account. Without it a
|
|
1350
|
+
* failover is invisible: the reply looks like it came from the account they
|
|
1351
|
+
* picked, and the only trace is a `logger.warn` they never see. The notice is
|
|
1352
|
+
* emitted ahead of the model's own text so it cannot be mistaken for part of
|
|
1353
|
+
* the answer, and only once — a retry that never delivers text must not leave
|
|
1354
|
+
* a stray line behind.
|
|
1355
|
+
*/
|
|
1356
|
+
notice;
|
|
1357
|
+
constructor(res, gate, logger, notice) {
|
|
1335
1358
|
this.res = res;
|
|
1336
1359
|
this.logger = logger;
|
|
1337
1360
|
this.buffer = gate === void 0 ? void 0 : new DsmlStreamBuffer(gate);
|
|
1361
|
+
this.notice = notice;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Emit the pending failover notice, if any, and forget it.
|
|
1365
|
+
*
|
|
1366
|
+
* Called immediately before the first content reaches the client. Placed
|
|
1367
|
+
* here rather than in the constructor because a stream whose write fails, or
|
|
1368
|
+
* whose attempt is discarded in favour of a retry, must not have already told
|
|
1369
|
+
* the user about a switch it never completed.
|
|
1370
|
+
*/
|
|
1371
|
+
flushNotice() {
|
|
1372
|
+
const notice = this.notice;
|
|
1373
|
+
if (notice === void 0) return;
|
|
1374
|
+
this.notice = void 0;
|
|
1375
|
+
if (!this.res.writable) return;
|
|
1376
|
+
const chunk = { choices: [{
|
|
1377
|
+
index: 0,
|
|
1378
|
+
delta: {},
|
|
1379
|
+
finish_reason: ""
|
|
1380
|
+
}] };
|
|
1381
|
+
const choiceRecord = {
|
|
1382
|
+
index: 0,
|
|
1383
|
+
delta: {},
|
|
1384
|
+
finish_reason: ""
|
|
1385
|
+
};
|
|
1386
|
+
this.windowOpened = true;
|
|
1387
|
+
this.res.write(contentFrame(chunk, choiceRecord, notice));
|
|
1338
1388
|
}
|
|
1339
1389
|
/** True when a call or real prose has reached the client. */
|
|
1340
1390
|
get delivered() {
|
|
@@ -1433,6 +1483,7 @@ var RecoveryStream = class {
|
|
|
1433
1483
|
if (Array.isArray(nativeCalls) && nativeCalls.length > 0) {
|
|
1434
1484
|
this.nativeCallsSeen = true;
|
|
1435
1485
|
this.windowOpened = true;
|
|
1486
|
+
this.flushNotice();
|
|
1436
1487
|
this.send(frame, false);
|
|
1437
1488
|
this.wroteContent = true;
|
|
1438
1489
|
this.closeWindow();
|
|
@@ -1447,6 +1498,7 @@ var RecoveryStream = class {
|
|
|
1447
1498
|
}
|
|
1448
1499
|
if (this.buffer === void 0 || this.nativeCallsSeen) {
|
|
1449
1500
|
this.windowOpened = true;
|
|
1501
|
+
this.flushNotice();
|
|
1450
1502
|
this.closeWindow();
|
|
1451
1503
|
this.res.write(frame);
|
|
1452
1504
|
this.wroteContent = true;
|
|
@@ -1455,6 +1507,7 @@ var RecoveryStream = class {
|
|
|
1455
1507
|
this.windowOpened = true;
|
|
1456
1508
|
const outcome = this.buffer.add(content);
|
|
1457
1509
|
if (outcome.calls !== void 0 && outcome.calls.length > 0) {
|
|
1510
|
+
this.flushNotice();
|
|
1458
1511
|
this.send(callsFrame(chunk, choiceRecord, outcome.calls), false);
|
|
1459
1512
|
this.wroteContent = true;
|
|
1460
1513
|
this.closeWindow();
|
|
@@ -1467,6 +1520,7 @@ var RecoveryStream = class {
|
|
|
1467
1520
|
return;
|
|
1468
1521
|
}
|
|
1469
1522
|
if (outcome.prose) {
|
|
1523
|
+
this.flushNotice();
|
|
1470
1524
|
this.send(contentFrame(chunk, choiceRecord, outcome.text), false);
|
|
1471
1525
|
this.wroteContent = true;
|
|
1472
1526
|
this.closeWindow();
|
|
@@ -1510,6 +1564,7 @@ var RecoveryStream = class {
|
|
|
1510
1564
|
this.windowOpened = true;
|
|
1511
1565
|
const frame = contentFrame(chunk, choiceRecord, text);
|
|
1512
1566
|
if (text.trim() !== "") {
|
|
1567
|
+
this.flushNotice();
|
|
1513
1568
|
this.send(frame, false);
|
|
1514
1569
|
this.wroteContent = true;
|
|
1515
1570
|
this.closeWindow();
|
|
@@ -1587,6 +1642,55 @@ function callsFrame(chunk, choice, calls) {
|
|
|
1587
1642
|
};
|
|
1588
1643
|
return `data: ${JSON.stringify(next)}\n\n`;
|
|
1589
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* How to name a pool account in a sentence the USER reads.
|
|
1647
|
+
*
|
|
1648
|
+
* Prefers the phone number (`uin`) because that is what the pool table shows and
|
|
1649
|
+
* what a person recognises; the nickname and the opaque account id are fallbacks
|
|
1650
|
+
* so this can never render an empty name. Deliberately NOT the hashed account id
|
|
1651
|
+
* on its own: a 24-character hex digest identifies an account to the plugin, but
|
|
1652
|
+
* to the person reading the answer it identifies nothing.
|
|
1653
|
+
*/
|
|
1654
|
+
function describeAccount(credential) {
|
|
1655
|
+
if (credential.uin !== void 0 && credential.uin !== "") return credential.uin;
|
|
1656
|
+
if (credential.nickname !== void 0 && credential.nickname !== "") return credential.nickname;
|
|
1657
|
+
return workbuddyAccountId(credential);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* One short phrase for why an account was abandoned, for the in-reply notice.
|
|
1661
|
+
*
|
|
1662
|
+
* Kept to the distinction the user can act on rather than the internal kind
|
|
1663
|
+
* name: "rate limited" says wait or add an account, "rejected" says the sign-in
|
|
1664
|
+
* is the problem. The numeric kind would only send them to the log.
|
|
1665
|
+
*/
|
|
1666
|
+
function failoverReasonText(failure) {
|
|
1667
|
+
if (failure.ok) return "unusable";
|
|
1668
|
+
switch (failure.kind) {
|
|
1669
|
+
case "soft_rate":
|
|
1670
|
+
case "hard_credit": return "rate limited";
|
|
1671
|
+
case "session_dead": return "signed out";
|
|
1672
|
+
case "policy_reject": return "rejected by policy";
|
|
1673
|
+
case "not_found": return "model unavailable";
|
|
1674
|
+
case "server": return "erroring upstream";
|
|
1675
|
+
default: return `unusable (http ${failure.status})`;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* The line prepended to an answer that came from a failover account.
|
|
1680
|
+
*
|
|
1681
|
+
* Block-quoted and bracketed so it reads as machinery rather than as the model's
|
|
1682
|
+
* own words, and so a user skimming for the answer can skip it. It states the
|
|
1683
|
+
* reason AND the account left behind, because those are the two facts that make
|
|
1684
|
+
* an unexpected reply explicable.
|
|
1685
|
+
*
|
|
1686
|
+
* The wording avoids "switched TO <account>" on purpose: the account that took
|
|
1687
|
+
* over is the one the user is about to keep talking to, and naming it invites
|
|
1688
|
+
* the reading that they must now manage it. What they need is to know the one
|
|
1689
|
+
* they chose did not answer, and why.
|
|
1690
|
+
*/
|
|
1691
|
+
function failoverNoticeText(from, reason) {
|
|
1692
|
+
return `\n\n> [dsh-connect-workbuddy] 「${from}」was ${reason} — this answer came from another account\n\n`;
|
|
1693
|
+
}
|
|
1590
1694
|
/** One frame carrying replacement text for a frame whose content was consumed. */
|
|
1591
1695
|
function contentFrame(chunk, choice, content) {
|
|
1592
1696
|
const delta = {
|
|
@@ -1715,6 +1819,15 @@ function createWorkBuddyShim(options) {
|
|
|
1715
1819
|
let result = await client.chatStream(credential, prepared, controller.signal);
|
|
1716
1820
|
const triedAccountIds = [];
|
|
1717
1821
|
/**
|
|
1822
|
+
* The account the request STARTED on, and the reason it was abandoned.
|
|
1823
|
+
*
|
|
1824
|
+
* `??=` on the first switch only: if three accounts fail in a row the user
|
|
1825
|
+
* cares that the answer did not come from the one they picked, not with a
|
|
1826
|
+
* blow-by-blow list of every candidate that was also rate limited.
|
|
1827
|
+
*/
|
|
1828
|
+
let switchedFrom;
|
|
1829
|
+
let switchedBecause;
|
|
1830
|
+
/**
|
|
1718
1831
|
* Tell the plugin about a failed attempt so it can store the measurement.
|
|
1719
1832
|
*
|
|
1720
1833
|
* Every failed attempt is reported exactly once, INCLUDING the last one — an
|
|
@@ -1769,6 +1882,8 @@ function createWorkBuddyShim(options) {
|
|
|
1769
1882
|
});
|
|
1770
1883
|
if (controller.signal.aborted) break;
|
|
1771
1884
|
logger?.warn(`dsh-connect-workbuddy: retrying chat on another pool account after ${result.kind} (http ${result.status})`);
|
|
1885
|
+
switchedFrom ??= describeAccount(credential);
|
|
1886
|
+
switchedBecause ??= failoverReasonText(result);
|
|
1772
1887
|
credential = next;
|
|
1773
1888
|
result = await client.chatStream(credential, prepared, controller.signal);
|
|
1774
1889
|
}
|
|
@@ -1777,6 +1892,16 @@ function createWorkBuddyShim(options) {
|
|
|
1777
1892
|
if (!result.ok) {
|
|
1778
1893
|
const attempts = triedAccountIds.length;
|
|
1779
1894
|
const note = attempts > 1 ? ` (after trying ${attempts} accounts)` : "";
|
|
1895
|
+
if (result.kind === "policy_reject") {
|
|
1896
|
+
const detail = result.detail;
|
|
1897
|
+
const lead = detail?.displayMsg ?? result.message.slice(0, 200);
|
|
1898
|
+
const meta = ["服务端策略拒绝"];
|
|
1899
|
+
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
1900
|
+
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
1901
|
+
const message = `${lead}(${meta.join(",")})${note}。该请求被 WorkBuddy 服务端策略拒绝,重新登录不会解决;可在 WorkBuddy 桌面端用同一账号验证,或切换区域/账号后重试`;
|
|
1902
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1780
1905
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy upstream ${result.kind} (http ${result.status})${note}: ${result.message.slice(0, 400)}`, hostErrorCode(result.kind));
|
|
1781
1906
|
return;
|
|
1782
1907
|
}
|
|
@@ -1792,7 +1917,8 @@ function createWorkBuddyShim(options) {
|
|
|
1792
1917
|
requiredParameters: declared.requiredParameters,
|
|
1793
1918
|
...declared.pinnedToolName === void 0 ? {} : { pinnedToolName: declared.pinnedToolName }
|
|
1794
1919
|
};
|
|
1795
|
-
const
|
|
1920
|
+
const notice = switchedFrom === void 0 || switchedBecause === void 0 ? void 0 : failoverNoticeText(switchedFrom, switchedBecause);
|
|
1921
|
+
const writer = new RecoveryStream(res, gate, logger, notice);
|
|
1796
1922
|
await writer.consume(result.response.body);
|
|
1797
1923
|
if (gate !== void 0 && !writer.delivered && writer.residueSeen && !controller.signal.aborted) {
|
|
1798
1924
|
logger?.warn("dsh-connect-workbuddy: the turn produced DSML markup only; retrying it once");
|
|
@@ -1847,7 +1973,7 @@ const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a single w
|
|
|
1847
1973
|
* Output cap for a probe. One token is enough to prove the model answers.
|
|
1848
1974
|
*
|
|
1849
1975
|
* The probe's cost comes from its INPUT, not its output (see
|
|
1850
|
-
* {@link
|
|
1976
|
+
* {@link DEFAULT_PROBE_INPUT_TOKENS}), so paying for a long completion would add money
|
|
1851
1977
|
* without adding signal.
|
|
1852
1978
|
*/
|
|
1853
1979
|
const PROBE_MAX_TOKENS = 1;
|
|
@@ -1874,8 +2000,44 @@ const PROBE_MAX_TOKENS = 1;
|
|
|
1874
2000
|
* A probe that sent a handful of tokens answered "usable" while every real
|
|
1875
2001
|
* request in a long conversation was refused — the probe was asking too small a
|
|
1876
2002
|
* question. 25k is sized to be a realistic long-conversation payload.
|
|
2003
|
+
*
|
|
2004
|
+
* Renamed from `PROBE_INPUT_TOKENS` to say what it now IS: the DEFAULT, not the
|
|
2005
|
+
* only possible size. The size is a property of the user's own traffic (see
|
|
2006
|
+
* {@link probeRequestBody}), so it became a setting; this constant is what an
|
|
2007
|
+
* unset setting, an older profile, and every existing test mean by "a probe".
|
|
2008
|
+
*/
|
|
2009
|
+
const DEFAULT_PROBE_INPUT_TOKENS = 25e3;
|
|
2010
|
+
/**
|
|
2011
|
+
* The probe sizes the card offers, in tokens.
|
|
2012
|
+
*
|
|
2013
|
+
* A closed set, each step a meaningful position relative to the measured 20k~30k
|
|
2014
|
+
* threshold: below it (10k/20k) proves an account can serve SHORT requests and
|
|
2015
|
+
* is the cheap choice; at it (30k) is the size that finally covers the whole
|
|
2016
|
+
* measured window; above it (50k/100k) is for conversations that routinely run
|
|
2017
|
+
* long, where a smaller probe would report "usable" for an account that refuses
|
|
2018
|
+
* the very next message.
|
|
2019
|
+
*
|
|
2020
|
+
* Exported so the schema's description, the card's dropdown, and the coercion
|
|
2021
|
+
* below cannot drift apart into three different menus.
|
|
1877
2022
|
*/
|
|
1878
|
-
const
|
|
2023
|
+
const PROBE_INPUT_TOKEN_CHOICES = [
|
|
2024
|
+
1e4,
|
|
2025
|
+
2e4,
|
|
2026
|
+
3e4,
|
|
2027
|
+
5e4,
|
|
2028
|
+
1e5
|
|
2029
|
+
];
|
|
2030
|
+
/**
|
|
2031
|
+
* A requested probe size reduced to a size this build will actually send.
|
|
2032
|
+
*
|
|
2033
|
+
* Anything outside {@link PROBE_INPUT_TOKEN_CHOICES} — `undefined` from an older
|
|
2034
|
+
* profile, `0` from an unset field, a hand-edited odd number — becomes the
|
|
2035
|
+
* measured default. Coercing rather than rejecting keeps a hand-edited profile
|
|
2036
|
+
* working instead of turning one odd value into a failed test batch.
|
|
2037
|
+
*/
|
|
2038
|
+
function resolveProbeInputTokens(requested) {
|
|
2039
|
+
return requested !== void 0 && PROBE_INPUT_TOKEN_CHOICES.includes(requested) ? requested : DEFAULT_PROBE_INPUT_TOKENS;
|
|
2040
|
+
}
|
|
1879
2041
|
/**
|
|
1880
2042
|
* Characters per token for the filler text, measured.
|
|
1881
2043
|
*
|
|
@@ -1894,11 +2056,14 @@ const FILLER_LINE = "The quick brown fox jumps over the lazy dog. ";
|
|
|
1894
2056
|
* `prepareChatBody` enforces that too, but sending it explicitly keeps this
|
|
1895
2057
|
* function's output valid on its own.
|
|
1896
2058
|
*
|
|
1897
|
-
* Measured on the built artifact: 112,713 bytes of body, and
|
|
1898
|
-
* counted `prompt_tokens: 25023` at a cost of `credit: 0.72`.
|
|
2059
|
+
* Measured on the built artifact at the DEFAULT size: 112,713 bytes of body, and
|
|
2060
|
+
* the upstream counted `prompt_tokens: 25023` at a cost of `credit: 0.72`. The
|
|
2061
|
+
* body bytes and the credit both scale with the size asked for, which is why the
|
|
2062
|
+
* size is a user setting: a larger probe answers more truthfully for someone
|
|
2063
|
+
* whose conversations are long, and costs proportionally more to ask.
|
|
1899
2064
|
*/
|
|
1900
|
-
function probeRequestBody(modelId) {
|
|
1901
|
-
const targetChars = Math.ceil(
|
|
2065
|
+
function probeRequestBody(modelId, inputTokens = DEFAULT_PROBE_INPUT_TOKENS) {
|
|
2066
|
+
const targetChars = Math.ceil(resolveProbeInputTokens(inputTokens) * CHARS_PER_TOKEN);
|
|
1902
2067
|
const filler = FILLER_LINE.repeat(Math.ceil(targetChars / 45));
|
|
1903
2068
|
return JSON.stringify({
|
|
1904
2069
|
model: modelId,
|
|
@@ -2026,12 +2191,16 @@ function cooldownOf(input) {
|
|
|
2026
2191
|
*
|
|
2027
2192
|
* Built on `classifyUpstreamError` rather than re-testing status codes, so a
|
|
2028
2193
|
* probe and a real chat request can never disagree about what the same upstream
|
|
2029
|
-
* answer means. The extra
|
|
2194
|
+
* answer means. The extra splits here are `credential-rejected`, which the
|
|
2030
2195
|
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
2031
|
-
* different advice (re-auth, not "wait")
|
|
2196
|
+
* different advice (re-auth, not "wait"), and `policy-rejected`, a JSON 403
|
|
2197
|
+
* content-policy refusal where re-auth advice would be actively misleading.
|
|
2032
2198
|
*/
|
|
2033
2199
|
function outcomeOfFailure(status, body) {
|
|
2034
|
-
if (status === 401 || status === 403)
|
|
2200
|
+
if (status === 401 || status === 403) {
|
|
2201
|
+
if (classifyUpstreamError(status, body) === "policy_reject") return "policy-rejected";
|
|
2202
|
+
return "credential-rejected";
|
|
2203
|
+
}
|
|
2035
2204
|
switch (classifyUpstreamError(status, body)) {
|
|
2036
2205
|
case "soft_rate": return "rate-limited";
|
|
2037
2206
|
case "hard_credit": return "out-of-credit";
|
|
@@ -2092,7 +2261,7 @@ async function probeModel(input) {
|
|
|
2092
2261
|
let answer;
|
|
2093
2262
|
const startedAt = Date.now();
|
|
2094
2263
|
try {
|
|
2095
|
-
answer = await input.client.probeChat(input.credential, probeRequestBody(input.modelId), input.signal);
|
|
2264
|
+
answer = await input.client.probeChat(input.credential, probeRequestBody(input.modelId, input.inputTokens), input.signal);
|
|
2096
2265
|
} catch (error) {
|
|
2097
2266
|
return {
|
|
2098
2267
|
modelId: input.modelId,
|
|
@@ -2285,6 +2454,7 @@ function probeUpdatesOf(rows) {
|
|
|
2285
2454
|
updates[row.accountId] = {
|
|
2286
2455
|
outcome: row.result.outcome,
|
|
2287
2456
|
atMs,
|
|
2457
|
+
source: "test-batch",
|
|
2288
2458
|
...row.result.retryAtMs === void 0 ? {} : { retryAtMs: row.result.retryAtMs },
|
|
2289
2459
|
...row.result.message === void 0 ? {} : { message: row.result.message }
|
|
2290
2460
|
};
|
|
@@ -2362,6 +2532,7 @@ function exclusionOf(probe, nowMs) {
|
|
|
2362
2532
|
case "failed": return retryDue(probe, nowMs) ? void 0 : "failed";
|
|
2363
2533
|
case "rate-limited": return retryDue(probe, nowMs) ? void 0 : "rate-limited";
|
|
2364
2534
|
case "out-of-credit": return retryDue(probe, nowMs) ? void 0 : "out-of-credit";
|
|
2535
|
+
case "policy-rejected": return;
|
|
2365
2536
|
default: return "unusable";
|
|
2366
2537
|
}
|
|
2367
2538
|
}
|
|
@@ -2548,7 +2719,7 @@ const POOL_STORE_PREFIX = ".workbuddy-pool";
|
|
|
2548
2719
|
function isProbe(value) {
|
|
2549
2720
|
if (typeof value !== "object" || value === null) return false;
|
|
2550
2721
|
const candidate = value;
|
|
2551
|
-
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");
|
|
2722
|
+
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");
|
|
2552
2723
|
}
|
|
2553
2724
|
/**
|
|
2554
2725
|
* One region's path for measured pool facts.
|
|
@@ -2595,6 +2766,7 @@ async function readPoolProbes(region) {
|
|
|
2595
2766
|
probes[accountId] = {
|
|
2596
2767
|
outcome: probe.outcome,
|
|
2597
2768
|
atMs: probe.atMs,
|
|
2769
|
+
...knownProbeSource(probe.source) === void 0 ? {} : { source: knownProbeSource(probe.source) },
|
|
2598
2770
|
...probe.retryAtMs === void 0 ? {} : { retryAtMs: probe.retryAtMs },
|
|
2599
2771
|
...probe.message === void 0 ? {} : { message: probe.message }
|
|
2600
2772
|
};
|
|
@@ -2602,6 +2774,17 @@ async function readPoolProbes(region) {
|
|
|
2602
2774
|
return probes;
|
|
2603
2775
|
}
|
|
2604
2776
|
/**
|
|
2777
|
+
* A stored source string reduced to a source this build understands.
|
|
2778
|
+
*
|
|
2779
|
+
* `isProbe` accepts any string so a newer build's label cannot destroy the
|
|
2780
|
+
* measurement it describes; this narrows it at the point of use. An unknown
|
|
2781
|
+
* value becomes `undefined`, which the card renders as "unknown" — the honest
|
|
2782
|
+
* answer, rather than mislabelling it as one of the two kinds we do know.
|
|
2783
|
+
*/
|
|
2784
|
+
function knownProbeSource(value) {
|
|
2785
|
+
return value === "test-batch" || value === "live-request" ? value : void 0;
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2605
2788
|
* Merge measurements into one region's store and write it atomically.
|
|
2606
2789
|
*
|
|
2607
2790
|
* A MERGE rather than a replace, because batches measure one account at a time
|
|
@@ -3101,6 +3284,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
3101
3284
|
...measured?.probe === void 0 ? {} : { probe: {
|
|
3102
3285
|
outcome: measured.probe.outcome,
|
|
3103
3286
|
atMs: measured.probe.atMs,
|
|
3287
|
+
...measured.probe.source === void 0 ? {} : { source: measured.probe.source },
|
|
3104
3288
|
...measured.probe.retryAtMs === void 0 ? {} : { retryAtMs: measured.probe.retryAtMs },
|
|
3105
3289
|
...measured.probe.message === void 0 ? {} : { message: measured.probe.message }
|
|
3106
3290
|
} },
|
|
@@ -3117,6 +3301,7 @@ async function workBuddyWebPool(deps, region) {
|
|
|
3117
3301
|
...target.staleModelId === void 0 ? {} : { staleTargetModelId: target.staleModelId },
|
|
3118
3302
|
targetModelSource: target.source,
|
|
3119
3303
|
memberAccountIds: preferences.memberAccountIds,
|
|
3304
|
+
...preferences.probeInputTokens === void 0 ? {} : { probeInputTokens: preferences.probeInputTokens },
|
|
3120
3305
|
effectiveMemberAccountIds: effectiveMembersOf(accounts.map((account) => account.accountId), new Set(preferences.memberAccountIds)),
|
|
3121
3306
|
accounts,
|
|
3122
3307
|
catalog: deps.displayModels(region).map((model) => ({
|
|
@@ -3496,6 +3681,16 @@ const modelConfig = z.object({
|
|
|
3496
3681
|
const poolConfig = z.object({
|
|
3497
3682
|
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."),
|
|
3498
3683
|
targetModelId: z.string().default("").description("Model id to test; empty means pick a zero-multiplier model from this region's catalog"),
|
|
3684
|
+
/**
|
|
3685
|
+
* Sizing of the test probe, as a token count the user picks from a fixed set.
|
|
3686
|
+
*
|
|
3687
|
+
* A closed set rather than a free number: each step is a meaningful position
|
|
3688
|
+
* relative to the measured 20k~30k threshold, and an arbitrary value would
|
|
3689
|
+
* invite a size that is neither safely under nor clearly over it. `0` (and any
|
|
3690
|
+
* unknown value) falls back to the measured default, so a profile written by
|
|
3691
|
+
* an older build — and one hand-edited to something odd — behaves identically.
|
|
3692
|
+
*/
|
|
3693
|
+
probeInputTokens: z.number().step(1).default(0).description("Input tokens each test probe sends: 10000 / 20000 / 30000 / 50000 / 100000. Bigger probes catch a rate limit real conversations would hit, but cost more credit per test and may mark an account unusable for large requests only. 0 uses the measured default (25000)"),
|
|
3499
3694
|
memberAccountIds: z.array(z.string()).default([]).description("Account ids checked into this pool (opt-in; empty means the pool covers nothing)")
|
|
3500
3695
|
});
|
|
3501
3696
|
const regionStateConfig = z.object({
|
|
@@ -4112,6 +4307,7 @@ function apply(ctx, config) {
|
|
|
4112
4307
|
await writePoolProbes(region, { [accountId]: {
|
|
4113
4308
|
outcome,
|
|
4114
4309
|
atMs: Date.now(),
|
|
4310
|
+
source: "live-request",
|
|
4115
4311
|
...retryAtMs === void 0 ? {} : { retryAtMs },
|
|
4116
4312
|
...message === "" ? {} : { message }
|
|
4117
4313
|
} });
|
|
@@ -4202,9 +4398,11 @@ function apply(ctx, config) {
|
|
|
4202
4398
|
/** One region's pool preferences, with the schema's defaults applied. */
|
|
4203
4399
|
const poolPreferencesOf = (config, region) => {
|
|
4204
4400
|
const pool = regionStateOf(config, region).pool;
|
|
4401
|
+
const probeInputTokens = pool?.probeInputTokens;
|
|
4205
4402
|
return {
|
|
4206
4403
|
enabled: pool?.enabled === true,
|
|
4207
4404
|
targetModelId: pool?.targetModelId ?? "",
|
|
4405
|
+
...probeInputTokens === void 0 ? {} : { probeInputTokens },
|
|
4208
4406
|
memberAccountIds: pool?.memberAccountIds ?? []
|
|
4209
4407
|
};
|
|
4210
4408
|
};
|
|
@@ -4250,11 +4448,13 @@ function apply(ctx, config) {
|
|
|
4250
4448
|
claimDailyCheckin: (credential) => client.claimDailyCheckin(credential),
|
|
4251
4449
|
probe: async (credential, modelId) => {
|
|
4252
4450
|
const quotaRefreshAtMs = await quotaRefreshOf(region, credential);
|
|
4451
|
+
const inputTokens = resolveProbeInputTokens(poolPreferencesOf(current(), region).probeInputTokens);
|
|
4253
4452
|
return probeModel({
|
|
4254
4453
|
client,
|
|
4255
4454
|
credential,
|
|
4256
4455
|
modelId,
|
|
4257
4456
|
nowMs: Date.now(),
|
|
4457
|
+
inputTokens,
|
|
4258
4458
|
...quotaRefreshAtMs === void 0 ? {} : { quotaRefreshAtMs }
|
|
4259
4459
|
});
|
|
4260
4460
|
}
|
|
@@ -4376,4 +4576,4 @@ function apply(ctx, config) {
|
|
|
4376
4576
|
});
|
|
4377
4577
|
}
|
|
4378
4578
|
//#endregion
|
|
4379
|
-
export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID,
|
|
4579
|
+
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, 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, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, 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, 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": "3.4.
|
|
5
|
+
"version": "3.4.4",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|