dsh-connect-workbuddy 5.8.1 → 5.8.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/CHANGELOG.md +66 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +347 -8
- package/lib/{host-heartbeat-CM-09T4U.js → host-heartbeat-DzfWuVsm.js} +147 -13
- package/lib/index.d.ts +61 -1
- package/lib/index.js +189 -40
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as
|
|
1
|
+
import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as isEncryptedFieldWrapper, B as WorkBuddyCredentialRejectedError, Bt as writeFileAtomic, C as WorkBuddyCredentialStore, Ct as WORKBUDDY_APP_EXECUTABLE_ENV, D as defaultDesktopAuthDirs, Dt as fetchAtRestKeyPayload, E as defaultDesktopAuthCandidates, Et as deriveAtRestKeyId, F as workbuddyAccountId, Ft as readAtRestKey, G as forceNeutralHead, H as channelGateMemberOf, I as workbuddyOwnAuthPath, It as workbuddyAppExecutableCandidates, J as isCredentialRejectedError, K as isContextTooLongBody, L as CONTEXT_OVERFLOW_MARKER, M as legacyWorkbuddyOwnAuthPath, Mt as macosBundleExecutable, N as parseWorkBuddyAuth, Nt as macosNestedAppBundles, O as defaultDesktopAuthPath, Ot as findWorkbuddyAppExecutable, P as preferredAccountName, Pt as openEncryptedField, Q as openingEvidenceOfBody, R as CREDENTIAL_REJECTED_CODE, Rt as resolveDshHome, S as WORKBUDDY_AUTH_FILE_ENV, St as nativeModalityOf, T as authFileName, Tt as deriveAtRestKey, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, W as declaredTools, X as knownRegionOf, Y as isFirstMessageGateBody, Z as modelIdOfBody, _ as deriveCatalog, _t as pickImageModel, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as NATIVE_MODALITY_BY_MODEL_ID, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as WorkBuddyImageError, h as WorkBuddyCatalog, ht as DEFAULT_IMAGE_SIZE, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as isWorkbuddyBundle, k as hasEncryptedCredentialFields, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseQueueAnswer, w as WorkBuddyEncryptedCredentialError, wt as clearAtRestKeyCache, x as WORKBUDDY_AUTH_FILENAME, xt as imageDefaultFor, y as withoutRouters, yt as queueNoticeText, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as withFileLock } from "./host-heartbeat-DzfWuVsm.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -4229,6 +4229,8 @@ function createWorkBuddyShim(options) {
|
|
|
4229
4229
|
* silent difference after an upgrade.
|
|
4230
4230
|
*/
|
|
4231
4231
|
const keepPromptLastEnabled = options.keepPromptLast === true;
|
|
4232
|
+
const extraInjectedPrefixes = Array.isArray(options.extraInjectedPrefixes) ? options.extraInjectedPrefixes.filter((entry) => typeof entry === "string" && entry !== "") : [];
|
|
4233
|
+
const sessionTitleOptions = options.sessionTitle ?? {};
|
|
4232
4234
|
const SHARED_SECRET = randomBytes(32).toString("base64url");
|
|
4233
4235
|
/**
|
|
4234
4236
|
* Whether this process's prompts have been PROVEN to need the invisible
|
|
@@ -4382,7 +4384,10 @@ function createWorkBuddyShim(options) {
|
|
|
4382
4384
|
return;
|
|
4383
4385
|
}
|
|
4384
4386
|
const raw = (await readBody(req)).toString("utf8");
|
|
4385
|
-
const prepared = prepareChatBody(raw, {
|
|
4387
|
+
const prepared = rewriteSessionTitleModel(prepareChatBody(raw, {
|
|
4388
|
+
keepPromptLast: keepPromptLastEnabled,
|
|
4389
|
+
extraInjectedPrefixes
|
|
4390
|
+
}), sessionTitleOptions);
|
|
4386
4391
|
const requestModelId = modelIdOfBody(prepared);
|
|
4387
4392
|
if (onRequestBody !== void 0) try {
|
|
4388
4393
|
onRequestBody(prepared);
|
|
@@ -5956,12 +5961,77 @@ function isInside(parent, child) {
|
|
|
5956
5961
|
if (inner === outer) return true;
|
|
5957
5962
|
return inner.startsWith(outer.endsWith(sep) ? outer : outer + sep);
|
|
5958
5963
|
}
|
|
5959
|
-
/** The session directory this
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5964
|
+
/** The session directory this call came from, when the Host supplies one.
|
|
5965
|
+
*
|
|
5966
|
+
* THE ORDER INSIDE THIS FUNCTION IS THE FIX. The session's live projection is
|
|
5967
|
+
* read FIRST, because it is the directory the human and the model are actually
|
|
5968
|
+
* in; the session header is the fallback, because it is where that projection
|
|
5969
|
+
* starts from (`init: header => header.cwd ?? null`) and because a session that
|
|
5970
|
+
* never committed a projection has nothing else.
|
|
5971
|
+
*
|
|
5972
|
+
* Both sources are re-validated here: the session service already rejects
|
|
5973
|
+
* relative headers, but a hand-built agent in a test (or a Host with a looser
|
|
5974
|
+
* contract) must not be able to smuggle a relative path through either one.
|
|
5975
|
+
*/
|
|
5976
|
+
function sessionDirOf(execution, directoryOf) {
|
|
5977
|
+
const dirs = sessionDirCandidates(execution, directoryOf);
|
|
5978
|
+
return dirs.projection !== "" ? dirs.projection : dirs.header;
|
|
5979
|
+
}
|
|
5980
|
+
/**
|
|
5981
|
+
* Both session-directory sources, kept APART so the caller can tell them — and
|
|
5982
|
+
* therefore fall back between them — by validity, not just by presence.
|
|
5983
|
+
*
|
|
5984
|
+
* {@link sessionDirOf} collapses these into one answer, which is right for "where
|
|
5985
|
+
* are we" but loses the information "the projection said X, the header says Y".
|
|
5986
|
+
* The ladder in {@link resolveWorkingDir} needs the pair: a projection can go
|
|
5987
|
+
* STALE (the directory was deleted, moved, or is an offline mount) while the
|
|
5988
|
+
* creation-time header still points at a perfectly good directory. Without the
|
|
5989
|
+
* split, a stale projection is a hard refusal even though a usable directory is
|
|
5990
|
+
* one field away.
|
|
5991
|
+
*
|
|
5992
|
+
* Both sources are re-validated the same way as in {@link sessionDirOf}: absolute
|
|
5993
|
+
* and non-blank, so a hand-built session cannot smuggle a relative path through
|
|
5994
|
+
* either field.
|
|
5995
|
+
*/
|
|
5996
|
+
function sessionDirCandidates(execution, directoryOf) {
|
|
5997
|
+
const session = execution?.agent?.session;
|
|
5998
|
+
let projection = "";
|
|
5999
|
+
if (session !== void 0 && directoryOf !== void 0) {
|
|
6000
|
+
const live = directoryOf(session);
|
|
6001
|
+
if (typeof live === "string") {
|
|
6002
|
+
const trimmed = live.trim();
|
|
6003
|
+
if (trimmed !== "" && isAbsolute(trimmed)) projection = trimmed;
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
const cwd = session?.header?.cwd;
|
|
6007
|
+
let header = "";
|
|
6008
|
+
if (typeof cwd === "string") {
|
|
6009
|
+
const trimmed = cwd.trim();
|
|
6010
|
+
if (trimmed !== "" && isAbsolute(trimmed)) header = trimmed;
|
|
6011
|
+
}
|
|
6012
|
+
return {
|
|
6013
|
+
projection,
|
|
6014
|
+
header
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
6017
|
+
/**
|
|
6018
|
+
* Why `path` cannot be a turn's working directory, or `''` when it can.
|
|
6019
|
+
*
|
|
6020
|
+
* The three checks the ladder has always applied (exists / is a directory /
|
|
6021
|
+
* readable), stated once so the projection→header fallback in
|
|
6022
|
+
* {@link resolveWorkingDir} re-runs exactly the same gate instead of a weaker
|
|
6023
|
+
* private copy.
|
|
6024
|
+
*/
|
|
6025
|
+
function dirRejection(path) {
|
|
6026
|
+
if (!existsSync(path)) return `working_dir "${path}" 不存在。`;
|
|
6027
|
+
let isDir = false;
|
|
6028
|
+
try {
|
|
6029
|
+
isDir = statSync(path).isDirectory();
|
|
6030
|
+
} catch (error) {
|
|
6031
|
+
return `working_dir "${path}" 无法读取:${error instanceof Error ? error.message : String(error)}`;
|
|
6032
|
+
}
|
|
6033
|
+
if (!isDir) return `working_dir "${path}" 不是一个目录。`;
|
|
6034
|
+
return "";
|
|
5965
6035
|
}
|
|
5966
6036
|
/**
|
|
5967
6037
|
* Decide the directory one turn runs in.
|
|
@@ -5980,8 +6050,10 @@ function sessionDirOf(execution) {
|
|
|
5980
6050
|
* A scratch turn is exempt by construction: it is a fresh empty temp directory
|
|
5981
6051
|
* the bridge just created, so there is nothing in it to leak.
|
|
5982
6052
|
*/
|
|
5983
|
-
function resolveWorkingDir(args, options, execution) {
|
|
5984
|
-
const
|
|
6053
|
+
function resolveWorkingDir(args, options, execution, directoryOf) {
|
|
6054
|
+
const dirs = sessionDirCandidates(execution, directoryOf);
|
|
6055
|
+
const primarySessionDir = dirs.projection !== "" && dirs.projection !== dirs.header ? dirs.projection : dirs.header;
|
|
6056
|
+
const fallbackSessionDir = primarySessionDir !== dirs.header && dirs.header !== "" ? dirs.header : "";
|
|
5985
6057
|
const explicit = typeof args["working_dir"] === "string" ? args["working_dir"].trim() : "";
|
|
5986
6058
|
let chosen = "";
|
|
5987
6059
|
let fromArgument = false;
|
|
@@ -5994,7 +6066,7 @@ function resolveWorkingDir(args, options, execution) {
|
|
|
5994
6066
|
};
|
|
5995
6067
|
chosen = explicit;
|
|
5996
6068
|
fromArgument = true;
|
|
5997
|
-
} else if (
|
|
6069
|
+
} else if (primarySessionDir !== "") chosen = primarySessionDir;
|
|
5998
6070
|
else if (options.workingDir !== "") chosen = options.workingDir;
|
|
5999
6071
|
else if (args["allow_unattended"] === true || options.allowUnattended) return {
|
|
6000
6072
|
ok: true,
|
|
@@ -6008,31 +6080,17 @@ function resolveWorkingDir(args, options, execution) {
|
|
|
6008
6080
|
unattended: false,
|
|
6009
6081
|
reason: "no working directory。宿主目前不把会话 cwd 传给插件工具,所以这个值必须由调用方给:① 传 working_dir 指定一个已存在的绝对路径目录;② 在插件配置里设 cliBridge.workingDir;③ 若本回合只是通用问答、不需要任何目录,传 allow_unattended: true(会在一个临时目录里跑,该目录是它的读写边界,不碰你的 home 或会话工作区)。"
|
|
6010
6082
|
};
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
unattended: false,
|
|
6015
|
-
reason: `working_dir "${chosen}" 不存在。`
|
|
6016
|
-
};
|
|
6017
|
-
let isDir = false;
|
|
6018
|
-
try {
|
|
6019
|
-
isDir = statSync(chosen).isDirectory();
|
|
6020
|
-
} catch (error) {
|
|
6021
|
-
return {
|
|
6022
|
-
ok: false,
|
|
6023
|
-
dir: "",
|
|
6024
|
-
unattended: false,
|
|
6025
|
-
reason: `working_dir "${chosen}" 无法读取:${error instanceof Error ? error.message : String(error)}`
|
|
6026
|
-
};
|
|
6027
|
-
}
|
|
6028
|
-
if (!isDir) return {
|
|
6083
|
+
const rejection = dirRejection(chosen);
|
|
6084
|
+
if (rejection !== "" && !fromArgument && chosen === primarySessionDir && fallbackSessionDir !== "" && dirRejection(fallbackSessionDir) === "") chosen = fallbackSessionDir;
|
|
6085
|
+
else if (rejection !== "") return {
|
|
6029
6086
|
ok: false,
|
|
6030
6087
|
dir: "",
|
|
6031
6088
|
unattended: false,
|
|
6032
|
-
reason:
|
|
6089
|
+
reason: !fromArgument && chosen === primarySessionDir && fallbackSessionDir !== "" && dirRejection(fallbackSessionDir) !== "" ? `${rejection}(回退的会话目录 "${fallbackSessionDir}" 同样不可用,工作目录投影与会话 header 都失效了。)` : rejection
|
|
6033
6090
|
};
|
|
6034
6091
|
const roots = [];
|
|
6035
|
-
if (
|
|
6092
|
+
if (primarySessionDir !== "") roots.push(primarySessionDir);
|
|
6093
|
+
if (fallbackSessionDir !== "") roots.push(fallbackSessionDir);
|
|
6036
6094
|
if (options.workingDir !== "") roots.push(options.workingDir);
|
|
6037
6095
|
roots.push(...options.allowedDirs);
|
|
6038
6096
|
if (!roots.some((root) => root !== "" && isInside(root, chosen))) {
|
|
@@ -6249,7 +6307,7 @@ async function runAsk(args, execution, deps, entrance = "tool") {
|
|
|
6249
6307
|
ok: false,
|
|
6250
6308
|
error: "message 不能为空:要说清楚这一轮让 CodeBuddy 做什么。"
|
|
6251
6309
|
});
|
|
6252
|
-
const dir = resolveWorkingDir(args, options, execution);
|
|
6310
|
+
const dir = resolveWorkingDir(args, options, execution, deps.directoryOf);
|
|
6253
6311
|
if (!dir.ok) return result({
|
|
6254
6312
|
ok: false,
|
|
6255
6313
|
error: dir.reason
|
|
@@ -6497,6 +6555,20 @@ function formatCommandOutcome(value, extra = {}) {
|
|
|
6497
6555
|
return head + denied + footer + truncated + delivery;
|
|
6498
6556
|
}
|
|
6499
6557
|
/**
|
|
6558
|
+
* The command's slice of the invocation the tool's resolver needs.
|
|
6559
|
+
*
|
|
6560
|
+
* The command's agent IS a live agent (the Host's command service passes the
|
|
6561
|
+
* whole thing, not a narrowed subset), so it satisfies `BridgeToolExecution`
|
|
6562
|
+
* structurally. The `signal` field is carried so the shared resolver sees the
|
|
6563
|
+
* same object shape it sees from a tool call.
|
|
6564
|
+
*/
|
|
6565
|
+
function sessionDirOfCommand(invocation, directoryOf) {
|
|
6566
|
+
return sessionDirOf({
|
|
6567
|
+
...invocation.agent !== void 0 ? { agent: invocation.agent } : {},
|
|
6568
|
+
...invocation.signal !== void 0 ? { signal: invocation.signal } : {}
|
|
6569
|
+
}, directoryOf);
|
|
6570
|
+
}
|
|
6571
|
+
/**
|
|
6500
6572
|
* Register `/codebuddy`, returning the disposer.
|
|
6501
6573
|
*
|
|
6502
6574
|
* `commands` is passed in rather than looked up so a Host without the service
|
|
@@ -6519,10 +6591,10 @@ function mountCodebuddyCommand(commands, deps) {
|
|
|
6519
6591
|
description: "用 CodeBuddy 在当前工作区跑一轮;留空 = 评审未提交改动;以 --edit 开头 = 这一轮允许改文件。注意:默认(cliBridge.injectResult=true)跑完会把结论回注给本会话的模型,那一次唤醒会真的消耗 DSH token;设 injectResult=false 才完全不碰模型。CodeBuddy 那一侧的账号额度每次都会消耗。",
|
|
6520
6592
|
input: { hint: "留空 = 评审未提交改动;写要求 = 只读执行(如\"看一下这个函数为什么崩\");要以 --edit 开头 = 允许改文件(如\"--edit 修一下这个空指针\")" },
|
|
6521
6593
|
handler: async (invocation) => {
|
|
6522
|
-
const cwd = invocation.
|
|
6523
|
-
if (
|
|
6594
|
+
const cwd = sessionDirOfCommand(invocation, deps.directoryOf);
|
|
6595
|
+
if (cwd === "") return {
|
|
6524
6596
|
kind: "error",
|
|
6525
|
-
text: "
|
|
6597
|
+
text: "这个会话没有工作区路径(会话的工作目录投影为空,回退的 session.header.cwd 也为空),无法确定 CodeBuddy 该在哪里跑。请在一个打开过目录的会话里再试。"
|
|
6526
6598
|
};
|
|
6527
6599
|
const raw = typeof invocation.rawInput === "string" ? invocation.rawInput : "";
|
|
6528
6600
|
const parsed = parseCommandInput(raw);
|
|
@@ -6750,7 +6822,7 @@ function normalizeCliBridgeOptions(settings, onRejected) {
|
|
|
6750
6822
|
* Returning `undefined` rather than an enabled flag keeps every caller honest:
|
|
6751
6823
|
* there is no "mounted but disabled" state to accidentally treat as live.
|
|
6752
6824
|
*/
|
|
6753
|
-
function createCliBridgeRuntime(settings, regions, log) {
|
|
6825
|
+
function createCliBridgeRuntime(settings, regions, log, directoryOf) {
|
|
6754
6826
|
const options = normalizeCliBridgeOptions(settings, (message) => log(message));
|
|
6755
6827
|
if (!options.enabled) return void 0;
|
|
6756
6828
|
const live = createLiveBoard();
|
|
@@ -6761,7 +6833,8 @@ function createCliBridgeRuntime(settings, regions, log) {
|
|
|
6761
6833
|
regions,
|
|
6762
6834
|
log,
|
|
6763
6835
|
gate: gateFor(options),
|
|
6764
|
-
live
|
|
6836
|
+
live,
|
|
6837
|
+
...directoryOf !== void 0 ? { directoryOf } : {}
|
|
6765
6838
|
},
|
|
6766
6839
|
live
|
|
6767
6840
|
};
|
|
@@ -8528,6 +8601,26 @@ async function writePoolProbes(region, updates) {
|
|
|
8528
8601
|
function safeMessage(error) {
|
|
8529
8602
|
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 500);
|
|
8530
8603
|
}
|
|
8604
|
+
/**
|
|
8605
|
+
* The upstream-identity switches the card may write through `__save`.
|
|
8606
|
+
*
|
|
8607
|
+
* These are TOP-LEVEL config fields (see the schema in `index.ts`), not a
|
|
8608
|
+
* nested block, so each is set on its own path and no merge base is involved.
|
|
8609
|
+
* The list is a constant here rather than derived from the schema because this
|
|
8610
|
+
* endpoint must stay a closed allow-list: deriving it would let an unrelated
|
|
8611
|
+
* schema addition quietly widen what the browser can write.
|
|
8612
|
+
*
|
|
8613
|
+
* Every entry is a switch the user can already set by hand in
|
|
8614
|
+
* `cordis.patch.yml`; this list only adds a UI route to the same fields (issue
|
|
8615
|
+
* #133 suggestions 2 and 3), so it can never expose anything the config file
|
|
8616
|
+
* does not already expose.
|
|
8617
|
+
*/
|
|
8618
|
+
const UPSTREAM_SWITCH_FIELDS = /* @__PURE__ */ new Set([
|
|
8619
|
+
"usageIdentity",
|
|
8620
|
+
"keepPromptLast",
|
|
8621
|
+
"extraInjectedPrefixes",
|
|
8622
|
+
"sessionTitle"
|
|
8623
|
+
]);
|
|
8531
8624
|
/** A non-null, non-array object: the only shape the slot merge applies to. */
|
|
8532
8625
|
function isPlainRecord(value) {
|
|
8533
8626
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -9373,7 +9466,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
9373
9466
|
req.on("error", reject);
|
|
9374
9467
|
});
|
|
9375
9468
|
const field = body.field;
|
|
9376
|
-
if (field !== "regions" && field !== "accounts" && field !== "cliBridge") return json(res, 400, { error: "field must be regions, accounts or
|
|
9469
|
+
if (field !== "regions" && field !== "accounts" && field !== "cliBridge" && !(typeof field === "string" && UPSTREAM_SWITCH_FIELDS.has(field))) return json(res, 400, { error: "field must be regions, accounts, cliBridge, usageIdentity, keepPromptLast, extraInjectedPrefixes or sessionTitle" });
|
|
9377
9470
|
const row = settings.describe().find((r) => String(r.ns).includes("workbuddy"));
|
|
9378
9471
|
if (row === void 0) return json(res, 503, { error: "workbuddy namespace missing from describe()" });
|
|
9379
9472
|
const current = field === "regions" ? regionsMapOf(row.value?.[field]) : unwrapVolatileDeep(row.value?.[field] ?? {});
|
|
@@ -9391,6 +9484,17 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
9391
9484
|
value: mergedBridge
|
|
9392
9485
|
});
|
|
9393
9486
|
}
|
|
9487
|
+
if (UPSTREAM_SWITCH_FIELDS.has(field)) {
|
|
9488
|
+
await settings.mutate(row.ns, [{
|
|
9489
|
+
op: "set",
|
|
9490
|
+
path: [field],
|
|
9491
|
+
value: body.value
|
|
9492
|
+
}], void 0);
|
|
9493
|
+
return json(res, 200, {
|
|
9494
|
+
ok: true,
|
|
9495
|
+
value: { [field]: body.value }
|
|
9496
|
+
});
|
|
9497
|
+
}
|
|
9394
9498
|
const merged = { ...current };
|
|
9395
9499
|
for (const [key, slot] of Object.entries(incoming)) {
|
|
9396
9500
|
const prior = current[key];
|
|
@@ -9746,7 +9850,12 @@ const Config = z.object({
|
|
|
9746
9850
|
z.const("always")
|
|
9747
9851
|
]).default("failure").description("When to perturb scanned messages with invisible characters: \"off\" only rescues after a channel-gate refusal, \"failure\" (default) learns from a successful rescue and starts later requests perturbed, \"always\" starts every request with the dense rescue for a gateway whose plain attempt is guaranteed to fail.")),
|
|
9748
9852
|
usageIdentity: asVolatile(z.boolean().default(false).description("Make the upstream's usage page record these calls like the official client's: add the X-IDE-* identity headers (which fill the 使用端 column) and send the conversation ids as 32-hex UUIDv7. Off by default — it is a display alignment measured by a reporter, not by this plugin, and the chat channel identity (User-Agent) and request body never change.")),
|
|
9749
|
-
keepPromptLast: asVolatile(z.boolean().default(false).description("Move the harness-injected runtime context above the real prompt, so the WorkBuddy usage page records what you actually typed instead of the injected block. Message count and text are preserved; the one edit is the recorded prompt's own leading <acp> marker. Off by default because it changes the message order the upstream counts."))
|
|
9853
|
+
keepPromptLast: asVolatile(z.boolean().default(false).description("Move the harness-injected runtime context above the real prompt, so the WorkBuddy usage page records what you actually typed instead of the injected block. Message count and text are preserved; the one edit is the recorded prompt's own leading <acp> marker. Off by default because it changes the message order the upstream counts.")),
|
|
9854
|
+
extraInjectedPrefixes: asVolatile(z.array(z.string()).default([]).description("Extra literal prefixes (issue #133) treated like the harness-injected context markers when keepPromptLast is on — for blocks a companion plugin injects that the built-in list does not recognize. Each entry matches the start of a trailing user message; read only while keepPromptLast is enabled. Sampled when the plugin mounts: reload the plugin (or restart the host) after changing it.")),
|
|
9855
|
+
sessionTitle: asVolatile(z.object({
|
|
9856
|
+
enabled: z.boolean().default(false),
|
|
9857
|
+
model: z.string().default("wb-lite")
|
|
9858
|
+
}).default({}).description("Move the host session-title request onto a hidden lightweight model (issues #132/#133). The host fires one real upstream request per new session either way; enabled re-points it at `model` (default wb-lite, measured 0 credits by the reporter) instead of billing the main paid model. Request volume still counts, and the lightweight model is reporter-measured, not reproduced here. Sampled when the plugin mounts: reload the plugin (or restart the host) after changing it."))
|
|
9750
9859
|
});
|
|
9751
9860
|
/**
|
|
9752
9861
|
* One region's saved model state. A config written before the region split has
|
|
@@ -9909,6 +10018,8 @@ function apply(ctx, config) {
|
|
|
9909
10018
|
logger: ctx.logger,
|
|
9910
10019
|
channelPerturbation: unwrapVolatile(config.channelPerturbation) ?? "failure",
|
|
9911
10020
|
keepPromptLast: unwrapVolatile(config.keepPromptLast) === true,
|
|
10021
|
+
extraInjectedPrefixes: unwrapVolatile(config.extraInjectedPrefixes) ?? [],
|
|
10022
|
+
sessionTitle: unwrapVolatile(config.sessionTitle) ?? {},
|
|
9912
10023
|
/**
|
|
9913
10024
|
* Pool routing, bound to THIS region.
|
|
9914
10025
|
*
|
|
@@ -10259,6 +10370,8 @@ function apply(ctx, config) {
|
|
|
10259
10370
|
});
|
|
10260
10371
|
let cliTools;
|
|
10261
10372
|
let cliCommands;
|
|
10373
|
+
let cliDirectoryOf;
|
|
10374
|
+
const cliDirectoryProbe = (session) => cliDirectoryOf?.(session) ?? null;
|
|
10262
10375
|
let cliRuntime;
|
|
10263
10376
|
let cliRuntimeKey = "";
|
|
10264
10377
|
let disposeCliTool;
|
|
@@ -10279,7 +10392,7 @@ function apply(ctx, config) {
|
|
|
10279
10392
|
cliRuntime = createCliBridgeRuntime(settings, REGION_KEYS.map((region) => ({
|
|
10280
10393
|
region,
|
|
10281
10394
|
store: stacks[region].store
|
|
10282
|
-
})), (message) => ctx.logger.warn(`dsh-connect-workbuddy: ${message}`));
|
|
10395
|
+
})), (message) => ctx.logger.warn(`dsh-connect-workbuddy: ${message}`), cliDirectoryProbe);
|
|
10283
10396
|
}
|
|
10284
10397
|
if (cliRuntime === void 0) {
|
|
10285
10398
|
disposeCliSurfaces();
|
|
@@ -10308,6 +10421,42 @@ function apply(ctx, config) {
|
|
|
10308
10421
|
cliCommands = void 0;
|
|
10309
10422
|
});
|
|
10310
10423
|
});
|
|
10424
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
10425
|
+
const reader = projectionCtx.sessionProjections;
|
|
10426
|
+
if (reader === void 0 || typeof reader.stateOf !== "function") return;
|
|
10427
|
+
/**
|
|
10428
|
+
* The one place the projection key is spelled out.
|
|
10429
|
+
*
|
|
10430
|
+
* It must match the Host's own registration — key
|
|
10431
|
+
* `"workingDirectory"`, stateVersion 1, in the Host's
|
|
10432
|
+
* @deepseek-ai/dsh-working-directory package. If the Host renames the key, this is
|
|
10433
|
+
* the line that has to follow, and the comment is what tells the next reader
|
|
10434
|
+
* where to look.
|
|
10435
|
+
*
|
|
10436
|
+
* Returns null for `"no committed directory"`, which is the same signal the header
|
|
10437
|
+
* path produces when it has nothing — so the ladder below it behaves identically.
|
|
10438
|
+
* NOTE what it deliberately does NOT do: fall back to the service's own default.
|
|
10439
|
+
* The Host's get() answers stateOf(...) ?? defaultDirectory, and that fallback is
|
|
10440
|
+
* the Host process's cwd, i.e. a directory nobody chose. Substituting it here would
|
|
10441
|
+
* quietly turn `"we do not know"` into `"use this one"`, which is the one change this
|
|
10442
|
+
* whole refactor must not make.
|
|
10443
|
+
*/
|
|
10444
|
+
const WORKING_DIRECTORY_PROJECTION_KEY = "workingDirectory";
|
|
10445
|
+
cliDirectoryOf = (session) => {
|
|
10446
|
+
let state;
|
|
10447
|
+
try {
|
|
10448
|
+
state = reader.stateOf(session, WORKING_DIRECTORY_PROJECTION_KEY);
|
|
10449
|
+
} catch {
|
|
10450
|
+
return null;
|
|
10451
|
+
}
|
|
10452
|
+
if (typeof state !== "string") return null;
|
|
10453
|
+
const trimmed = state.trim();
|
|
10454
|
+
return trimmed === "" ? null : trimmed;
|
|
10455
|
+
};
|
|
10456
|
+
projectionCtx.effect(() => () => {
|
|
10457
|
+
cliDirectoryOf = void 0;
|
|
10458
|
+
});
|
|
10459
|
+
});
|
|
10311
10460
|
ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
|
|
10312
10461
|
store: (region) => stacks[region].store,
|
|
10313
10462
|
client,
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
|
|
5
|
-
"version": "5.8.
|
|
5
|
+
"version": "5.8.3",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|