dsh-connect-workbuddy 5.6.3 → 5.8.0

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as prepareChatBody, A as parseWorkBuddyAuth, At as readAtRestKey, B as declaredTools, C as defaultDesktopAuthCandidates, Ct as findWorkbuddyAppExecutable, D as isEncryptedCredentialError, Dt as macosBundleExecutable, E as hasEncryptedCredentialFields, Et as isWorkbuddyBundle, F as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, Ft as writeFileAtomic, G as isFirstMessageGateBody, H as isContextTooLongBody, I as WorkBuddyCredentialRejectedError, J as openingEvidenceOfBody, K as knownRegionOf, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, N as CONTEXT_OVERFLOW_MARKER, Nt as resolveDshHome, O as isVaultAccountId, Ot as macosNestedAppBundles, P as CREDENTIAL_REJECTED_CODE, Pt as withFileLock, Q as perturbChannelFingerprints, R as channelGateMemberOf, S as authFileName, St as fetchAtRestKeyPayload, T as defaultDesktopAuthPath, Tt as isEncryptedFieldWrapper, U as isConversationIdGateBody, V as forceNeutralHead, W as isCredentialRejectedError, X as parseReasoning, Y as parseCreditMultiplier, Z as parseUpstreamModel, _ as ENCRYPTED_CREDENTIAL_CODE, _t as nativeModalityOf, a as readHostHeartbeat, at as lastRefusalFor, b as WorkBuddyCredentialStore, bt as deriveAtRestKey, c as WORKBUDDY_CONNECT_VERSION, ct as recordRefusal, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as WorkBuddyImageError, et as regionOf, f as WorkBuddyCatalog, ft as pickImageModel, g as withoutRouters, gt as imageDefaultFor, h as fallbackModelsFor, ht as NATIVE_MODALITY_BY_MODEL_ID, i as processStartTimeMs, it as defaultOffModelIds, j as workbuddyAccountId, jt as workbuddyAppExecutableCandidates, k as legacyWorkbuddyOwnAuthPath, kt as openEncryptedField, l as piAiRuntimeInfo, lt as withEffectiveOff, m as deriveCatalog, mt as queueNoticeText, n as clearHostHeartbeat, nt as attachBusyLevels, o as workbuddyHostHeartbeatPath, ot as offDefaultFor, p as applyContextBudgets, pt as parseQueueAnswer, q as modelIdOfBody, r as isHeartbeatProcessAlive, rt as REFUSES_OFF_MODEL_IDS, s as writeHostHeartbeat, st as offRefused, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as requestEvidenceOf, u as FALLBACK_WORKBUDDY_MODELS, ut as DEFAULT_IMAGE_SIZE, v as WORKBUDDY_AUTH_FILENAME, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, x as WorkBuddyEncryptedCredentialError, xt as deriveAtRestKeyId, y as WORKBUDDY_AUTH_FILE_ENV, yt as clearAtRestKeyCache, z as classifyUpstreamError } from "./host-heartbeat-r_icwyw3.js";
1
+ import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as isWorkbuddyBundle, B as WorkBuddyCredentialRejectedError, C as WorkBuddyCredentialStore, Ct as clearAtRestKeyCache, D as defaultDesktopAuthDirs, Dt as findWorkbuddyAppExecutable, E as defaultDesktopAuthCandidates, Et as fetchAtRestKeyPayload, F as workbuddyAccountId, Ft as workbuddyAppExecutableCandidates, G as forceNeutralHead, H as channelGateMemberOf, I as workbuddyOwnAuthPath, J as isCredentialRejectedError, K as isContextTooLongBody, L as CONTEXT_OVERFLOW_MARKER, Lt as resolveDshHome, M as legacyWorkbuddyOwnAuthPath, Mt as macosNestedAppBundles, N as parseWorkBuddyAuth, Nt as openEncryptedField, O as defaultDesktopAuthPath, P as preferredAccountName, Pt as readAtRestKey, Q as openingEvidenceOfBody, R as CREDENTIAL_REJECTED_CODE, Rt as withFileLock, S as WORKBUDDY_AUTH_FILE_ENV, St as WORKBUDDY_APP_EXECUTABLE_ENV, T as authFileName, Tt as deriveAtRestKeyId, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, W as declaredTools, X as knownRegionOf, Y as isFirstMessageGateBody, Z as modelIdOfBody, _ as deriveCatalog, _t as parseQueueAnswer, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as imageDefaultFor, c as WORKBUDDY_CONNECT_VERSION, ct as defaultOffModelIds, dt as offRefused, et as parseReasoning, f as piAiRuntimeInfo, ft as recordRefusal, g as applyContextBudgets, gt as pickImageModel, h as WorkBuddyCatalog, ht as WorkBuddyImageError, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as macosBundleExecutable, k as hasEncryptedCredentialFields, kt as isEncryptedFieldWrapper, l as createFileRosterSeenStore, lt as lastRefusalFor, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as DEFAULT_IMAGE_SIZE, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as attachBusyLevels, p as FALLBACK_WORKBUDDY_MODELS, pt as withEffectiveOff, q as isConversationIdGateBody, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as REFUSES_OFF_MODEL_IDS, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as offDefaultFor, v as fallbackModelsFor, vt as queueNoticeText, w as WorkBuddyEncryptedCredentialError, wt as deriveAtRestKey, x as WORKBUDDY_AUTH_FILENAME, xt as nativeModalityOf, y as withoutRouters, yt as NATIVE_MODALITY_BY_MODEL_ID, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as writeFileAtomic } from "./host-heartbeat-BiVFMXGE.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";
@@ -3280,6 +3280,7 @@ const KIND_STATUS = {
3280
3280
  model_busy: 429,
3281
3281
  session_dead: 401,
3282
3282
  policy_reject: 403,
3283
+ account_restricted: 403,
3283
3284
  channel_reject: 400,
3284
3285
  model_queue: 429,
3285
3286
  not_found: 502,
@@ -3955,6 +3956,7 @@ function failoverReasonText(failure) {
3955
3956
  case "model_busy": return "该模型当前繁忙(不是账号问题)";
3956
3957
  case "session_dead": return "登录已失效";
3957
3958
  case "policy_reject": return "被服务端策略拒绝";
3959
+ case "account_restricted": return "账号被上游限制使用";
3958
3960
  case "channel_reject": return "被网关的渠道校验拦截(不是账号、额度或网络问题)";
3959
3961
  case "not_found": return "模型不可用";
3960
3962
  case "server": return "上游异常";
@@ -4020,7 +4022,8 @@ function channelEvidenceText(bodyJson) {
4020
4022
  const shown = entries.slice(0, CHANNEL_EVIDENCE_LIMIT);
4021
4023
  const rendered = shown.map((entry) => {
4022
4024
  const label = entry.neutralized ? "(插件已加中性头)" : "(不在插件已知名单里,网关可能新增了词条)";
4023
- return `「${entry.role}:${entry.opening}」${label}`;
4025
+ const span = entry.closing === void 0 ? `,共 ${entry.textLength} 字` : `,首尾为「${entry.closing}」,共 ${entry.textLength} 字`;
4026
+ return `「${entry.role}:${entry.opening}${span}」${label}`;
4024
4027
  }).join("、");
4025
4028
  const hidden = entries.length - shown.length;
4026
4029
  return `请求里被网关扫描的消息开头:${rendered}${hidden > 0 ? `……另有 ${hidden} 条未列出。` : "。"}`;
@@ -4217,6 +4220,15 @@ function createWorkBuddyShim(options) {
4217
4220
  const onAccountFailure = options.onAccountFailure;
4218
4221
  const onRequestBody = options.onRequestBody;
4219
4222
  const onUpstreamUsage = options.onUpstreamUsage;
4223
+ const channelPerturbation = options.channelPerturbation ?? "failure";
4224
+ /**
4225
+ * Whether the harness's injected `user` blocks are moved above the real prompt.
4226
+ *
4227
+ * OFF by default (issue #132): the reorder changes the message ORDER the
4228
+ * upstream counts, so it must be the user's deliberate choice rather than a
4229
+ * silent difference after an upgrade.
4230
+ */
4231
+ const keepPromptLastEnabled = options.keepPromptLast === true;
4220
4232
  const SHARED_SECRET = randomBytes(32).toString("base64url");
4221
4233
  /**
4222
4234
  * Whether this process's prompts have been PROVEN to need the invisible
@@ -4370,7 +4382,7 @@ function createWorkBuddyShim(options) {
4370
4382
  return;
4371
4383
  }
4372
4384
  const raw = (await readBody(req)).toString("utf8");
4373
- const prepared = prepareChatBody(raw);
4385
+ const prepared = prepareChatBody(raw, { keepPromptLast: keepPromptLastEnabled });
4374
4386
  const requestModelId = modelIdOfBody(prepared);
4375
4387
  if (onRequestBody !== void 0) try {
4376
4388
  onRequestBody(prepared);
@@ -4379,19 +4391,7 @@ function createWorkBuddyShim(options) {
4379
4391
  }
4380
4392
  const controller = new AbortController();
4381
4393
  res.on("close", () => controller.abort());
4382
- /**
4383
- * The body to send FIRST, which is the perturbed form once this process has
4384
- * proven the gateway demands it.
4385
- *
4386
- * `perturbChannelFingerprints` is consulted only when the flag is set, so a
4387
- * fresh process still pays the plain attempt — that is what discovers
4388
- * whether the gate is on at all (see `channelNeedsPerturbation`).
4389
- *
4390
- * The perturbed form is a purely additive transform: it inserts zero-width
4391
- * spaces and changes no word the model reads, so starting from it is not a
4392
- * behaviour change, only a saved round trip.
4393
- */
4394
- const preferred = channelNeedsPerturbation ? perturbChannelFingerprints(prepared) ?? prepared : prepared;
4394
+ const preferred = channelPerturbation === "always" || channelNeedsPerturbation ? perturbChannelFingerprints(prepared, channelPerturbation === "always") ?? prepared : prepared;
4395
4395
  let result = await client.chatStream(credential, preferred, controller.signal);
4396
4396
  if (channelNeedsPerturbation && !result.ok && result.kind === "channel_reject") {
4397
4397
  channelNeedsPerturbation = false;
@@ -4432,6 +4432,7 @@ function createWorkBuddyShim(options) {
4432
4432
  */
4433
4433
  let retriedWithForcedHead = false;
4434
4434
  let retriedWithPerturbation = false;
4435
+ let retriedWithDensePerturbation = false;
4435
4436
  if (!result.ok && result.kind === "channel_reject" && !controller.signal.aborted) {
4436
4437
  const forced = forceNeutralHead(prepared);
4437
4438
  if (forced !== void 0) {
@@ -4448,6 +4449,15 @@ function createWorkBuddyShim(options) {
4448
4449
  if (result.ok) channelNeedsPerturbation = true;
4449
4450
  }
4450
4451
  }
4452
+ if (!result.ok && result.kind === "channel_reject" && !controller.signal.aborted) {
4453
+ const dense = perturbChannelFingerprints(forced ?? prepared, true);
4454
+ if (dense !== void 0) {
4455
+ logger?.warn("dsh-connect-workbuddy: the channel gate refused the sparse perturbation too; retrying once with a dense word-boundary perturbation");
4456
+ result = await client.chatStream(credential, dense, controller.signal);
4457
+ retriedWithDensePerturbation = true;
4458
+ if (result.ok && channelPerturbation === "failure") channelNeedsPerturbation = true;
4459
+ }
4460
+ }
4451
4461
  }
4452
4462
  const triedAccountIds = [];
4453
4463
  /**
@@ -4534,13 +4544,22 @@ function createWorkBuddyShim(options) {
4534
4544
  writeOpenAIError(res, 429, "model_queue", `${queueNoticeText(queue)}(上游 code ${queue.code})${note}`, void 0, NO_RETRY_HEADERS);
4535
4545
  return;
4536
4546
  }
4547
+ if (result.kind === "account_restricted") {
4548
+ const detail = result.detail;
4549
+ const meta = ["账号被上游限制使用"];
4550
+ if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
4551
+ if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
4552
+ const message = `${detail?.displayMsg ?? clippedUpstreamText(result.message, 200)}(${meta.join(",")})${note}${why}。该账号已被 WorkBuddy 上游按账号限制,与本次请求内容、网络和额度无关;插件会记录该测量并改用其他可用账号。该限制是上游对该账号的判定:重新登录不改变插件认的这个账号,需要 WorkBuddy 官方渠道解除后,在卡片上手动「测试」该账号才会回池`;
4553
+ writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
4554
+ return;
4555
+ }
4537
4556
  if (result.kind === "policy_reject") {
4538
4557
  const detail = result.detail;
4539
4558
  const lead = detail?.displayMsg ?? clippedUpstreamText(result.message, 200);
4540
4559
  const meta = ["服务端策略拒绝"];
4541
4560
  if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
4542
4561
  if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
4543
- const message = `${lead}(${meta.join(",")})${note}。该请求被 WorkBuddy 服务端策略拒绝,重新登录不会解决;可在 WorkBuddy 桌面端用同一账号验证,或切换区域/账号后重试`;
4562
+ const message = `${lead}(${meta.join(",")})${note}。该请求被 WorkBuddy 服务端策略拒绝;若上游的说明指向内容,重新登录不会解决,可在 WorkBuddy 桌面端用同一账号验证;若它指向账号本身,则需要走官方渠道处理,插件会在遇到该措辞时另行标明`;
4544
4563
  writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
4545
4564
  return;
4546
4565
  }
@@ -4564,6 +4583,7 @@ function createWorkBuddyShim(options) {
4564
4583
  const retries = [];
4565
4584
  if (retriedWithForcedHead) retries.push("强制中性头");
4566
4585
  if (retriedWithPerturbation) retries.push("一次不可见扰动");
4586
+ if (retriedWithDensePerturbation) retries.push("一次密集不可见扰动");
4567
4587
  const retryNote = retries.length === 0 ? "" : `,并已用${retries.join("与")}重试过`;
4568
4588
  const member = channelGateMemberOf(result.message, requestEvidenceOf(prepared));
4569
4589
  const said = detail?.rawMsg ?? detail?.displayMsg;
@@ -7059,7 +7079,9 @@ function cooldownOf(input) {
7059
7079
  */
7060
7080
  function outcomeOfFailure(status, body) {
7061
7081
  if (status === 401 || status === 403) {
7062
- if (classifyUpstreamError(status, body) === "policy_reject") return "policy-rejected";
7082
+ const kind = classifyUpstreamError(status, body);
7083
+ if (kind === "policy_reject") return "policy-rejected";
7084
+ if (kind === "account_restricted") return "account-restricted";
7063
7085
  return "credential-rejected";
7064
7086
  }
7065
7087
  switch (classifyUpstreamError(status, body)) {
@@ -7067,6 +7089,7 @@ function outcomeOfFailure(status, body) {
7067
7089
  case "model_busy": return "model-busy";
7068
7090
  case "hard_credit": return "out-of-credit";
7069
7091
  case "session_dead": return "credential-rejected";
7092
+ case "account_restricted": return "account-restricted";
7070
7093
  case "channel_reject": return "channel-rejected";
7071
7094
  case "not_found": return "not-found";
7072
7095
  default: return status === 0 || status >= 500 ? "unavailable" : "failed";
@@ -7607,6 +7630,7 @@ function exclusionOf(probe, nowMs) {
7607
7630
  switch (probe.outcome) {
7608
7631
  case "ok": return;
7609
7632
  case "credential-rejected": return "credential-rejected";
7633
+ case "account-restricted": return "account-restricted";
7610
7634
  case "not-found": return "not-found";
7611
7635
  case "unavailable": return retryDue(probe, nowMs) ? void 0 : "unavailable";
7612
7636
  case "failed": return retryDue(probe, nowMs) ? void 0 : "failed";
@@ -8648,7 +8672,8 @@ function toWebAccount(account) {
8648
8672
  domain: account.domain,
8649
8673
  source: account.source,
8650
8674
  tokenExpiresAtMs: account.tokenExpiresAtMs,
8651
- selected: account.selected
8675
+ selected: account.selected,
8676
+ ...account.enterprise === true ? { enterprise: true } : {}
8652
8677
  };
8653
8678
  }
8654
8679
  /**
@@ -8758,6 +8783,9 @@ async function workBuddyWebStatus(deps, region, options = {}) {
8758
8783
  const creditPosition = deps.creditPosition?.(region) ?? "composer";
8759
8784
  const imageToolEnabled = deps.imageToolEnabled?.(region) ?? false;
8760
8785
  const cliBridgeEnabled = deps.cliBridgeEnabled?.() === true;
8786
+ const cliBridgeModel = deps.cliBridgeModel?.() ?? "";
8787
+ const cliBridgeEffort = deps.cliBridgeEffort?.() ?? "";
8788
+ const cliBridgeRegionOrder = deps.cliBridgeRegionOrder?.() ?? "";
8761
8789
  const busyLevels = deps.busyLevels?.(region) ?? {};
8762
8790
  const authStatus = await store.status();
8763
8791
  const selectionExplicit = store.hasExplicitSelection();
@@ -8769,6 +8797,9 @@ async function workBuddyWebStatus(deps, region, options = {}) {
8769
8797
  showCreditsInMainUi,
8770
8798
  imageToolEnabled,
8771
8799
  cliBridgeEnabled,
8800
+ cliBridgeModel,
8801
+ cliBridgeEffort,
8802
+ cliBridgeRegionOrder,
8772
8803
  busyLevels
8773
8804
  };
8774
8805
  let credential;
@@ -8787,6 +8818,9 @@ async function workBuddyWebStatus(deps, region, options = {}) {
8787
8818
  creditPosition,
8788
8819
  imageToolEnabled,
8789
8820
  cliBridgeEnabled,
8821
+ cliBridgeModel,
8822
+ cliBridgeEffort,
8823
+ cliBridgeRegionOrder,
8790
8824
  busyLevels,
8791
8825
  ...await store.selectionLost() ? { selectionLost: true } : {},
8792
8826
  ...webAccounts.length === 0 ? await searchedPaths(store) : {}
@@ -8795,7 +8829,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
8795
8829
  const selected = accounts.find((account) => account.selected);
8796
8830
  const account = {
8797
8831
  accountId: selected?.id ?? "",
8798
- accountName: credential.nickname ?? "",
8832
+ accountName: preferredAccountName(credential),
8799
8833
  ...credential.domain === "" ? {} : { domain: credential.domain },
8800
8834
  region,
8801
8835
  source: credential.source,
@@ -8806,6 +8840,9 @@ async function workBuddyWebStatus(deps, region, options = {}) {
8806
8840
  creditPosition,
8807
8841
  imageToolEnabled,
8808
8842
  cliBridgeEnabled,
8843
+ cliBridgeModel,
8844
+ cliBridgeEffort,
8845
+ cliBridgeRegionOrder,
8809
8846
  busyLevels,
8810
8847
  accounts: accounts.map(toWebAccount),
8811
8848
  models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))).map((model) => model.reasoning === void 0 || lastRefusalFor(model.id) === void 0 ? model : {
@@ -8820,6 +8857,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
8820
8857
  offModelIds: [...deps.offModelIds(region)]
8821
8858
  };
8822
8859
  const figures = fast ? {} : await liveFigures(deps, region, credential, selected);
8860
+ const rosterSeen = deps.rosterSeen === void 0 ? void 0 : await deps.rosterSeen(region);
8823
8861
  return {
8824
8862
  status: "signed-in",
8825
8863
  contextBudgets: Object.fromEntries(Object.entries(deps.contextBudgets(region)).filter(([, value]) => typeof value === "number")),
@@ -8829,6 +8867,10 @@ async function workBuddyWebStatus(deps, region, options = {}) {
8829
8867
  authFile: volatileFlagOf("authFile")
8830
8868
  },
8831
8869
  ...account,
8870
+ ...rosterSeen === void 0 ? {} : {
8871
+ newModelIds: [...rosterSeen.newModelIds],
8872
+ ...rosterSeen.versionNote === void 0 ? {} : { rosterSeenVersionNote: rosterSeen.versionNote }
8873
+ },
8832
8874
  ...figures,
8833
8875
  ...await poolSectionOf(deps, region, fast)
8834
8876
  };
@@ -8901,6 +8943,7 @@ async function workBuddyWebPool(deps, region, fast = false) {
8901
8943
  return {
8902
8944
  accountId: account.id,
8903
8945
  accountName: account.accountName,
8946
+ ...account.enterprise === true ? { enterprise: true } : {},
8904
8947
  ...measured?.credits === void 0 ? {} : {
8905
8948
  credits: measured.credits.total,
8906
8949
  expiringSoon: measured.credits.expiringSoon,
@@ -9083,6 +9126,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
9083
9126
  try {
9084
9127
  const credential = await deps.store(region).credentialFor(accountId);
9085
9128
  if (credential === void 0) return json(res, 404, { error: "no credential for this account" });
9129
+ if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") return json(res, 409, { error: "enterprise-account-unsupported" });
9086
9130
  const target = (await deps.client.fetchBenefits(credential)).find((benefit) => benefit.kind === kind);
9087
9131
  if (target === void 0) return json(res, 404, { error: "benefit not available on this account" });
9088
9132
  if (target.claimed) return json(res, 409, {
@@ -9695,7 +9739,14 @@ const Config = z.object({
9695
9739
  enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
9696
9740
  imageModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN image opt-in"),
9697
9741
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
9698
- cliBridge: asVolatile(cliBridgeConfig.default({}).description("Optional CodeBuddy CLI route; off by default and independent of the providers"))
9742
+ cliBridge: asVolatile(cliBridgeConfig.default({}).description("Optional CodeBuddy CLI route; off by default and independent of the providers")),
9743
+ channelPerturbation: asVolatile(z.union([
9744
+ z.const("off"),
9745
+ z.const("failure"),
9746
+ z.const("always")
9747
+ ]).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
+ 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."))
9699
9750
  });
9700
9751
  /**
9701
9752
  * One region's saved model state. A config written before the region split has
@@ -9798,7 +9849,7 @@ async function legacyAttributionRegion(value, accountsFor) {
9798
9849
  function apply(ctx, config) {
9799
9850
  const client = new WorkBuddyUpstreamClient((message) => {
9800
9851
  ctx.logger.warn(`dsh-connect-workbuddy: ${message}`);
9801
- });
9852
+ }, { usageIdentity: unwrapVolatile(config.usageIdentity) === true });
9802
9853
  /**
9803
9854
  * How long a check-in reading is reused before asking the upstream again.
9804
9855
  *
@@ -9836,6 +9887,7 @@ function apply(ctx, config) {
9836
9887
  refresh: (credential) => client.refreshToken(credential)
9837
9888
  });
9838
9889
  const catalog = new WorkBuddyCatalog(region);
9890
+ const rosterSeen = createFileRosterSeenStore(region);
9839
9891
  /**
9840
9892
  * Where the next upstream `usage` should be written.
9841
9893
  *
@@ -9855,6 +9907,8 @@ function apply(ctx, config) {
9855
9907
  client,
9856
9908
  catalog,
9857
9909
  logger: ctx.logger,
9910
+ channelPerturbation: unwrapVolatile(config.channelPerturbation) ?? "failure",
9911
+ keepPromptLast: unwrapVolatile(config.keepPromptLast) === true,
9858
9912
  /**
9859
9913
  * Pool routing, bound to THIS region.
9860
9914
  *
@@ -9887,7 +9941,8 @@ function apply(ctx, config) {
9887
9941
  onAccountFailure: (accountId, failure) => {
9888
9942
  recordAccountFailure(region, accountId, failure);
9889
9943
  }
9890
- })
9944
+ }),
9945
+ rosterSeen
9891
9946
  };
9892
9947
  }
9893
9948
  const accountUsabilityProbe = createAccountUsabilityProbe({
@@ -10303,6 +10358,15 @@ function apply(ctx, config) {
10303
10358
  offModelIds: (region) => savedOffModelIds(regionStateOf(current(), region), displayModels(current(), region)),
10304
10359
  contextBudgets: (region) => regionStateOf(current(), region).contextBudgets ?? {},
10305
10360
  discoverModels,
10361
+ /**
10362
+ * This region's first-sighting readout, the data behind the composer pill's
10363
+ * "· N 新模型" segment.
10364
+ *
10365
+ * READ-ONLY here: only the reconciler's `note` writes first-sighting
10366
+ * history, so no amount of polling by the pill or the card can change who
10367
+ * counts as new — the one property that keeps the marker honest.
10368
+ */
10369
+ rosterSeen: (region) => stacks[region].rosterSeen.readout(),
10306
10370
  regionEnabled: (region) => regionEnabled(current(), region),
10307
10371
  /**
10308
10372
  * The region's committed sidebar-credit flag, read from the same config the
@@ -10328,6 +10392,16 @@ function apply(ctx, config) {
10328
10392
  */
10329
10393
  cliBridgeEnabled: () => unwrapVolatile(current().cliBridge)?.enabled === true,
10330
10394
  /**
10395
+ * The model / reasoning effort the optional CodeBuddy CLI route runs on.
10396
+ *
10397
+ * `cliBridge` is volatile, hence the unwrap; absent keys read as `''` —
10398
+ * "the CLI's own default" — because an empty pin is a COMMITTED value the
10399
+ * card's pickers can render, not a missing one.
10400
+ */
10401
+ cliBridgeModel: () => unwrapVolatile(current().cliBridge)?.model ?? "",
10402
+ cliBridgeEffort: () => unwrapVolatile(current().cliBridge)?.effort ?? "",
10403
+ cliBridgeRegionOrder: () => unwrapVolatile(current().cliBridge)?.regionOrder ?? "",
10404
+ /**
10331
10405
  * WHERE the readout sits, resolved here rather than read from the browser
10332
10406
  * settings mirror, for the same measured reason as the flag above: on the
10333
10407
  * affected 0.1.7 deployments that mirror never picks up this plugin's
@@ -10491,7 +10565,8 @@ function apply(ctx, config) {
10491
10565
  const wanted = new Set(poolPreferencesOf(current(), region).memberAccountIds);
10492
10566
  return (await stacks[region].store.accounts()).filter((account) => !wanted.has(account.id)).map((account) => ({
10493
10567
  id: account.id,
10494
- accountName: account.accountName
10568
+ accountName: account.accountName,
10569
+ ...account.enterprise === true ? { enterprise: true } : {}
10495
10570
  }));
10496
10571
  },
10497
10572
  /**
@@ -10564,6 +10639,7 @@ function apply(ctx, config) {
10564
10639
  if (cached !== void 0 && now - cached.atMs < 18e5) return [account.id, cached.views];
10565
10640
  const credential = await store.credentialFor(account.id).catch(() => void 0);
10566
10641
  if (credential === void 0) return void 0;
10642
+ if (credential.enterpriseId !== void 0 && credential.enterpriseId !== "") return [account.id, []];
10567
10643
  const read = await client.fetchBenefits(credential).catch(() => void 0);
10568
10644
  if (read === void 0) return void 0;
10569
10645
  const views = claimableBenefits(read, now);
@@ -10875,6 +10951,7 @@ function apply(ctx, config) {
10875
10951
  case "out-of-credit": return "账号积分耗尽";
10876
10952
  case "model-busy": return "账号撞上模型繁忙";
10877
10953
  case "credential-rejected": return "账号登录已失效";
10954
+ case "account-restricted": return "账号被上游限制使用";
10878
10955
  case "not-found": return "账号已不存在";
10879
10956
  case "unusable": return "账号不可用";
10880
10957
  default: return "账号不可用";
@@ -10914,7 +10991,8 @@ function apply(ctx, config) {
10914
10991
  return {
10915
10992
  account: {
10916
10993
  id: account.id,
10917
- accountName: account.accountName
10994
+ accountName: account.accountName,
10995
+ ...account.enterprise === true ? { enterprise: true } : {}
10918
10996
  },
10919
10997
  ...reading === void 0 ? {} : { credits: reading },
10920
10998
  ...probes[account.id] === void 0 ? {} : { probe: probes[account.id] },
@@ -11054,7 +11132,8 @@ function apply(ctx, config) {
11054
11132
  return {
11055
11133
  account: {
11056
11134
  id: account.id,
11057
- accountName: account.accountName
11135
+ accountName: account.accountName,
11136
+ ...account.enterprise === true ? { enterprise: true } : {}
11058
11137
  },
11059
11138
  ...credits === void 0 ? {} : { credits: {
11060
11139
  total: credits.total,
@@ -11272,6 +11351,11 @@ function apply(ctx, config) {
11272
11351
  const state = regionStateOf(current(), region);
11273
11352
  stacks[region].catalog.set(withEffectiveOff(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])), new Set(savedOffModelIds(state, models))));
11274
11353
  adapters[region].invalidate();
11354
+ try {
11355
+ await stacks[region].rosterSeen.note(models.map((model) => model.id));
11356
+ } catch (error) {
11357
+ ctx.logger.warn(`dsh-connect-workbuddy: ${region} first-sighting history not updated`, error);
11358
+ }
11275
11359
  await regionsFieldHealed;
11276
11360
  await repairRegionCatalog(region, models);
11277
11361
  } catch (error) {
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.6.3",
5
+ "version": "5.8.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",