dsh-workbuddy-bridge 0.5.1 → 0.5.3
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/lib/bin.js +1 -1
- package/lib/{heartbeat-DR6QsZ9h.js → heartbeat-BJrYAacX.js} +205 -13
- package/lib/index.d.ts +9 -1
- package/lib/index.js +21 -2
- package/package.json +1 -1
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as variantFor, D as WorkBuddyUpstreamClient, S as WORKBUDDY_VARIANTS,
|
|
2
|
+
import { C as variantFor, D as WorkBuddyUpstreamClient, S as WORKBUDDY_VARIANTS, X as resolveAppVersion, a as readHostHeartbeat, c as WORKBUDDY_BRIDGE_VERSION, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, x as CN_VARIANT } from "./heartbeat-BJrYAacX.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/cli/bin.ts
|
|
@@ -366,13 +366,15 @@ async function resolveGlobalIdentity(options) {
|
|
|
366
366
|
const UPSTREAM_CODE_KINDS = {
|
|
367
367
|
"12153": "session_dead",
|
|
368
368
|
"11128": "client",
|
|
369
|
-
"11133": "client"
|
|
369
|
+
"11133": "client",
|
|
370
|
+
"11140": "client"
|
|
370
371
|
};
|
|
371
372
|
/** 业务码 → 失败类别:信封 code 非 0 时的第一落点。 */
|
|
372
373
|
const ENVELOPE_CODE_KINDS = {
|
|
373
374
|
12153: "session_dead",
|
|
374
375
|
11128: "client",
|
|
375
|
-
11133: "client"
|
|
376
|
+
11133: "client",
|
|
377
|
+
11140: "client"
|
|
376
378
|
};
|
|
377
379
|
/**
|
|
378
380
|
* 文案兜底标记。**只在既无机器码也无业务码可依时使用**,且命中后必须记日志:
|
|
@@ -396,8 +398,43 @@ const TEXT_MARKERS = [
|
|
|
396
398
|
["offline user session not found", "session_dead"],
|
|
397
399
|
["12153", "session_dead"],
|
|
398
400
|
["11128", "client"],
|
|
399
|
-
["11133", "client"]
|
|
401
|
+
["11133", "client"],
|
|
402
|
+
["11140", "client"]
|
|
400
403
|
];
|
|
404
|
+
/**
|
|
405
|
+
* 上游自己的、面向用户的文案:`displayMsg` 优先,其次是 `msg`。
|
|
406
|
+
*
|
|
407
|
+
* 上游把「真正该给人看的那句话」放在 `displayMsg` 里(分别给出 `zh` / `zh-hant` /
|
|
408
|
+
* `en` 三种拼写),而 `msg` 常常是一句机器可读的英文短语(`request illegal`)。
|
|
409
|
+
* 把 `displayMsg` 丢掉的代价不是"少一句说明":`msg` 本身看不出是封号、限流还是
|
|
410
|
+
* 参数错,使用者只能看到一句无从下手的英文。
|
|
411
|
+
*
|
|
412
|
+
* 读取顺序固定在 `zh` → `zh-hant` → `en`:国内网关的 zh 文案是原始表述,en 是
|
|
413
|
+
* 它的译文;先中文,只有上游没给中文时才退回英文。
|
|
414
|
+
*
|
|
415
|
+
* @param body - 响应体原文。
|
|
416
|
+
* @returns 干净的、可直接展示的一行文案;读不出来返回 undefined。
|
|
417
|
+
*/
|
|
418
|
+
function upstreamDisplayMessage(body) {
|
|
419
|
+
const parsed = parseJson(body);
|
|
420
|
+
if (!isRecord(parsed)) return void 0;
|
|
421
|
+
const display = parsed["displayMsg"];
|
|
422
|
+
if (isRecord(display)) for (const key of [
|
|
423
|
+
"zh",
|
|
424
|
+
"zh-hant",
|
|
425
|
+
"en"
|
|
426
|
+
]) {
|
|
427
|
+
const value = display[key];
|
|
428
|
+
if (typeof value === "string" && value.trim() !== "") return value.trim();
|
|
429
|
+
}
|
|
430
|
+
const msg = parsed["msg"];
|
|
431
|
+
if (typeof msg === "string" && msg.trim() !== "") return msg.trim();
|
|
432
|
+
const extError = parsed["extError"];
|
|
433
|
+
if (isRecord(extError)) {
|
|
434
|
+
const message = extError["message"];
|
|
435
|
+
if (typeof message === "string" && message.trim() !== "") return message.trim();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
401
438
|
/** 上游错误体:只取结构化字段,其余一概不看。 */
|
|
402
439
|
const ErrorBody = z.object({
|
|
403
440
|
extError: z.object({
|
|
@@ -494,6 +531,10 @@ function parseJson(body) {
|
|
|
494
531
|
return;
|
|
495
532
|
}
|
|
496
533
|
}
|
|
534
|
+
/** 是否是一个可按键读取的对象(数组与 null 都不算)。 */
|
|
535
|
+
function isRecord(value) {
|
|
536
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
537
|
+
}
|
|
497
538
|
//#endregion
|
|
498
539
|
//#region src/probe/probe.ts
|
|
499
540
|
/**
|
|
@@ -538,15 +579,31 @@ function randomSentinel() {
|
|
|
538
579
|
return `probe_sentinel_${randomBytes(12).toString("hex")}`;
|
|
539
580
|
}
|
|
540
581
|
/**
|
|
541
|
-
* The upstream's "this effort value is not supported" code
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
582
|
+
* The upstream's "this effort value is not supported" code.
|
|
583
|
+
*
|
|
584
|
+
* Measured 2026-09-11 (plan §4.2) as `invalid_reasoning_effort`. It is *not* a
|
|
585
|
+
* permanent protocol promise: the upstream has since respelled it to the
|
|
586
|
+
* generic `invalid_parameter_value` on both endpoints, which is why that
|
|
587
|
+
* spelling is recognized too. Anything unrecognized still degrades to
|
|
588
|
+
* `unknown` rather than to a capability conclusion.
|
|
545
589
|
*/
|
|
546
590
|
const INVALID_EFFORT_CODE = "invalid_reasoning_effort";
|
|
591
|
+
/**
|
|
592
|
+
* Generic rejection code the upstream now uses for every bad field.
|
|
593
|
+
*
|
|
594
|
+
* Because it names no field, a body carrying it is only attributed to the
|
|
595
|
+
* effort when `extError.param` points at the effort; otherwise a rejected
|
|
596
|
+
* `max_tokens` would be misread as "this level is unsupported" and shrink the
|
|
597
|
+
* reported capability set.
|
|
598
|
+
*/
|
|
599
|
+
const GENERIC_INVALID_CODE = "invalid_parameter_value";
|
|
547
600
|
/** Whether an attempt is an attributable rejection of the effort value. */
|
|
548
601
|
function isEffortRejection(attempt) {
|
|
549
|
-
|
|
602
|
+
if (attempt.status !== 400) return false;
|
|
603
|
+
const code = attempt.errorCode;
|
|
604
|
+
if (code === INVALID_EFFORT_CODE) return true;
|
|
605
|
+
if (code !== GENERIC_INVALID_CODE) return false;
|
|
606
|
+
return (attempt.param ?? "").toLowerCase().includes("effort");
|
|
550
607
|
}
|
|
551
608
|
/** Whether an attempt shows the upstream accepted the request and streamed. */
|
|
552
609
|
function isAcceptance(attempt) {
|
|
@@ -828,7 +885,8 @@ function billingHeaders(credential) {
|
|
|
828
885
|
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
829
886
|
* force `stream: true` (the upstream rejects non-streaming), flatten
|
|
830
887
|
* `tool_choice` (the upstream's field is a string; object forms return 400),
|
|
831
|
-
* rewrite `developer` messages as `system`,
|
|
888
|
+
* rewrite `developer` messages as `system`, neutralize the channel signature
|
|
889
|
+
* (see {@link neutralizeChannelSignature}), and drop the adapter's own `off`
|
|
832
890
|
* effort spelling (see {@link dropUnsupportedEffort}).
|
|
833
891
|
*
|
|
834
892
|
* The `developer` rewrite is load-bearing: pi-ai emits the system prompt as
|
|
@@ -855,10 +913,132 @@ function prepareChatBody(source) {
|
|
|
855
913
|
const obj = body;
|
|
856
914
|
obj["stream"] = true;
|
|
857
915
|
normalizeDeveloperRole(obj);
|
|
916
|
+
neutralizeChannelSignature(obj);
|
|
858
917
|
normalizeToolChoice(obj);
|
|
859
918
|
dropUnsupportedEffort(obj);
|
|
860
919
|
return JSON.stringify(obj);
|
|
861
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* Strip the product-identity phrase the CN gateway reads as an unofficial
|
|
923
|
+
* client signature.
|
|
924
|
+
*
|
|
925
|
+
* Measured 2026-10-21 on `copilot.tencent.com/v2/chat/completions`: a request
|
|
926
|
+
* whose message bodies carry the phrase `DeepSeek Harness` answers `400` /
|
|
927
|
+
* `11128` "Illegal API invocation from an unapproved channel", while the same
|
|
928
|
+
* account, headers and body with the phrase rewritten answers 200. DSH injects
|
|
929
|
+
* exactly that phrase as the head of its system prompt ("You are an AI agent
|
|
930
|
+
* powered by DeepSeek Harness."), so every request the harness sends is
|
|
931
|
+
* affected; the official desktop app never sends it.
|
|
932
|
+
*
|
|
933
|
+
* Two rewrites, in this order, because they solve different halves of it:
|
|
934
|
+
*
|
|
935
|
+
* 1. **The opener**, when a message starts with it, becomes a plain
|
|
936
|
+
* "You are an AI agent." — a readable sentence that survives upstream
|
|
937
|
+
* normalization (unlike an invisible character, which a tokenizer or a
|
|
938
|
+
* normalizer may simply drop, and which the same measurement showed the
|
|
939
|
+
* gateway still rejects once an account is restricted).
|
|
940
|
+
* 2. **Any other occurrence** gets a zero-width space between the two words:
|
|
941
|
+
* lossless, the model reads the same thing, and the substring match no
|
|
942
|
+
* longer fires.
|
|
943
|
+
*
|
|
944
|
+
* **Path contexts are never rewritten** (`…\DeepSeek Harness\…`,
|
|
945
|
+
* `…/DeepSeek Harness/…`): a real directory name carries this phrase, and a
|
|
946
|
+
* zero-width space inside it would turn every tool call that quotes the path
|
|
947
|
+
* into a silently wrong path. Both rewrites anchor on the surrounding slashes
|
|
948
|
+
* for that reason.
|
|
949
|
+
*
|
|
950
|
+
* **Roles covered** — `system`, `developer` and `assistant`; `user` and `tool`
|
|
951
|
+
* are left alone. Measured 2026-10-21 by ablation against the same gateway
|
|
952
|
+
* (independently reproduced on the CPA-plugin side and published as
|
|
953
|
+
* mmqz/cpa-multi-plugins PR #49 — a sibling fix, not this repo's upstream and
|
|
954
|
+
* not a dependency): the filter reads `system` and `assistant` bodies, and in
|
|
955
|
+
* that request the triggering phrases sat mostly in the **assistant history
|
|
956
|
+
* echo** — a 32 KB compaction summary carried 42 brand occurrences, and
|
|
957
|
+
* rewriting only `system` still answered 400. The same ablation found `user`
|
|
958
|
+
* text, tool results, tool definitions, `tool_calls` arguments and
|
|
959
|
+
* `reasoning_content` did not fire.
|
|
960
|
+
*
|
|
961
|
+
* This stays narrower than "every role" on purpose. The two excluded kinds are
|
|
962
|
+
* the ones whose bytes are ground truth for the model: user input is the user's
|
|
963
|
+
* own words, and a tool result is what a file or command actually returned.
|
|
964
|
+
* Rewriting either would make the transcript disagree with the world the model
|
|
965
|
+
* is reasoning about — a silent correctness bug traded for a filter that, per
|
|
966
|
+
* the measurement, is not looking there. If the filter later widens, that is a
|
|
967
|
+
* decision with fresh evidence, not something to pre-empt.
|
|
968
|
+
*
|
|
969
|
+
* Idempotent — a second pass finds neither the opener nor a bare phrase.
|
|
970
|
+
*/
|
|
971
|
+
function neutralizeChannelSignature(obj) {
|
|
972
|
+
const messages = obj["messages"];
|
|
973
|
+
if (!Array.isArray(messages)) return;
|
|
974
|
+
for (const message of messages) {
|
|
975
|
+
if (!isObject(message)) continue;
|
|
976
|
+
const role = message["role"];
|
|
977
|
+
if (role === "user" || role === "tool") continue;
|
|
978
|
+
neutralizeMessageText(message);
|
|
979
|
+
if (role === "assistant") neutralizeAssistantExtras(message);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/** Rewrite a message's string `content`, or each text part of an array `content`. */
|
|
983
|
+
function neutralizeMessageText(message) {
|
|
984
|
+
const content = message["content"];
|
|
985
|
+
if (typeof content === "string") {
|
|
986
|
+
message["content"] = neutralizeSignatureText(content);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (!Array.isArray(content)) return;
|
|
990
|
+
for (const part of content) {
|
|
991
|
+
if (!isObject(part) || part["type"] !== "text") continue;
|
|
992
|
+
const text = part["text"];
|
|
993
|
+
if (typeof text === "string") part["text"] = neutralizeSignatureText(text);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Rewrite the assistant-only carriers that can reach the wire.
|
|
998
|
+
*
|
|
999
|
+
* `reasoning_content` / `reasoning` are the model's own thinking echoed back on
|
|
1000
|
+
* a replayed assistant turn, and `tool_calls[].function.arguments` is a JSON
|
|
1001
|
+
* string the model wrote earlier. Both are model-generated, so rewriting them
|
|
1002
|
+
* keeps the transcript self-consistent, and the path guard still protects any
|
|
1003
|
+
* real directory name quoted inside a tool argument.
|
|
1004
|
+
*/
|
|
1005
|
+
function neutralizeAssistantExtras(message) {
|
|
1006
|
+
for (const key of ["reasoning_content", "reasoning"]) {
|
|
1007
|
+
const value = message[key];
|
|
1008
|
+
if (typeof value === "string") message[key] = neutralizeSignatureText(value);
|
|
1009
|
+
}
|
|
1010
|
+
const toolCalls = message["tool_calls"];
|
|
1011
|
+
if (!Array.isArray(toolCalls)) return;
|
|
1012
|
+
for (const call of toolCalls) {
|
|
1013
|
+
if (!isObject(call)) continue;
|
|
1014
|
+
const fn = call["function"];
|
|
1015
|
+
if (!isObject(fn)) continue;
|
|
1016
|
+
const args = fn["arguments"];
|
|
1017
|
+
if (typeof args === "string") fn["arguments"] = neutralizeSignatureText(args);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/** Apply the two signature rewrites to one string; see {@link neutralizeChannelSignature}. */
|
|
1021
|
+
function neutralizeSignatureText(text) {
|
|
1022
|
+
return text.replace(CHANNEL_OPENER, "$1You are an AI agent.").replace(CHANNEL_SIGNATURE_PHRASE, `DeepSeek${ZERO_WIDTH_SPACE}$1Harness`);
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* The opener DSH puts at the head of every system prompt.
|
|
1026
|
+
*
|
|
1027
|
+
* Leading whitespace and a BOM are tolerated (group 1 carries them through to
|
|
1028
|
+
* the replacement, so the rewrite never touches anything but the sentence), and
|
|
1029
|
+
* every gap between the words is matched as whitespace so a respelling cannot
|
|
1030
|
+
* walk past it. Case-insensitive for the same reason.
|
|
1031
|
+
*/
|
|
1032
|
+
const CHANNEL_OPENER = /^([\s\uFEFF]*)You\s+are\s+an\s+AI\s+agent\s+powered\s+by\s+DeepSeek[\s\u00A0]+Harness\s*\./i;
|
|
1033
|
+
/**
|
|
1034
|
+
* The bare phrase, with slash guards on both sides so a real path survives.
|
|
1035
|
+
*
|
|
1036
|
+
* The capture keeps whatever whitespace the original used, so the rewritten
|
|
1037
|
+
* sentence reads identically.
|
|
1038
|
+
*/
|
|
1039
|
+
const CHANNEL_SIGNATURE_PHRASE = /(?<![\\/])DeepSeek([\s\u00A0]+)Harness(?![\\/])/gi;
|
|
1040
|
+
/** U+200B: invisible to a reader and to the model, breaks a substring match. */
|
|
1041
|
+
const ZERO_WIDTH_SPACE = "";
|
|
862
1042
|
/** Rewrite `role: "developer"` messages to `role: "system"` (upstream rejects developer). */
|
|
863
1043
|
function normalizeDeveloperRole(obj) {
|
|
864
1044
|
const messages = obj["messages"];
|
|
@@ -1338,7 +1518,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1338
1518
|
};
|
|
1339
1519
|
}
|
|
1340
1520
|
};
|
|
1341
|
-
/**
|
|
1521
|
+
/**
|
|
1522
|
+
* Pull the structured failure fields out of an upstream error body.
|
|
1523
|
+
*
|
|
1524
|
+
* `extError.param` matters as much as the code: the upstream reuses one generic
|
|
1525
|
+
* code (`invalid_parameter_value`) for every rejected field, so the parameter
|
|
1526
|
+
* name is the only thing that attributes the rejection to `reasoning.effort`
|
|
1527
|
+
* rather than to `max_tokens` or anything else.
|
|
1528
|
+
*/
|
|
1342
1529
|
function errorCodeOf(text) {
|
|
1343
1530
|
try {
|
|
1344
1531
|
const parsed = JSON.parse(text);
|
|
@@ -1346,7 +1533,12 @@ function errorCodeOf(text) {
|
|
|
1346
1533
|
const extError = parsed["extError"];
|
|
1347
1534
|
if (typeof extError === "object" && extError !== null && !Array.isArray(extError)) {
|
|
1348
1535
|
const code = extError["code"];
|
|
1349
|
-
|
|
1536
|
+
const param = extError["param"];
|
|
1537
|
+
if (typeof code === "string") return typeof param === "string" ? {
|
|
1538
|
+
errorCode: code,
|
|
1539
|
+
detail: code,
|
|
1540
|
+
param
|
|
1541
|
+
} : {
|
|
1350
1542
|
errorCode: code,
|
|
1351
1543
|
detail: code
|
|
1352
1544
|
};
|
|
@@ -3725,7 +3917,7 @@ var WorkBuddyCatalog = class {
|
|
|
3725
3917
|
};
|
|
3726
3918
|
//#endregion
|
|
3727
3919
|
//#region src/version.ts
|
|
3728
|
-
const WORKBUDDY_BRIDGE_VERSION = "0.5.
|
|
3920
|
+
const WORKBUDDY_BRIDGE_VERSION = "0.5.3";
|
|
3729
3921
|
//#endregion
|
|
3730
3922
|
//#region src/web/heartbeat.ts
|
|
3731
3923
|
/**
|
|
@@ -3896,4 +4088,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
3896
4088
|
return startAtMs <= heartbeat.registeredAt;
|
|
3897
4089
|
}
|
|
3898
4090
|
//#endregion
|
|
3899
|
-
export { parseModelCatalog as A,
|
|
4091
|
+
export { parseModelCatalog as A, FALLBACK_CN_APP_VERSION as B, variantFor as C, WorkBuddyUpstreamClient as D, WORKBUDDY_STATUS_PATH as E, probeModel as F, validCliVersion as G, fallbackChatIdentity as H, randomSentinel as I, installedAppVersion as J, WORKBUDDY_APP_VERSION_FILENAME as K, classifyUpstreamError as L, prepareInternationalChatBody as M, regionOf as N, modelWithCurrentPromotion as O, PROBE_EFFORT_CANDIDATES as P, upstreamDisplayMessage as R, WORKBUDDY_VARIANTS as S, WORKBUDDY_PROBE_PATH as T, readCliVersion as U, chatUserAgent as V, resolveChatIdentity as W, resolveAppVersion as X, readBundleVersion as Y, validAppVersion as Z, desktopAuthCandidatesFor as _, readHostHeartbeat as a, AI_VARIANT as b, WORKBUDDY_BRIDGE_VERSION as c, WorkBuddyCatalog as d, WORKBUDDY_AUTH_FILENAME as f, defaultDesktopAuthPath as g, defaultDesktopAuthCandidates as h, processStartTimeMs as i, prepareChatBody as j, normalizeCredits as k, FALLBACK_WORKBUDDY_AI_MODELS as l, WorkBuddyCredentialStore as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, WORKBUDDY_AUTH_FILE_ENV as p, appUserAgent as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, parseWorkBuddyAuth as v, WORKBUDDY_CREDIT_PATH as w, CN_VARIANT as x, workbuddyOwnAuthPath as y, CN_APP_VERSION_FILENAME as z };
|
package/lib/index.d.ts
CHANGED
|
@@ -424,6 +424,13 @@ interface ProbeAttempt {
|
|
|
424
424
|
streamed: boolean;
|
|
425
425
|
/** `extError.code` from a JSON error body, when present. */
|
|
426
426
|
errorCode?: string;
|
|
427
|
+
/**
|
|
428
|
+
* `extError.param` from the same body.
|
|
429
|
+
*
|
|
430
|
+
* Load-bearing since the upstream started reusing one generic rejection code
|
|
431
|
+
* for every field: it is what attributes the rejection to the effort value.
|
|
432
|
+
*/
|
|
433
|
+
param?: string;
|
|
427
434
|
/** Free-form detail for logs; never shown as a capability claim. */
|
|
428
435
|
detail?: string;
|
|
429
436
|
}
|
|
@@ -613,7 +620,8 @@ declare function regionOf(domain: string): WorkBuddyRegion;
|
|
|
613
620
|
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
614
621
|
* force `stream: true` (the upstream rejects non-streaming), flatten
|
|
615
622
|
* `tool_choice` (the upstream's field is a string; object forms return 400),
|
|
616
|
-
* rewrite `developer` messages as `system`,
|
|
623
|
+
* rewrite `developer` messages as `system`, neutralize the channel signature
|
|
624
|
+
* (see {@link neutralizeChannelSignature}), and drop the adapter's own `off`
|
|
617
625
|
* effort spelling (see {@link dropUnsupportedEffort}).
|
|
618
626
|
*
|
|
619
627
|
* The `developer` rewrite is load-bearing: pi-ai emits the system prompt as
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as parseModelCatalog, B as
|
|
1
|
+
import { A as parseModelCatalog, B as FALLBACK_CN_APP_VERSION, C as variantFor, D as WorkBuddyUpstreamClient, F as probeModel, G as validCliVersion, H as fallbackChatIdentity, I as randomSentinel, J as installedAppVersion, K as WORKBUDDY_APP_VERSION_FILENAME, L as classifyUpstreamError, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as upstreamDisplayMessage, S as WORKBUDDY_VARIANTS, U as readCliVersion, V as chatUserAgent, W as resolveChatIdentity, X as resolveAppVersion, Y as readBundleVersion, Z as validAppVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as AI_VARIANT, c as WORKBUDDY_BRIDGE_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as prepareChatBody, k as normalizeCredits, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as appUserAgent, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as WORKBUDDY_CREDIT_PATH, x as CN_VARIANT, y as workbuddyOwnAuthPath, z as CN_APP_VERSION_FILENAME } from "./heartbeat-BJrYAacX.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -805,6 +805,25 @@ function writeOpenAIError(res, status, kind, message) {
|
|
|
805
805
|
code: kind
|
|
806
806
|
} });
|
|
807
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* The one-line reason a failed upstream request is reported with.
|
|
810
|
+
*
|
|
811
|
+
* The host does not read our HTTP status to classify a failure: `dsh-llm-pi-ai`
|
|
812
|
+
* matches the **message text** (`if (/\b(?:401|403)\b/.test(message)) return
|
|
813
|
+
* "AUTH"`), and the chat UI then replaces whatever we wrote with
|
|
814
|
+
* "API key is invalid". So a status may only appear here when the failure
|
|
815
|
+
* really is an authentication one; every other class keeps its digits out, or
|
|
816
|
+
* a restricted account or a rejected prompt reaches the user as a bad key.
|
|
817
|
+
*
|
|
818
|
+
* The upstream's own `displayMsg` wins over the raw body: it is the sentence
|
|
819
|
+
* written for a human ("账号存在异常行为,已被限制使用"), while the body is a JSON
|
|
820
|
+
* document whose `msg` is often a machine phrase such as `request illegal`.
|
|
821
|
+
*/
|
|
822
|
+
function failureMessage(result) {
|
|
823
|
+
const reason = upstreamDisplayMessage(result.message) ?? result.message.slice(0, 400);
|
|
824
|
+
const head = `workbuddy upstream ${result.kind}`;
|
|
825
|
+
return result.kind === "session_dead" ? `${head} (http ${result.status}): ${reason}` : `${head}: ${reason}`;
|
|
826
|
+
}
|
|
808
827
|
/** Read a request body with a size cap; over-limit bodies fail the request. */
|
|
809
828
|
function readBody$1(req) {
|
|
810
829
|
return new Promise((resolve, reject) => {
|
|
@@ -916,7 +935,7 @@ function createWorkBuddyShim(options) {
|
|
|
916
935
|
req.on("close", () => controller.abort());
|
|
917
936
|
const result = await client.chatStream(credential, prepared, controller.signal);
|
|
918
937
|
if (!result.ok) {
|
|
919
|
-
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind,
|
|
938
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, failureMessage(result));
|
|
920
939
|
return;
|
|
921
940
|
}
|
|
922
941
|
res.writeHead(200, {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-workbuddy-bridge",
|
|
3
3
|
"displayName": "DSH WorkBuddy Bridge",
|
|
4
4
|
"description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.3",
|
|
6
6
|
"author": "zlZayn",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh",
|