dsh-workbuddy-bridge 0.5.0 → 0.5.2
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/README.md +2 -1
- package/README_en.md +2 -0
- package/lib/bin.js +1 -1
- package/lib/{heartbeat-BpW2mSgI.js → heartbeat-6xWs7ak3.js} +219 -35
- package/lib/index.d.ts +36 -2
- package/lib/index.js +28 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -47,10 +47,11 @@
|
|
|
47
47
|
|
|
48
48
|
- **开箱即用** —— 装完启用,模型分组自己出现。
|
|
49
49
|
- **两版隔离** —— 国内版与国际版各自分组,模型、账号、积分互不混用,只跟随各自 App 的登录状态。
|
|
50
|
-
- **推理档位** —— 上游声明了档位的模型直接可选;没声明的可在插件页**手动检测**。
|
|
51
50
|
- **推理档位** —— 上游声明了档位的模型直接可选;没声明的可在插件页**手动检测**。
|
|
52
51
|
灯泡表示能切档位;上游已声明的模型也亮着,只是不需要检测。
|
|
53
52
|
能切档位的模型会带一个**默认档位**(上游声明了就按声明,否则取 `high`),因此选择器里不再出现「Default」这一项。
|
|
53
|
+
档位里的 **Off 表示「不发送档位参数」**:两个接口都不接受这个字面量,所以插件不会把它发出去;
|
|
54
|
+
实际是否关闭思考由上游决定,插件不做保证。
|
|
54
55
|
- **积分可见** —— 每条回复下方显示本次消耗,模型选择器旁常驻剩余积分。拿不到数字时不显示。
|
|
55
56
|
- **模型显隐** —— 卡片「模型」标签里勾选哪些模型出现在选择器里,按登录账号分别保存。
|
|
56
57
|
- **图片输入** —— 多数模型可直接粘贴或拖入图片;纯文字模型会明确说明。
|
package/README_en.md
CHANGED
|
@@ -54,6 +54,8 @@ models appear directly in the DSH model picker — no separate provider to confi
|
|
|
54
54
|
on the plugin page. The bulb means "this model can switch levels"; a declared model shows it too, it just
|
|
55
55
|
has nothing to detect. A switchable model also carries a **default level** (the upstream's own declaration
|
|
56
56
|
when it has one, otherwise `high`), so the picker no longer offers "Default".
|
|
57
|
+
**Off means "send no level parameter"**: neither endpoint accepts that literal, so the plugin never puts it
|
|
58
|
+
on the wire — whether thinking is then actually disabled is up to the upstream, and the plugin makes no claim.
|
|
57
59
|
- **Credit in view** — each reply shows what it consumed, and remaining credit sits beside the model picker.
|
|
58
60
|
Nothing renders when a figure is unavailable.
|
|
59
61
|
- **Model visibility** — tick which models appear in the picker on the card's "Models" tab, saved per
|
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-6xWs7ak3.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,13 +885,22 @@ 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
|
-
*
|
|
888
|
+
* rewrite `developer` messages as `system`, neutralize the channel signature
|
|
889
|
+
* (see {@link neutralizeChannelSignature}), and drop the adapter's own `off`
|
|
890
|
+
* effort spelling (see {@link dropUnsupportedEffort}).
|
|
832
891
|
*
|
|
833
892
|
* The `developer` rewrite is load-bearing: pi-ai emits the system prompt as
|
|
834
893
|
* `role: "developer"` (the OpenAI convention it adopted), but the WorkBuddy
|
|
835
894
|
* upstream rejects that role with HTTP 400 code 11128 ("Illegal API
|
|
836
895
|
* invocation from an unapproved channel"). Rewriting to `system` is the
|
|
837
896
|
* compatible spelling the upstream accepts.
|
|
897
|
+
*
|
|
898
|
+
* Every transformation here is region-agnostic on purpose. The two endpoints
|
|
899
|
+
* differ in what they *add* (the international one also needs a leading system
|
|
900
|
+
* prompt — see {@link prepareInternationalChatBody}), not in what they reject:
|
|
901
|
+
* both refuse the picker's `off` spelling, which is why that strip lives here
|
|
902
|
+
* rather than in the international layer (issue #13 reversed the earlier
|
|
903
|
+
* international-only scope decision, which rested on CN accepting it).
|
|
838
904
|
*/
|
|
839
905
|
function prepareChatBody(source) {
|
|
840
906
|
let body;
|
|
@@ -847,9 +913,87 @@ function prepareChatBody(source) {
|
|
|
847
913
|
const obj = body;
|
|
848
914
|
obj["stream"] = true;
|
|
849
915
|
normalizeDeveloperRole(obj);
|
|
916
|
+
neutralizeChannelSignature(obj);
|
|
850
917
|
normalizeToolChoice(obj);
|
|
918
|
+
dropUnsupportedEffort(obj);
|
|
851
919
|
return JSON.stringify(obj);
|
|
852
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 **system/developer** message carries the phrase `DeepSeek Harness`
|
|
927
|
+
* answers `400` / `11128` "Illegal API invocation from an unapproved channel",
|
|
928
|
+
* while the same account, headers and body with the phrase rewritten answers
|
|
929
|
+
* 200. DSH injects exactly that phrase as the head of its system prompt
|
|
930
|
+
* ("You are an AI agent powered by DeepSeek Harness."), so every request the
|
|
931
|
+
* harness sends is 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
|
+
* Only `system` / `developer` messages are touched: the same measurement shows
|
|
951
|
+
* user messages are not inspected, and rewriting user content would corrupt
|
|
952
|
+
* data the plugin has no business editing. Idempotent — a second pass finds
|
|
953
|
+
* neither the opener nor a bare phrase.
|
|
954
|
+
*/
|
|
955
|
+
function neutralizeChannelSignature(obj) {
|
|
956
|
+
const messages = obj["messages"];
|
|
957
|
+
if (!Array.isArray(messages)) return;
|
|
958
|
+
for (const message of messages) {
|
|
959
|
+
if (!isObject(message)) continue;
|
|
960
|
+
const role = message["role"];
|
|
961
|
+
if (role !== "system" && role !== "developer") continue;
|
|
962
|
+
const content = message["content"];
|
|
963
|
+
if (typeof content === "string") {
|
|
964
|
+
message["content"] = neutralizeSignatureText(content);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (!Array.isArray(content)) continue;
|
|
968
|
+
for (const part of content) {
|
|
969
|
+
if (!isObject(part) || part["type"] !== "text") continue;
|
|
970
|
+
const text = part["text"];
|
|
971
|
+
if (typeof text === "string") part["text"] = neutralizeSignatureText(text);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/** Apply the two signature rewrites to one string; see {@link neutralizeChannelSignature}. */
|
|
976
|
+
function neutralizeSignatureText(text) {
|
|
977
|
+
return text.replace(CHANNEL_OPENER, "$1You are an AI agent.").replace(CHANNEL_SIGNATURE_PHRASE, `DeepSeek${ZERO_WIDTH_SPACE}$1Harness`);
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* The opener DSH puts at the head of every system prompt.
|
|
981
|
+
*
|
|
982
|
+
* Leading whitespace and a BOM are tolerated (group 1 carries them through to
|
|
983
|
+
* the replacement, so the rewrite never touches anything but the sentence), and
|
|
984
|
+
* every gap between the words is matched as whitespace so a respelling cannot
|
|
985
|
+
* walk past it. Case-insensitive for the same reason.
|
|
986
|
+
*/
|
|
987
|
+
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;
|
|
988
|
+
/**
|
|
989
|
+
* The bare phrase, with slash guards on both sides so a real path survives.
|
|
990
|
+
*
|
|
991
|
+
* The capture keeps whatever whitespace the original used, so the rewritten
|
|
992
|
+
* sentence reads identically.
|
|
993
|
+
*/
|
|
994
|
+
const CHANNEL_SIGNATURE_PHRASE = /(?<![\\/])DeepSeek([\s\u00A0]+)Harness(?![\\/])/gi;
|
|
995
|
+
/** U+200B: invisible to a reader and to the model, breaks a substring match. */
|
|
996
|
+
const ZERO_WIDTH_SPACE = "";
|
|
853
997
|
/** Rewrite `role: "developer"` messages to `role: "system"` (upstream rejects developer). */
|
|
854
998
|
function normalizeDeveloperRole(obj) {
|
|
855
999
|
const messages = obj["messages"];
|
|
@@ -937,7 +1081,20 @@ var WorkBuddyUpstreamClient = class {
|
|
|
937
1081
|
this.resolveAppVersion = options.resolveAppVersion ?? (() => resolveAppVersion());
|
|
938
1082
|
this.resolveChatIdentity = options.resolveChatIdentity ?? ((region) => resolveChatIdentity(region));
|
|
939
1083
|
}
|
|
940
|
-
/**
|
|
1084
|
+
/**
|
|
1085
|
+
* POST the chat endpoint; a successful answer is the raw SSE response.
|
|
1086
|
+
*
|
|
1087
|
+
* This is the one place every chat request converges on (the loopback shim,
|
|
1088
|
+
* the host LLM route, and any direct caller all end up here), so it is also
|
|
1089
|
+
* where the body is normalized for the upstream — in *both* regions. The
|
|
1090
|
+
* region only decides which extra requirement applies on top: the
|
|
1091
|
+
* international gateway additionally demands a leading system message. It
|
|
1092
|
+
* used to decide whether the body was normalized at all, which left the CN
|
|
1093
|
+
* path sending the picker's `off` spelling verbatim (issue #13).
|
|
1094
|
+
*
|
|
1095
|
+
* Normalization is idempotent, so a caller that already prepared the body
|
|
1096
|
+
* (the shim does) sees no second change.
|
|
1097
|
+
*/
|
|
941
1098
|
async chatStream(credential, bodyJson, signal) {
|
|
942
1099
|
const region = regionOf(credential.domain);
|
|
943
1100
|
let userAgent;
|
|
@@ -946,6 +1103,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
946
1103
|
} catch {
|
|
947
1104
|
userAgent = chatUserAgent(fallbackChatIdentity(region), region);
|
|
948
1105
|
}
|
|
1106
|
+
const body = region === "global" ? prepareInternationalChatBody(bodyJson) : prepareChatBody(bodyJson);
|
|
949
1107
|
let response;
|
|
950
1108
|
try {
|
|
951
1109
|
response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
|
|
@@ -954,7 +1112,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
954
1112
|
...chatHeaders(credential, userAgent),
|
|
955
1113
|
Authorization: `Bearer ${credential.accessToken}`
|
|
956
1114
|
},
|
|
957
|
-
body
|
|
1115
|
+
body,
|
|
958
1116
|
...signal === void 0 ? {} : { signal }
|
|
959
1117
|
});
|
|
960
1118
|
} catch (error) {
|
|
@@ -1315,7 +1473,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1315
1473
|
};
|
|
1316
1474
|
}
|
|
1317
1475
|
};
|
|
1318
|
-
/**
|
|
1476
|
+
/**
|
|
1477
|
+
* Pull the structured failure fields out of an upstream error body.
|
|
1478
|
+
*
|
|
1479
|
+
* `extError.param` matters as much as the code: the upstream reuses one generic
|
|
1480
|
+
* code (`invalid_parameter_value`) for every rejected field, so the parameter
|
|
1481
|
+
* name is the only thing that attributes the rejection to `reasoning.effort`
|
|
1482
|
+
* rather than to `max_tokens` or anything else.
|
|
1483
|
+
*/
|
|
1319
1484
|
function errorCodeOf(text) {
|
|
1320
1485
|
try {
|
|
1321
1486
|
const parsed = JSON.parse(text);
|
|
@@ -1323,7 +1488,12 @@ function errorCodeOf(text) {
|
|
|
1323
1488
|
const extError = parsed["extError"];
|
|
1324
1489
|
if (typeof extError === "object" && extError !== null && !Array.isArray(extError)) {
|
|
1325
1490
|
const code = extError["code"];
|
|
1326
|
-
|
|
1491
|
+
const param = extError["param"];
|
|
1492
|
+
if (typeof code === "string") return typeof param === "string" ? {
|
|
1493
|
+
errorCode: code,
|
|
1494
|
+
detail: code,
|
|
1495
|
+
param
|
|
1496
|
+
} : {
|
|
1327
1497
|
errorCode: code,
|
|
1328
1498
|
detail: code
|
|
1329
1499
|
};
|
|
@@ -1498,6 +1668,11 @@ function modelWithCurrentPromotion(model, now = Date.now()) {
|
|
|
1498
1668
|
* merged: existing messages keep their order and wording. A body that is not a
|
|
1499
1669
|
* JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
|
|
1500
1670
|
* so this is safe to run over an already-prepared-or-not body.
|
|
1671
|
+
*
|
|
1672
|
+
* This layer now carries *only* that requirement. The `reasoning_effort` strip
|
|
1673
|
+
* used to live here, scoped to this region; issue #13 showed CN rejects the
|
|
1674
|
+
* same spelling, so it moved into the shared {@link prepareChatBody} and this
|
|
1675
|
+
* function no longer touches the effort field at all.
|
|
1501
1676
|
*/
|
|
1502
1677
|
function prepareInternationalChatBody(source) {
|
|
1503
1678
|
const prepared = prepareChatBody(source);
|
|
@@ -1508,7 +1683,6 @@ function prepareInternationalChatBody(source) {
|
|
|
1508
1683
|
return prepared;
|
|
1509
1684
|
}
|
|
1510
1685
|
if (!isObject(body)) return prepared;
|
|
1511
|
-
dropUnsupportedEffort(body);
|
|
1512
1686
|
const messages = body["messages"];
|
|
1513
1687
|
if (!Array.isArray(messages)) return JSON.stringify(body);
|
|
1514
1688
|
const first = messages[0];
|
|
@@ -1520,25 +1694,35 @@ function prepareInternationalChatBody(source) {
|
|
|
1520
1694
|
return JSON.stringify(body);
|
|
1521
1695
|
}
|
|
1522
1696
|
/**
|
|
1523
|
-
* Remove the adapter's own `off` effort spelling from the
|
|
1524
|
-
*
|
|
1525
|
-
* `thinkingLevelMap.off` is pinned to the literal `'off'`
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
* `
|
|
1533
|
-
*
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1697
|
+
* Remove the adapter's own `off` effort spelling from the wire, in both regions.
|
|
1698
|
+
*
|
|
1699
|
+
* `thinkingLevelMap.off` is pinned to the literal `'off'` so the level stays
|
|
1700
|
+
* selectable in the picker — pi-ai exposes a level only when its entry is a
|
|
1701
|
+
* string — but that same entry is also the value pi-ai puts on the wire, both
|
|
1702
|
+
* for an explicit Off selection and for *every* request that carries no level
|
|
1703
|
+
* at all. `'off'` is a picker name, not an upstream spelling, and both
|
|
1704
|
+
* endpoints reject it:
|
|
1705
|
+
*
|
|
1706
|
+
* - international GPT family: HTTP 400 `11133` / `extError.param ===
|
|
1707
|
+
* 'reasoning.effort'` (issue #49);
|
|
1708
|
+
* - CN: the same 400 `11133` on every `canDisableThinking` model — measured
|
|
1709
|
+
* on `deepseek-v4.1-flash`, where the author also confirmed `'disabled'`
|
|
1710
|
+
* is rejected and `'none'` answers 200 (issue #13). This is what made the
|
|
1711
|
+
* earlier international-only scope decision obsolete.
|
|
1712
|
+
*
|
|
1713
|
+
* Omission is the only form measured good on every affected model, so it is
|
|
1714
|
+
* the only form this function can produce; a literal `'none'` is *not* a safe
|
|
1715
|
+
* substitute (accepted by the GPT-5.6 family and GLM, rejected by
|
|
1716
|
+
* `gpt-6-astra`, and silently ignored by `kimi-k2.8-preview`). Declared
|
|
1717
|
+
* spellings (`low`/`medium`/`high`/`xhigh`/`max`) and an explicit `none` pass
|
|
1718
|
+
* through untouched — the adapter does not decide a user's `none` for them.
|
|
1719
|
+
*
|
|
1720
|
+
* Consequences, stated honestly: the picker still offers Off in both regions,
|
|
1721
|
+
* but selecting it means "the field is omitted". The model's actual behaviour
|
|
1722
|
+
* is then decided upstream — *not* guaranteed to disable thinking and not
|
|
1723
|
+
* guaranteed to match the catalog's `defaultEffort`. The plugin used to claim
|
|
1724
|
+
* more than that on CN, where `'off'` was merely accepted (accepted never
|
|
1725
|
+
* meant effective); #13 is why the claim is withdrawn rather than reworded.
|
|
1542
1726
|
*/
|
|
1543
1727
|
function dropUnsupportedEffort(obj) {
|
|
1544
1728
|
if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
|
|
@@ -3688,7 +3872,7 @@ var WorkBuddyCatalog = class {
|
|
|
3688
3872
|
};
|
|
3689
3873
|
//#endregion
|
|
3690
3874
|
//#region src/version.ts
|
|
3691
|
-
const WORKBUDDY_BRIDGE_VERSION = "0.5.
|
|
3875
|
+
const WORKBUDDY_BRIDGE_VERSION = "0.5.2";
|
|
3692
3876
|
//#endregion
|
|
3693
3877
|
//#region src/web/heartbeat.ts
|
|
3694
3878
|
/**
|
|
@@ -3859,4 +4043,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
3859
4043
|
return startAtMs <= heartbeat.registeredAt;
|
|
3860
4044
|
}
|
|
3861
4045
|
//#endregion
|
|
3862
|
-
export { parseModelCatalog as A,
|
|
4046
|
+
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,13 +620,22 @@ 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
|
-
*
|
|
623
|
+
* rewrite `developer` messages as `system`, neutralize the channel signature
|
|
624
|
+
* (see {@link neutralizeChannelSignature}), and drop the adapter's own `off`
|
|
625
|
+
* effort spelling (see {@link dropUnsupportedEffort}).
|
|
617
626
|
*
|
|
618
627
|
* The `developer` rewrite is load-bearing: pi-ai emits the system prompt as
|
|
619
628
|
* `role: "developer"` (the OpenAI convention it adopted), but the WorkBuddy
|
|
620
629
|
* upstream rejects that role with HTTP 400 code 11128 ("Illegal API
|
|
621
630
|
* invocation from an unapproved channel"). Rewriting to `system` is the
|
|
622
631
|
* compatible spelling the upstream accepts.
|
|
632
|
+
*
|
|
633
|
+
* Every transformation here is region-agnostic on purpose. The two endpoints
|
|
634
|
+
* differ in what they *add* (the international one also needs a leading system
|
|
635
|
+
* prompt — see {@link prepareInternationalChatBody}), not in what they reject:
|
|
636
|
+
* both refuse the picker's `off` spelling, which is why that strip lives here
|
|
637
|
+
* rather than in the international layer (issue #13 reversed the earlier
|
|
638
|
+
* international-only scope decision, which rested on CN accepting it).
|
|
623
639
|
*/
|
|
624
640
|
declare function prepareChatBody(source: string): string;
|
|
625
641
|
/** Provenance of one successful catalog fetch, surfaced by the status card. */
|
|
@@ -660,7 +676,20 @@ declare class WorkBuddyUpstreamClient {
|
|
|
660
676
|
/** Provenance of the most recent successful catalog fetch, for the card. */
|
|
661
677
|
lastCatalog: WorkBuddyCatalogFetch | undefined;
|
|
662
678
|
constructor(options?: WorkBuddyUpstreamClientOptions);
|
|
663
|
-
/**
|
|
679
|
+
/**
|
|
680
|
+
* POST the chat endpoint; a successful answer is the raw SSE response.
|
|
681
|
+
*
|
|
682
|
+
* This is the one place every chat request converges on (the loopback shim,
|
|
683
|
+
* the host LLM route, and any direct caller all end up here), so it is also
|
|
684
|
+
* where the body is normalized for the upstream — in *both* regions. The
|
|
685
|
+
* region only decides which extra requirement applies on top: the
|
|
686
|
+
* international gateway additionally demands a leading system message. It
|
|
687
|
+
* used to decide whether the body was normalized at all, which left the CN
|
|
688
|
+
* path sending the picker's `off` spelling verbatim (issue #13).
|
|
689
|
+
*
|
|
690
|
+
* Normalization is idempotent, so a caller that already prepared the body
|
|
691
|
+
* (the shim does) sees no second change.
|
|
692
|
+
*/
|
|
664
693
|
chatStream(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyChatResult>;
|
|
665
694
|
/** POST the token-refresh endpoint; the caller merges the outcome. */
|
|
666
695
|
refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
|
|
@@ -833,6 +862,11 @@ declare function modelWithCurrentPromotion(model: WorkBuddyUpstreamModel, now?:
|
|
|
833
862
|
* merged: existing messages keep their order and wording. A body that is not a
|
|
834
863
|
* JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
|
|
835
864
|
* so this is safe to run over an already-prepared-or-not body.
|
|
865
|
+
*
|
|
866
|
+
* This layer now carries *only* that requirement. The `reasoning_effort` strip
|
|
867
|
+
* used to live here, scoped to this region; issue #13 showed CN rejects the
|
|
868
|
+
* same spelling, so it moved into the shared {@link prepareChatBody} and this
|
|
869
|
+
* function no longer touches the effort field at all.
|
|
836
870
|
*/
|
|
837
871
|
declare function prepareInternationalChatBody(source: string): string;
|
|
838
872
|
//#endregion
|
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-6xWs7ak3.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";
|
|
@@ -446,6 +446,13 @@ function withCatalogDisplay(name, info) {
|
|
|
446
446
|
* The offered set is described internally as "verified accepted", never as
|
|
447
447
|
* "verified effective": acceptance proves the upstream did not reject the
|
|
448
448
|
* spelling, not that it changes what the model does.
|
|
449
|
+
*
|
|
450
|
+
* `off` is a picker name, not a wire spelling. It stays pinned to the literal
|
|
451
|
+
* `'off'` (a `null` here would remove the level from the picker entirely), and
|
|
452
|
+
* the shared preparation in `src/protocol/client.ts` strips that exact value
|
|
453
|
+
* before the request leaves the plugin — in both regions, because both
|
|
454
|
+
* endpoints reject it (issues #49 and #13). Selecting Off therefore means "send
|
|
455
|
+
* no `reasoning_effort`", and what the model then does is upstream's call.
|
|
449
456
|
*/
|
|
450
457
|
function reasoningFields(info, observed) {
|
|
451
458
|
const resolved = resolveEfforts(info, observed);
|
|
@@ -798,6 +805,25 @@ function writeOpenAIError(res, status, kind, message) {
|
|
|
798
805
|
code: kind
|
|
799
806
|
} });
|
|
800
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
|
+
}
|
|
801
827
|
/** Read a request body with a size cap; over-limit bodies fail the request. */
|
|
802
828
|
function readBody$1(req) {
|
|
803
829
|
return new Promise((resolve, reject) => {
|
|
@@ -909,7 +935,7 @@ function createWorkBuddyShim(options) {
|
|
|
909
935
|
req.on("close", () => controller.abort());
|
|
910
936
|
const result = await client.chatStream(credential, prepared, controller.signal);
|
|
911
937
|
if (!result.ok) {
|
|
912
|
-
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind,
|
|
938
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, failureMessage(result));
|
|
913
939
|
return;
|
|
914
940
|
}
|
|
915
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.2",
|
|
6
6
|
"author": "zlZayn",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"react-dom": "^18.2.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
|
-
"@deepseek-ai/cordis": "4.0.4",
|
|
100
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
101
101
|
"@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
102
102
|
"@deepseek-ai/dsh-atomic-write": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
103
103
|
"@deepseek-ai/dsh-attachment": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
120
120
|
"@deepseek-ai/dsh-util-code-language": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
121
121
|
"@deepseek-ai/dsh-util-workspace-path": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
122
|
-
"@deepseek-ai/schemastery": "3.18.
|
|
122
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
123
123
|
"@earendil-works/pi-ai": "^1.1.0",
|
|
124
124
|
"@eslint/js": "^10.0.1",
|
|
125
125
|
"@shikijs/langs": "^4.4.3",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"micromark-util-symbol": "^2.0.1",
|
|
151
151
|
"prettier": "^3.9.9",
|
|
152
152
|
"react": "^18.2.0",
|
|
153
|
-
"react-dom": "^18.
|
|
153
|
+
"react-dom": "^18.2.0",
|
|
154
154
|
"react-test-renderer": "18.3.1",
|
|
155
155
|
"shiki": "^4.4.3",
|
|
156
156
|
"simple-icons": "^16.32.0",
|