dsh-workbuddy-bridge 0.5.3 → 0.5.5
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-BJrYAacX.js → heartbeat-DYtNMdMm.js} +111 -7
- package/lib/index.js +19 -4
- package/package.json +5 -3
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, Z 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-DYtNMdMm.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/cli/bin.ts
|
|
@@ -839,6 +839,37 @@ function commonHeaders(credential) {
|
|
|
839
839
|
};
|
|
840
840
|
}
|
|
841
841
|
/**
|
|
842
|
+
* Per-request channel identity headers the international gateway verifies.
|
|
843
|
+
*
|
|
844
|
+
* Measured 2026-10-23 against `www.workbuddy.ai/v2/chat/completions`: the same
|
|
845
|
+
* account, credential, UA and body answer `400` / `11128` ("request illegal",
|
|
846
|
+
* channel verification) without these headers, and `200` SSE with them. The
|
|
847
|
+
* official desktop app sends this trace/conversation quintet on every chat
|
|
848
|
+
* request; the CN gateway accepts requests without it, so the injection stays
|
|
849
|
+
* global-region-only to keep the CN wire byte-identical to what passes today.
|
|
850
|
+
*
|
|
851
|
+
* `X-Trace-ID` carries a bare 32-hex id (the OTel trace-id form the app uses);
|
|
852
|
+
* the other four are UUIDs minted per request, exactly like the app does.
|
|
853
|
+
*/
|
|
854
|
+
function channelIdentityHeaders() {
|
|
855
|
+
const uuid = () => {
|
|
856
|
+
const b = randomBytes(16);
|
|
857
|
+
const b6 = b[6] ?? 0;
|
|
858
|
+
const b8 = b[8] ?? 0;
|
|
859
|
+
b[6] = b6 & 15 | 64;
|
|
860
|
+
b[8] = b8 & 63 | 128;
|
|
861
|
+
const h = b.toString("hex");
|
|
862
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
863
|
+
};
|
|
864
|
+
return {
|
|
865
|
+
"X-Trace-ID": randomBytes(16).toString("hex"),
|
|
866
|
+
"X-Request-ID": uuid(),
|
|
867
|
+
"X-Conversation-ID": uuid(),
|
|
868
|
+
"X-Conversation-Request-ID": uuid(),
|
|
869
|
+
"X-Conversation-Message-ID": uuid()
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
842
873
|
* Chat request headers, including the X-No-* conventions the official CLI uses.
|
|
843
874
|
*
|
|
844
875
|
* `userAgent` carries the desktop identity for chat and probe requests; when
|
|
@@ -853,7 +884,8 @@ function chatHeaders(credential, userAgent) {
|
|
|
853
884
|
...credential.uid === "" ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
|
|
854
885
|
...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
|
|
855
886
|
...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
|
|
856
|
-
"X-Product": "SaaS"
|
|
887
|
+
"X-Product": "SaaS",
|
|
888
|
+
...regionOf(credential.domain) === "global" ? channelIdentityHeaders() : {}
|
|
857
889
|
};
|
|
858
890
|
}
|
|
859
891
|
/** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
|
|
@@ -1017,11 +1049,67 @@ function neutralizeAssistantExtras(message) {
|
|
|
1017
1049
|
if (typeof args === "string") fn["arguments"] = neutralizeSignatureText(args);
|
|
1018
1050
|
}
|
|
1019
1051
|
}
|
|
1020
|
-
/**
|
|
1052
|
+
/**
|
|
1053
|
+
* Rewrite the measured channel-gate fingerprints in one text blob.
|
|
1054
|
+
*
|
|
1055
|
+
* Exported for tests and for callers that assemble a body themselves.
|
|
1056
|
+
* Idempotent: every `to` differs from its `from` and recreates no other `from`.
|
|
1057
|
+
*/
|
|
1058
|
+
function rewriteChannelGateSentences(text) {
|
|
1059
|
+
let out = text;
|
|
1060
|
+
for (const [from, to] of [...IDENTITY_REWRITES, ...FILE_REFERENCE_REWRITES]) if (out.includes(from)) out = out.split(from).join(to);
|
|
1061
|
+
return out;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Apply the signature rewrites to one string; see {@link neutralizeChannelSignature}.
|
|
1065
|
+
*
|
|
1066
|
+
* Every rewrite is a **real wording change**. The zero-width-space variant this
|
|
1067
|
+
* function used until 2026-10-29 is retired: both reference implementations
|
|
1068
|
+
* measured that the CN gateway normalizes invisible characters before matching,
|
|
1069
|
+
* so a U+200B separator was equivalent to not rewriting at all.
|
|
1070
|
+
*/
|
|
1021
1071
|
function neutralizeSignatureText(text) {
|
|
1022
|
-
return text.replace(CHANNEL_OPENER, "$1You are an AI agent.").replace(CHANNEL_SIGNATURE_PHRASE,
|
|
1072
|
+
return rewriteChannelGateSentences(text.replace(CHANNEL_OPENER, "$1You are an AI agent.").replace(CHANNEL_SIGNATURE_PHRASE, "the agent runtime"));
|
|
1023
1073
|
}
|
|
1024
1074
|
/**
|
|
1075
|
+
* DSH's identity lines. The gate matches each one **independently** (measured
|
|
1076
|
+
* 2026-10-29 by two probes on the sibling plugin), so a body whose brand is
|
|
1077
|
+
* already neutralized still fails on these.
|
|
1078
|
+
*/
|
|
1079
|
+
const IDENTITY_REWRITES = [
|
|
1080
|
+
["You are a helpful AI assistant.", "You are a capable AI helper."],
|
|
1081
|
+
["You are an AI assistant", "You are an AI agent."],
|
|
1082
|
+
["You are a coding agent powered by the ", "You are a coding agent running on the "],
|
|
1083
|
+
["You are interacting with the user through the DeepSeek Harness web interface", "You are working with the user inside the DeepSeek Harness web interface"],
|
|
1084
|
+
["Agent Teams is available in this session", "Agent Teams is enabled for this session"]
|
|
1085
|
+
];
|
|
1086
|
+
/**
|
|
1087
|
+
* The @-path paragraph `@deepseek-ai/dsh-file-reference` injects into every
|
|
1088
|
+
* system prompt — a **combination** fingerprint, not one magic sentence:
|
|
1089
|
+
* every sentence alone answers 200, the paragraph answers 400, and rewriting
|
|
1090
|
+
* one sentence breaks it (all three measured on 2026-10-27→29 by the two
|
|
1091
|
+
* reference plugins). Hence one row per sentence: a single whole-paragraph
|
|
1092
|
+
* literal would stop matching the moment the host rewords one word.
|
|
1093
|
+
*
|
|
1094
|
+
* The `from` strings come from the upstream reports' wording. A capture of THIS
|
|
1095
|
+
* host (issue #25 diagnostic, `~/.dsh/workbuddy-sent-body.json`) shows it
|
|
1096
|
+
* already ships several of the neutralized forms, so most rows are no-ops here
|
|
1097
|
+
* and are kept for hosts that still send the originals. Only `from` strings a
|
|
1098
|
+
* measurement actually produced appear in the table — an unmatched sentence
|
|
1099
|
+
* passes through byte-identical rather than being guessed at.
|
|
1100
|
+
*/
|
|
1101
|
+
const FILE_REFERENCE_REWRITES = [
|
|
1102
|
+
["Paths prefixed with @ point to files the user referenced.", "Tokens starting with @ are locations the user explicitly named."],
|
|
1103
|
+
["Relative paths resolve against the working directory;", "Relative paths are resolved against the workspace root;"],
|
|
1104
|
+
["absolute paths identify files or directories on the host.", "absolute paths point at files or directories on the host."],
|
|
1105
|
+
["A trailing slash marks a directory:", "A trailing slash indicates a directory:"],
|
|
1106
|
+
["list it when its contents matter.", "list it whenever its contents matter."],
|
|
1107
|
+
["Anything else is a file:", "Everything else is a file:"],
|
|
1108
|
+
["use the read tool when its contents are needed,", "use the read tool whenever you need its contents,"],
|
|
1109
|
+
["and do not claim to have inspected it before reading.", "and never claim to have inspected it before reading it."],
|
|
1110
|
+
["@\"...\" quotes a path containing spaces.", "A path containing spaces is quoted with @\"...\"."]
|
|
1111
|
+
];
|
|
1112
|
+
/**
|
|
1025
1113
|
* The opener DSH puts at the head of every system prompt.
|
|
1026
1114
|
*
|
|
1027
1115
|
* Leading whitespace and a BOM are tolerated (group 1 carries them through to
|
|
@@ -1037,8 +1125,24 @@ const CHANNEL_OPENER = /^([\s\uFEFF]*)You\s+are\s+an\s+AI\s+agent\s+powered\s+by
|
|
|
1037
1125
|
* sentence reads identically.
|
|
1038
1126
|
*/
|
|
1039
1127
|
const CHANNEL_SIGNATURE_PHRASE = /(?<![\\/])DeepSeek([\s\u00A0]+)Harness(?![\\/])/gi;
|
|
1040
|
-
/**
|
|
1041
|
-
|
|
1128
|
+
/**
|
|
1129
|
+
* The upstream refusal that means "content filter", not "bad request".
|
|
1130
|
+
*
|
|
1131
|
+
* Two shapes reach us and both must be recognised:
|
|
1132
|
+
*
|
|
1133
|
+
* - the documented gate form, `{"code":11-128,"msg":"Illegal API invocation…"}`;
|
|
1134
|
+
* - the shape issue #25 actually renders (reproduced 2026-10-29):
|
|
1135
|
+
* `{"message":"请求被拦截,请重新发送","type":"client","code":"client"}` — **no
|
|
1136
|
+
* `11-128` anywhere**, so a code-only test misses the exact case the hint
|
|
1137
|
+
* exists for.
|
|
1138
|
+
*/
|
|
1139
|
+
const CHANNEL_GATE_CODE = /\b11-128\b/;
|
|
1140
|
+
/** The CN gateway's user-facing block copy, which survives even when the code does not. */
|
|
1141
|
+
const CHANNEL_GATE_COPY = /请求被拦截/;
|
|
1142
|
+
/** True when an upstream body is a channel-gate refusal; see {@link CHANNEL_GATE_CODE}. */
|
|
1143
|
+
function isChannelGateBody(body) {
|
|
1144
|
+
return CHANNEL_GATE_CODE.test(body) || CHANNEL_GATE_COPY.test(body);
|
|
1145
|
+
}
|
|
1042
1146
|
/** Rewrite `role: "developer"` messages to `role: "system"` (upstream rejects developer). */
|
|
1043
1147
|
function normalizeDeveloperRole(obj) {
|
|
1044
1148
|
const messages = obj["messages"];
|
|
@@ -3917,7 +4021,7 @@ var WorkBuddyCatalog = class {
|
|
|
3917
4021
|
};
|
|
3918
4022
|
//#endregion
|
|
3919
4023
|
//#region src/version.ts
|
|
3920
|
-
const WORKBUDDY_BRIDGE_VERSION = "0.5.
|
|
4024
|
+
const WORKBUDDY_BRIDGE_VERSION = "0.5.5";
|
|
3921
4025
|
//#endregion
|
|
3922
4026
|
//#region src/web/heartbeat.ts
|
|
3923
4027
|
/**
|
|
@@ -4088,4 +4192,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
4088
4192
|
return startAtMs <= heartbeat.registeredAt;
|
|
4089
4193
|
}
|
|
4090
4194
|
//#endregion
|
|
4091
|
-
export {
|
|
4195
|
+
export { normalizeCredits as A, CN_APP_VERSION_FILENAME as B, variantFor as C, WorkBuddyUpstreamClient as D, WORKBUDDY_STATUS_PATH as E, PROBE_EFFORT_CANDIDATES as F, resolveChatIdentity as G, chatUserAgent as H, probeModel as I, appUserAgent as J, validCliVersion as K, randomSentinel as L, prepareChatBody as M, prepareInternationalChatBody as N, isChannelGateBody as O, regionOf as P, validAppVersion as Q, classifyUpstreamError as R, WORKBUDDY_VARIANTS as S, WORKBUDDY_PROBE_PATH as T, fallbackChatIdentity as U, FALLBACK_CN_APP_VERSION as V, readCliVersion as W, readBundleVersion as X, installedAppVersion as Y, resolveAppVersion 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, parseModelCatalog as j, modelWithCurrentPromotion as k, FALLBACK_WORKBUDDY_AI_MODELS as l, WorkBuddyCredentialStore as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, WORKBUDDY_AUTH_FILE_ENV as p, WORKBUDDY_APP_VERSION_FILENAME 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, upstreamDisplayMessage as z };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as normalizeCredits, B as CN_APP_VERSION_FILENAME, C as variantFor, D as WorkBuddyUpstreamClient, F as PROBE_EFFORT_CANDIDATES, G as resolveChatIdentity, H as chatUserAgent, I as probeModel, J as appUserAgent, K as validCliVersion, L as randomSentinel, M as prepareChatBody, N as prepareInternationalChatBody, O as isChannelGateBody, P as regionOf, Q as validAppVersion, R as classifyUpstreamError, S as WORKBUDDY_VARIANTS, U as fallbackChatIdentity, V as FALLBACK_CN_APP_VERSION, W as readCliVersion, X as readBundleVersion, Y as installedAppVersion, Z 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 parseModelCatalog, k as modelWithCurrentPromotion, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as WORKBUDDY_APP_VERSION_FILENAME, 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 upstreamDisplayMessage } from "./heartbeat-DYtNMdMm.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";
|
|
@@ -790,6 +790,15 @@ const KIND_STATUS = {
|
|
|
790
790
|
server: 502,
|
|
791
791
|
client: 400
|
|
792
792
|
};
|
|
793
|
+
/**
|
|
794
|
+
* What to do when the gateway refuses a request on content (code `11-128`).
|
|
795
|
+
*
|
|
796
|
+
* Two actions only, and the copy keeps the code spelled `11-128` so that the
|
|
797
|
+
* guidance itself, echoed back into a later request, cannot re-arm the gate.
|
|
798
|
+
* No status digits (see {@link failureMessage}) and no retry advice: retrying
|
|
799
|
+
* the same poisoned conversation reproduces the refusal.
|
|
800
|
+
*/
|
|
801
|
+
const CHANNEL_GATE_HINT = " — 渠道校验拦截(11-128)。请新开会话重试;若持续出现,先更新插件 / blocked by channel verification (11-128): start a new conversation, and update the plugin if it persists";
|
|
793
802
|
function writeJson(res, status, body) {
|
|
794
803
|
const payload = JSON.stringify(body);
|
|
795
804
|
res.writeHead(status, {
|
|
@@ -818,14 +827,20 @@ function writeOpenAIError(res, status, kind, message) {
|
|
|
818
827
|
* The upstream's own `displayMsg` wins over the raw body: it is the sentence
|
|
819
828
|
* written for a human ("账号存在异常行为,已被限制使用"), while the body is a JSON
|
|
820
829
|
* document whose `msg` is often a machine phrase such as `request illegal`.
|
|
830
|
+
*
|
|
831
|
+
* The channel-gate code (`11-128`) appends {@link CHANNEL_GATE_HINT}: its raw
|
|
832
|
+
* body is `{"message":"请求被拦截,请重新发送","type":"client","code":"client"}`,
|
|
833
|
+
* which on its own reads as a transient hiccup the user should simply retry —
|
|
834
|
+
* the two actions that actually help (a new conversation; update the plugin)
|
|
835
|
+
* are not guessable from it.
|
|
821
836
|
*/
|
|
822
837
|
function failureMessage(result) {
|
|
823
838
|
const reason = upstreamDisplayMessage(result.message) ?? result.message.slice(0, 400);
|
|
824
839
|
const head = `workbuddy upstream ${result.kind}`;
|
|
825
|
-
|
|
840
|
+
const line = result.kind === "session_dead" ? `${head} (http ${result.status}): ${reason}` : `${head}: ${reason}`;
|
|
841
|
+
return isChannelGateBody(result.message) ? `${line}${CHANNEL_GATE_HINT}` : line;
|
|
826
842
|
}
|
|
827
|
-
/** Read a request body with a size cap; over-limit bodies fail the request. */
|
|
828
|
-
function readBody$1(req) {
|
|
843
|
+
/** Read a request body with a size cap; over-limit bodies fail the request. */ function readBody$1(req) {
|
|
829
844
|
return new Promise((resolve, reject) => {
|
|
830
845
|
const chunks = [];
|
|
831
846
|
let size = 0;
|
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.5",
|
|
6
6
|
"author": "zlZayn",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh",
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
91
91
|
"@deepseek-ai/dsh-llm": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
92
92
|
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
93
|
+
"@deepseek-ai/dsh-session": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
93
94
|
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
94
95
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
95
96
|
"@earendil-works/pi-ai": "^1.1.0",
|
|
@@ -116,6 +117,7 @@
|
|
|
116
117
|
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
117
118
|
"@deepseek-ai/dsh-llm": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
118
119
|
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
120
|
+
"@deepseek-ai/dsh-session": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
119
121
|
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
120
122
|
"@deepseek-ai/dsh-util-code-language": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
121
123
|
"@deepseek-ai/dsh-util-workspace-path": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
|
|
@@ -127,7 +129,7 @@
|
|
|
127
129
|
"@types/react": "~18.3.1",
|
|
128
130
|
"@types/react-dom": "^18.3.7",
|
|
129
131
|
"@types/react-test-renderer": "^18.3.1",
|
|
130
|
-
"@vitest/coverage-v8": "^4.1.
|
|
132
|
+
"@vitest/coverage-v8": "^4.1.12",
|
|
131
133
|
"anser": "^2.3.5",
|
|
132
134
|
"clsx": "^2.1.1",
|
|
133
135
|
"diff": "^9.0.0",
|
|
@@ -158,7 +160,7 @@
|
|
|
158
160
|
"typescript": "^5.9.3",
|
|
159
161
|
"typescript-eslint": "^8.70.1",
|
|
160
162
|
"unrun": "^0.3.1",
|
|
161
|
-
"vitest": "^4.1.
|
|
163
|
+
"vitest": "^4.1.12",
|
|
162
164
|
"zustand": "^5.0.15"
|
|
163
165
|
},
|
|
164
166
|
"scripts": {
|