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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as variantFor, D as WorkBuddyUpstreamClient, S as WORKBUDDY_VARIANTS, Y 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-DR6QsZ9h.js";
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, measured
542
- * 2026-09-11 (plan §4.2). It is *not* treated as a permanent protocol promise:
543
- * anything unrecognized degrades to `unknown` rather than to a capability
544
- * conclusion.
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
- return attempt.status === 400 && attempt.errorCode === INVALID_EFFORT_CODE;
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`, and drop the adapter's own `off`
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
- /** Pull `extError.code` out of an upstream error body, if it is shaped that way. */
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
- if (typeof code === "string") return {
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.1";
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, chatUserAgent as B, variantFor as C, WorkBuddyUpstreamClient as D, WORKBUDDY_STATUS_PATH as E, probeModel as F, WORKBUDDY_APP_VERSION_FILENAME as G, readCliVersion as H, randomSentinel as I, readBundleVersion as J, appUserAgent as K, classifyUpstreamError as L, prepareInternationalChatBody as M, regionOf as N, modelWithCurrentPromotion as O, PROBE_EFFORT_CANDIDATES as P, CN_APP_VERSION_FILENAME as R, WORKBUDDY_VARIANTS as S, WORKBUDDY_PROBE_PATH as T, resolveChatIdentity as U, fallbackChatIdentity as V, validCliVersion as W, validAppVersion as X, resolveAppVersion as Y, 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, installedAppVersion 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, FALLBACK_CN_APP_VERSION as z };
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`, and drop the adapter's own `off`
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 chatUserAgent, C as variantFor, D as WorkBuddyUpstreamClient, F as probeModel, G as WORKBUDDY_APP_VERSION_FILENAME, H as readCliVersion, I as randomSentinel, J as readBundleVersion, K as appUserAgent, L as classifyUpstreamError, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as CN_APP_VERSION_FILENAME, S as WORKBUDDY_VARIANTS, U as resolveChatIdentity, V as fallbackChatIdentity, W as validCliVersion, X as validAppVersion, Y as resolveAppVersion, _ 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 installedAppVersion, 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 FALLBACK_CN_APP_VERSION } from "./heartbeat-DR6QsZ9h.js";
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, `workbuddy upstream ${result.kind} (http ${result.status}): ${result.message.slice(0, 400)}`);
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.1",
5
+ "version": "0.5.3",
6
6
  "author": "zlZayn",
7
7
  "keywords": [
8
8
  "dsh",