dsh-connect-workbuddy 5.9.0 → 5.10.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 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-C1tVU2IN.js";
1
+ import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as nativeModalityOf, B as WorkBuddyCredentialRejectedError, Bt as macosBundleExecutable, C as WorkBuddyCredentialStore, Ct as parseImageDataUrl, D as defaultDesktopAuthDirs, Dt as queueNoticeText, E as defaultDesktopAuthCandidates, Et as parseQueueAnswer, F as workbuddyAccountId, Ft as fetchAtRestKeyPayload, G as forceNeutralHead, H as channelGateMemberOf, Ht as openEncryptedField, I as workbuddyOwnAuthPath, It as findWorkbuddyAppExecutable, J as isCredentialRejectedError, Jt as writeFileAtomic, K as isContextTooLongBody, Kt as resolveDshHome, L as CONTEXT_OVERFLOW_MARKER, M as legacyWorkbuddyOwnAuthPath, Mt as clearAtRestKeyCache, N as parseWorkBuddyAuth, Nt as deriveAtRestKey, O as defaultDesktopAuthPath, Ot as NATIVE_MODALITY_BY_MODEL_ID, P as preferredAccountName, Pt as deriveAtRestKeyId, Q as openingEvidenceOfBody, R as CREDENTIAL_REJECTED_CODE, Rt as isEncryptedFieldWrapper, S as WORKBUDDY_AUTH_FILE_ENV, St as isBareBase64Image, T as authFileName, Tt as sniffImageMime, U as classifyUpstreamError, Ut as readAtRestKey, V as WorkBuddyUpstreamClient, Vt as macosNestedAppBundles, W as declaredTools, Wt as workbuddyAppExecutableCandidates, X as knownRegionOf, Y as isFirstMessageGateBody, Z as modelIdOfBody, _ as deriveCatalog, _t as IMAGE_TAG_TEXT_TO_IMAGE, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as imageDataUrl, 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 IMAGE_TAG_IMAGE_TO_IMAGE, h as WorkBuddyCatalog, ht as DEFAULT_IMAGE_SIZE, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as WORKBUDDY_APP_EXECUTABLE_ENV, k as hasEncryptedCredentialFields, kt as imageDefaultFor, 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, qt as withFileLock, 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 MAX_INPUT_IMAGE_BYTES, w as WorkBuddyEncryptedCredentialError, wt as pickMediaModel, x as WORKBUDDY_AUTH_FILENAME, xt as imageMimeForPath, y as withoutRouters, yt as WorkBuddyImageError, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as isWorkbuddyBundle } from "./host-heartbeat-_EeM2YDF.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";
@@ -10,7 +10,7 @@ import { createAssistantMessageEventStream, createProvider, isContextOverflow }
10
10
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
11
11
  import { ReasoningEffortId, createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
12
12
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
13
- import { pathToFileURL } from "node:url";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
14
  import { createServer } from "node:http";
15
15
  import { Readable } from "node:stream";
16
16
  import { isIP } from "node:net";
@@ -311,6 +311,18 @@ const WORKBUDDY_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-workbuddy/account-c
311
311
  const WORKBUDDY_BENEFIT_CLAIM_PATH = "/plugins/dsh-connect-workbuddy/benefit/claim";
312
312
  /** Plugin-owned live model refresh endpoint. */
313
313
  const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
314
+ /**
315
+ * Plugin-owned MEDIA model list endpoint (image + video, read from the region's
316
+ * `/v3/config`).
317
+ *
318
+ * Separate from {@link WORKBUDDY_MODELS_REFRESH_PATH} because it answers a
319
+ * different question from a different document projection: the refresh route
320
+ * returns the CHAT roster, from which `selectCliModels` drops every media
321
+ * entry, while this one keeps the `text-to-image`/`image-to-image`/
322
+ * `text-to-video`/`image-to-video` rows the card lists. Read-only and free —
323
+ * it is the same GET the image tool already performs.
324
+ */
325
+ const WORKBUDDY_MEDIA_MODELS_PATH = "/plugins/dsh-connect-workbuddy/media-models";
314
326
  /** Plugin-owned local account rescan endpoint. */
315
327
  const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
316
328
  /** Plugin-owned daily check-in action endpoint. */
@@ -4868,7 +4880,8 @@ function createWorkBuddyShim(options) {
4868
4880
  //#endregion
4869
4881
  //#region src/image-tool.ts
4870
4882
  /**
4871
- * `workbuddy_draw_image`:把 WorkBuddy 国内区的文生图能力暴露成一个 DSH agent 工具。
4883
+ * `workbuddy_draw_image`:把 WorkBuddy 的文生图能力(国内区与国际区各自的
4884
+ * `text-to-image` 模型)暴露成一个 DSH agent 工具。
4872
4885
  *
4873
4886
  * 为什么手写 ToolDefinition 而不 import `@deepseek-ai/dsh-tools` 的 `defineTool`:
4874
4887
  * 宿主 `Tools.register` 只校验两件事——`output.render` 是函数、`output.schema` 是
@@ -4886,8 +4899,8 @@ function createWorkBuddyShim(options) {
4886
4899
  const WORKBUDDY_IMAGE_TOOL_NAME = "workbuddy_draw_image";
4887
4900
  /** 工具单次执行的上限。实测一张图 17–39 秒,留足余量。 */
4888
4901
  const IMAGE_TOOL_TIMEOUT_MS = 18e4;
4889
- /** 媒体模型目录的缓存时长:目录变化很慢,不必每张图都重新读一遍。 */
4890
- const MEDIA_MODEL_TTL_MS = 3e5;
4902
+ /** 下载远程输入图的上限(只覆盖 http(s) 那一次 GET,不含生图本身)。 */
4903
+ const IMAGE_INPUT_TIMEOUT_MS = 3e4;
4891
4904
  /** 失败后的冷却:生图按张扣积分,连续重试会在几秒内烧掉几十积分。 */
4892
4905
  const FAILURE_COOLDOWN_MS = 3e4;
4893
4906
  function asRecord(value) {
@@ -4938,18 +4951,82 @@ async function writeTempImage(b64, model) {
4938
4951
  return path;
4939
4952
  }
4940
4953
  /**
4954
+ * 输入图的一切毛病都发生在「请求还没发出去」之前,所以统一用 INVALID_ARGS:
4955
+ * 不进冷却、不花积分,错误信息直接说清是哪个环节。
4956
+ */
4957
+ function inputImageError(message) {
4958
+ return new WorkBuddyImageError(message, "INVALID_ARGS");
4959
+ }
4960
+ const INLINE_BASE64_LABEL = "(内联 base64)";
4961
+ const INLINE_DATA_URL_LABEL = "(内联 data URL)";
4962
+ /**
4963
+ * 体积与空文件闸门——四种输入形态都从这里过,所以「空文件」和「超过 8 MB」的
4964
+ * 文案只有一份。
4965
+ *
4966
+ * `byteLength` 必须是**解码后**的字节数:base64 比原始字节长约 4/3,拿字符串长度
4967
+ * 比上限度会把一个 6 MB 的图误判成超限(并在错误信息里报出一个虚高的 MB 数)。
4968
+ */
4969
+ function assertInputImageBytes(byteLength, source) {
4970
+ if (byteLength === 0) throw inputImageError(`workbuddy image: 输入图是空文件:${source}`);
4971
+ if (byteLength > 8388608) {
4972
+ const limit = MAX_INPUT_IMAGE_BYTES / 1024 / 1024;
4973
+ throw inputImageError(`workbuddy image: 输入图 ${(byteLength / 1024 / 1024).toFixed(1)} MB,超过 ${limit} MB 上限:${source}`);
4974
+ }
4975
+ }
4976
+ /**
4977
+ * 把任意形态的输入图规整成上游唯一接受的形态:`data:<mime>;base64,<b64>`。
4978
+ *
4979
+ * 官方 CLI 的 `processImageInput` 收 data URL、http(s)、裸 base64、本地路径四
4980
+ * 种;这里只是把前三种归一,**不做**缩放、裁剪、格式转换——用户给什么就传什么,
4981
+ * 免得悄悄改了输入图。
4982
+ */
4983
+ async function resolveImageInput(value, signal) {
4984
+ const text = value.trim();
4985
+ if (text.startsWith("data:")) {
4986
+ const parsed = parseImageDataUrl(text);
4987
+ if (parsed === void 0) throw inputImageError("workbuddy image: image 看起来是 data URL,但格式不对(需要 data:image/*;base64,...)。");
4988
+ assertInputImageBytes(Buffer.from(parsed.base64, "base64").byteLength, INLINE_DATA_URL_LABEL);
4989
+ return text;
4990
+ }
4991
+ if (/^https?:\/\//i.test(text)) {
4992
+ let response;
4993
+ try {
4994
+ response = await fetch(text, { signal: signal ?? AbortSignal.timeout(IMAGE_INPUT_TIMEOUT_MS) });
4995
+ } catch (error) {
4996
+ throw inputImageError(`workbuddy image: 下载输入图失败(${text}):` + (error instanceof Error ? error.message : String(error)));
4997
+ }
4998
+ if (!response.ok) throw inputImageError(`workbuddy image: 下载输入图失败(HTTP ${response.status}):${text}`);
4999
+ return encodeInputImage(new Uint8Array(await response.arrayBuffer()), response.headers.get("content-type") ?? void 0, text);
5000
+ }
5001
+ if (isBareBase64Image(text)) return encodeInputImage(new Uint8Array(Buffer.from(text, "base64")), void 0, INLINE_BASE64_LABEL);
5002
+ const path = text.startsWith("file:") ? fileURLToPath(text) : text;
5003
+ let bytes;
5004
+ try {
5005
+ bytes = await readFile(path);
5006
+ } catch (error) {
5007
+ throw inputImageError(`workbuddy image: 读不到输入图「${path}」:` + (error instanceof Error ? error.message : String(error)));
5008
+ }
5009
+ return encodeInputImage(new Uint8Array(bytes), void 0, path);
5010
+ }
5011
+ /** 校验体积、定出 mime,再把字节编成 data URL。 */
5012
+ function encodeInputImage(bytes, contentType, source) {
5013
+ assertInputImageBytes(bytes.byteLength, source);
5014
+ const declared = contentType?.split(";")[0]?.trim().toLowerCase();
5015
+ const mime = declared !== void 0 && declared.startsWith("image/") ? declared : imageMimeForPath(source) ?? sniffImageMime(bytes);
5016
+ return imageDataUrl(mime, Buffer.from(bytes).toString("base64"));
5017
+ }
5018
+ /**
4941
5019
  * 造出工具定义。
4942
5020
  *
4943
5021
  * 每次 `apply()` 只造一个实例,所以冷却状态与目录缓存都挂在闭包里,
4944
5022
  * 生命周期跟着插件走。
4945
5023
  */
4946
5024
  function createImageTool(deps) {
4947
- const regionLabel = deps.regionLabel ?? "国内区";
4948
5025
  let mediaCache;
4949
5026
  let cooldownUntil = 0;
4950
5027
  const listModels = async (credential, signal) => {
4951
5028
  const now = Date.now();
4952
- if (mediaCache !== void 0 && now - mediaCache.at < MEDIA_MODEL_TTL_MS) return mediaCache.models;
5029
+ if (mediaCache !== void 0 && now - mediaCache.at < 3e5) return mediaCache.models;
4953
5030
  const models = await deps.listMediaModels(credential, signal);
4954
5031
  mediaCache = {
4955
5032
  at: now,
@@ -4959,7 +5036,7 @@ function createImageTool(deps) {
4959
5036
  };
4960
5037
  return {
4961
5038
  name: WORKBUDDY_IMAGE_TOOL_NAME,
4962
- description: "用当前账号的 WorkBuddy 生图能力画一张图(文生图)。只在国内区账号下可用;国际区没有生图模型。每张图会真实消耗 WorkBuddy 积分(实测约 5.71 积分/张),不要在没必要时调用,也不要为了试错连续调用。返回的是 COS 签名 URL,约 24 小时后失效;需要展示给用户时,用 markdown 图片语法引用该 URL。",
5039
+ description: "用当前账号的 WorkBuddy 生图能力画一张图。不填 image 是文生图(用该区带 text-to-image 标签的模型);填了 image 是改图(用带 image-to-image 标签的模型)。国内区与国际区都可用,模型取自该区产品配置(国内区文生图 hunyuan-image-alpha、改图 hunyuan-image-alpha-edit;国际区 hy-image-3.5 同时带这两个标签,以该区目录为准)。每张图会真实消耗 WorkBuddy 积分,单价随区域与模型不同,以返回结果里的 credit 为准;不要在没必要时调用,也不要为了试错连续调用。返回的是 COS 签名 URL,约 24 小时后失效;需要展示给用户时,用 markdown 图片语法引用该 URL。",
4963
5040
  parameters: {
4964
5041
  type: "object",
4965
5042
  properties: {
@@ -4971,9 +5048,13 @@ function createImageTool(deps) {
4971
5048
  type: "string",
4972
5049
  description: `图片尺寸,形如 1024x1024。默认 ${DEFAULT_IMAGE_SIZE}。`
4973
5050
  },
5051
+ image: {
5052
+ type: "string",
5053
+ description: "输入图,可选。填了就是改图(image-to-image);不填就是文生图。接受本地绝对路径、file:// URL、http(s) URL、data URL 或裸 base64。上限 8 MB,不做缩放或格式转换。"
5054
+ },
4974
5055
  model: {
4975
5056
  type: "string",
4976
- description: "生图模型 id,可选。不填就用产品配置里第一个带 text-to-image 标签的模型(国内区即 hunyuan-image-alpha)。填了但目录里没有会直接报错,不会盲发。"
5057
+ description: "模型 id,可选。不填就用该区目录里第一个带当前模式标签的模型(文生图看 text-to-image,改图看 image-to-image)。填了但该 tag 组里没有会直接报错,不会盲发。"
4977
5058
  }
4978
5059
  },
4979
5060
  required: ["prompt"],
@@ -4987,9 +5068,13 @@ function createImageTool(deps) {
4987
5068
  type: "string",
4988
5069
  description: "图片地址(COS 签名 URL,或本地 file:// 地址)。"
4989
5070
  },
5071
+ mode: {
5072
+ type: "string",
5073
+ description: "'generate'(文生图)或 'edit'(改图)。"
5074
+ },
4990
5075
  model: {
4991
5076
  type: "string",
4992
- description: "实际使用的生图模型 id。"
5077
+ description: "实际使用的模型 id。"
4993
5078
  },
4994
5079
  prompt: {
4995
5080
  type: "string",
@@ -5010,6 +5095,7 @@ function createImageTool(deps) {
5010
5095
  },
5011
5096
  required: [
5012
5097
  "url",
5098
+ "mode",
5013
5099
  "model",
5014
5100
  "prompt",
5015
5101
  "hint"
@@ -5020,13 +5106,14 @@ function createImageTool(deps) {
5020
5106
  const record = asRecord(value);
5021
5107
  const url = asNonEmptyString(record["url"]) ?? "";
5022
5108
  const model = asNonEmptyString(record["model"]) ?? "unknown";
5109
+ const mode = record["mode"] === "edit" ? "改图" : "文生图";
5023
5110
  const credit = typeof record["credit"] === "number" ? record["credit"] : void 0;
5024
5111
  const path = asNonEmptyString(record["path"]);
5025
5112
  const hint = asNonEmptyString(record["hint"]) ?? "";
5026
5113
  const lines = [
5027
- `![WorkBuddy 生图](${url})`,
5114
+ `![WorkBuddy ${mode}](${url})`,
5028
5115
  "",
5029
- `模型:\`${model}\`${credit === void 0 ? "" : ` · 消耗 ${credit} 积分`}`
5116
+ `${mode} · 模型:\`${model}\`${credit === void 0 ? "" : ` · 消耗 ${credit} 积分`}`
5030
5117
  ];
5031
5118
  if (path !== void 0) lines.push(`本地文件:${path}`);
5032
5119
  if (hint !== "") lines.push("", hint);
@@ -5043,28 +5130,37 @@ function createImageTool(deps) {
5043
5130
  if (prompt === void 0) throw new WorkBuddyImageError(`${WORKBUDDY_IMAGE_TOOL_NAME}: 参数 prompt 必填,且必须是非空字符串。`, "INVALID_ARGS");
5044
5131
  const size = asNonEmptyString(record["size"]) ?? "1024x1024";
5045
5132
  const requestedModel = asNonEmptyString(record["model"]);
5133
+ const image = asNonEmptyString(record["image"]);
5046
5134
  const now = Date.now();
5047
5135
  if (now < cooldownUntil) {
5048
5136
  const seconds = Math.ceil((cooldownUntil - now) / 1e3);
5049
5137
  throw new WorkBuddyImageError(`workbuddy image: 上一次生图刚失败过,请 ${seconds} 秒后再试(失败冷却是为了避免连续重试白白烧积分)。`, "COOLDOWN");
5050
5138
  }
5051
5139
  const credential = await deps.resolveCredential();
5052
- if (credential === void 0) throw new WorkBuddyImageError(`workbuddy image: ${regionLabel}未登录 WorkBuddy。请先在插件设置里登录国内区账号,再调用生图工具。`, "NOT_LOGGED_IN");
5140
+ if (credential === void 0) throw new WorkBuddyImageError("workbuddy image: 没有可用的 WorkBuddy 账号——要么两个区都没登录,要么已登录的那个区没有打开「允许模型自主生图」。请先在插件设置里登录该区账号,并打开那个开关。", "NOT_LOGGED_IN");
5053
5141
  const signal = exec.signal === void 0 ? AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS) : AbortSignal.any([exec.signal, AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS)]);
5054
5142
  let sent = false;
5055
5143
  try {
5056
5144
  const models = await listModels(credential, signal);
5057
- const model = pickImageModel(models, requestedModel);
5145
+ const input = image === void 0 ? void 0 : await resolveImageInput(image, signal);
5146
+ const model = pickMediaModel(models, requestedModel, input === void 0 ? IMAGE_TAG_TEXT_TO_IMAGE : IMAGE_TAG_IMAGE_TO_IMAGE);
5058
5147
  sent = true;
5059
- const outcome = await deps.generateImage(credential, {
5148
+ const outcome = input === void 0 ? await deps.generateImage(credential, {
5060
5149
  model: model.id,
5061
5150
  prompt,
5062
5151
  size
5152
+ }, signal) : await deps.editImage(credential, {
5153
+ model: model.id,
5154
+ prompt,
5155
+ size,
5156
+ image: [input]
5063
5157
  }, signal);
5158
+ const mode = input === void 0 ? "generate" : "edit";
5064
5159
  const value = {
5160
+ mode,
5065
5161
  model: model.id,
5066
5162
  prompt,
5067
- hint: `这张图来自 WorkBuddy(模型 ${model.id}),每张约消耗 5.71 积分。COS 签名 URL 约 24 小时失效;要长期保留请及时下载。` + (outcome.credit === void 0 ? "" : `本次实际扣费 ${outcome.credit} 积分。`)
5163
+ hint: `这张图来自 WorkBuddy(模型 ${model.id},${mode === "edit" ? "改图" : "文生图"})。按张扣积分,单价随区域与模型不同` + (outcome.credit === void 0 ? "(本次响应没带扣费字段,以额度面板为准)。" : `;本次实际扣费 ${outcome.credit} 积分。`) + "COS 签名 URL 约 24 小时失效;要长期保留请及时下载。"
5068
5164
  };
5069
5165
  if (outcome.url !== void 0) value["url"] = outcome.url;
5070
5166
  else if (outcome.b64 !== void 0) {
@@ -9472,6 +9568,27 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
9472
9568
  }
9473
9569
  }
9474
9570
  });
9571
+ const disposeMediaModels = ctx.webServer.register({
9572
+ kind: "exact",
9573
+ path: WORKBUDDY_MEDIA_MODELS_PATH,
9574
+ handler: async (req, res) => {
9575
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
9576
+ if (!requireLoopback(req, res)) return;
9577
+ if (deps.mediaModels === void 0) return json(res, 503, { error: "media model list unavailable" });
9578
+ const region = requestRegion(req, res);
9579
+ if (region === void 0) return;
9580
+ try {
9581
+ const refresh = new URL(req.url ?? "/", "http://localhost").searchParams.get("refresh") === "1";
9582
+ json(res, 200, { models: (await deps.mediaModels(region, { refresh })).map((model) => ({
9583
+ id: model.id,
9584
+ name: model.name,
9585
+ tags: [...model.tags]
9586
+ })) });
9587
+ } catch (error) {
9588
+ json(res, 500, { error: safeMessage(error) });
9589
+ }
9590
+ }
9591
+ });
9475
9592
  const disposeProbe = ctx.webServer.register({
9476
9593
  kind: "exact",
9477
9594
  path: WORKBUDDY_PROBE_PATH,
@@ -9847,6 +9964,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
9847
9964
  return () => {
9848
9965
  disposeBenefitClaim();
9849
9966
  disposeRefresh();
9967
+ disposeMediaModels();
9850
9968
  disposeCheckin();
9851
9969
  disposeAccounts();
9852
9970
  disposeUsage();
@@ -9950,25 +10068,30 @@ const regionStateConfig = z.object({
9950
10068
  /**
9951
10069
  * Whether the model may call the image-generation tool (opt-IN, default off).
9952
10070
  *
9953
- * Per-REGION like the credit switches, and only meaningful on `cn`: the image
9954
- * catalog has a `text-to-image` model on the CN gateway and none at all on the
9955
- * international one (measured, see `docs/IMAGE_GEN.md`).
10071
+ * Per-REGION like the credit switches, and it now means the same thing on both
10072
+ * tabs: each region's catalog carries its own `text-to-image` model (CN
10073
+ * `hunyuan-image-alpha`, international `hy-image-3.5`; measured 2026-10-25,
10074
+ * see `docs/IMAGE_GEN.md`). Whichever region is switched on AND opted in owns
10075
+ * the tool; CN wins when both qualify (see `imageToolRegionOf`).
9956
10076
  *
9957
10077
  * Every other spending path in this plugin requires a human action: a chat
9958
10078
  * request is one the user sent, and the per-model "test" button is one the
9959
10079
  * user clicked. `workbuddy_draw_image` is the first capability where the
9960
10080
  * MODEL decides to spend — it calls the tool on its own, at about 5.71 credits
9961
- * an image, and there is no per-call approval seam in the host to ask the user
9962
- * first (`needsApproval` / `toolApproval` do not exist; verified against the
9963
- * shipped `app.asar`).
10081
+ * an image on CN and 0.03 on the international gateway (measured, not a price
10082
+ * list: the media catalog carries no cost, so the tool reports the
10083
+ * `usage.credit` the upstream actually settles), and there is no per-call
10084
+ * approval seam in the host to ask the user first (`needsApproval` /
10085
+ * `toolApproval` do not exist; verified against the shipped `app.asar`).
9964
10086
  *
9965
10087
  * So it is opt-in, the same way the account pool's credit-based rotation is
9966
10088
  * opt-in (3.0.0's reasoning): a feature that can move money on its own must be
9967
10089
  * one the user switched on, not one that arrives with an upgrade. Off also
9968
- * means the tool is not REGISTERED at all, so the model never sees it and
9969
- * cannot attempt a call that would be refused.
10090
+ * means that region contributes nothing, and with no opted-in region at all
10091
+ * the tool is not REGISTERED, so the model never sees it and cannot attempt a
10092
+ * call that would be refused.
9970
10093
  */
9971
- imageToolEnabled: z.boolean().default(false).description("Let the model call workbuddy_draw_image on its own (opt-in; each image costs ~5.71 credits and there is no per-call approval). Off by default, and off means the tool is not registered at all."),
10094
+ imageToolEnabled: z.boolean().default(false).description("Let the model call workbuddy_draw_image on its own (opt-in; an image costs ~5.71 credits on the CN gateway and ~0.03 on the international one, and there is no per-call approval). Off by default, and off means no region registers the tool at all."),
9972
10095
  /**
9973
10096
  * Per-model override of whether thinking `off` is offered, keyed by model id
9974
10097
  * (issue #34). An ABSENT key means "use the built-in rule" — the upstream's
@@ -10111,6 +10234,28 @@ function regionEnabled(config, region) {
10111
10234
  /** Every region, in card tab order. */
10112
10235
  const REGION_KEYS = ["cn", "global"];
10113
10236
  /**
10237
+ * The region that owns the image tool right now, or `undefined` when no region
10238
+ * has it switched on.
10239
+ *
10240
+ * BOTH gates are required, and both are read live: the region's provider switch
10241
+ * must be ON (`regionEnabled`, opt-OUT) and the user must have opted IN to the
10242
+ * model spending credits there (`imageToolEnabled`, opt-IN). Absence of
10243
+ * `imageToolEnabled` is never consent.
10244
+ *
10245
+ * CN wins when both regions qualify, and that order is the only backward
10246
+ * compatible one: before the international gateway carried a `text-to-image`
10247
+ * model (measured 2026-10-25: `hy-image-3.5`) CN was the only region that could
10248
+ * ever be opted in, so a profile that opted in there keeps using CN even after
10249
+ * the global tab is switched on as well. That is exactly `REGION_KEYS`' order;
10250
+ * it is not incidental.
10251
+ */
10252
+ function imageToolRegionOf(config) {
10253
+ for (const region of REGION_KEYS) {
10254
+ if (!regionEnabled(config, region)) continue;
10255
+ if (regionStateOf(config, region).imageToolEnabled === true) return region;
10256
+ }
10257
+ }
10258
+ /**
10114
10259
  * The account id a region should select, or `undefined` for the documented
10115
10260
  * default: follow whatever the WorkBuddy app is currently signed in as.
10116
10261
  *
@@ -10417,6 +10562,30 @@ function apply(ctx, config) {
10417
10562
  return client.fetchModels(credential, signal, keep);
10418
10563
  };
10419
10564
  /**
10565
+ * The region's MEDIA models, cached briefly for the card's read-only roster.
10566
+ *
10567
+ * Two callers ask the same question — the image tool (to validate a model
10568
+ * argument) and the card's roster block (to SHOW what exists) — and both get
10569
+ * the same answer from the same free GET. The cache exists because the card
10570
+ * re-reads it on every settings render, not because the answer is expensive.
10571
+ *
10572
+ * Region-keyed, and a failure is NOT cached: a gateway hiccup must not make
10573
+ * the roster read "no models" for five minutes.
10574
+ */
10575
+ const mediaCaches = /* @__PURE__ */ new Map();
10576
+ const readMediaModels = async (region, options) => {
10577
+ const cached = mediaCaches.get(region);
10578
+ const now = Date.now();
10579
+ if (options?.refresh !== true && cached !== void 0 && now - cached.at < 3e5) return cached.models;
10580
+ const credential = await stacks[region].store.resolve();
10581
+ const models = await client.fetchMediaModels(credential, options?.signal);
10582
+ mediaCaches.set(region, {
10583
+ at: Date.now(),
10584
+ models
10585
+ });
10586
+ return models;
10587
+ };
10588
+ /**
10420
10589
  * Stamp each model's LIVE load level onto the runtime roster.
10421
10590
  *
10422
10591
  * Deliberately NOT part of {@link discoverModels}: that reader feeds the
@@ -10552,19 +10721,24 @@ function apply(ctx, config) {
10552
10721
  let imageTools;
10553
10722
  let disposeImageTool;
10554
10723
  const syncImageTool = (value) => {
10555
- const wanted = regionEnabled(value, "cn") && regionStateOf(value, "cn").imageToolEnabled === true;
10724
+ const wanted = imageToolRegionOf(value) !== void 0;
10556
10725
  if (wanted && disposeImageTool === void 0 && imageTools !== void 0) {
10557
10726
  disposeImageTool = registerWorkBuddyImageTool(imageTools, {
10558
10727
  resolveCredential: async () => {
10559
- if (!regionEnabled(current(), "cn")) return void 0;
10560
- try {
10561
- return await stacks.cn.store.resolve();
10562
- } catch {
10563
- return;
10728
+ const live = current();
10729
+ for (const region of REGION_KEYS) {
10730
+ if (!regionEnabled(live, region)) continue;
10731
+ if (regionStateOf(live, region).imageToolEnabled !== true) continue;
10732
+ try {
10733
+ return await stacks[region].store.resolve();
10734
+ } catch {
10735
+ continue;
10736
+ }
10564
10737
  }
10565
10738
  },
10566
10739
  listMediaModels: (credential, signal) => client.fetchMediaModels(credential, signal),
10567
10740
  generateImage: (credential, request, signal) => client.generateImage(credential, request, signal),
10741
+ editImage: (credential, request, signal) => client.editImage(credential, request, signal),
10568
10742
  logger: ctx.logger
10569
10743
  });
10570
10744
  return;
@@ -10732,6 +10906,7 @@ function apply(ctx, config) {
10732
10906
  * counts as new — the one property that keeps the marker honest.
10733
10907
  */
10734
10908
  rosterSeen: (region) => stacks[region].rosterSeen.readout(),
10909
+ mediaModels: readMediaModels,
10735
10910
  regionEnabled: (region) => regionEnabled(current(), region),
10736
10911
  /**
10737
10912
  * The region's committed sidebar-credit flag, read from the same config the
@@ -11733,4 +11908,4 @@ function apply(ctx, config) {
11733
11908
  });
11734
11909
  }
11735
11910
  //#endregion
11736
- export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_TPS_PATH, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, modelIdOfBody, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, regionsFieldIsCorrupt, regionsMapOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
11911
+ export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_TPS_PATH, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, imageToolRegionOf, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, knownRegionOf, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, modelIdOfBody, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, regionsFieldIsCorrupt, regionsMapOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
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.9.0",
5
+ "version": "5.10.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",