dsh-connect-workbuddy 5.9.0 → 5.11.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, $t as withFileLock, A as isEncryptedCredentialError, At as pickMediaModel, B as WorkBuddyCredentialRejectedError, Bt as deriveAtRestKeyId, C as WorkBuddyCredentialStore, Ct as IMAGE_TAG_TEXT_TO_IMAGE, D as defaultDesktopAuthDirs, Dt as imageMimeForPath, E as defaultDesktopAuthCandidates, Et as imageDataUrl, F as workbuddyAccountId, Ft as imageDefaultFor, G as forceNeutralHead, Gt as isWorkbuddyBundle, H as channelGateMemberOf, Ht as findWorkbuddyAppExecutable, I as workbuddyOwnAuthPath, It as nativeModalityOf, J as isCredentialRejectedError, Jt as openEncryptedField, K as isContextTooLongBody, Kt as macosBundleExecutable, L as CONTEXT_OVERFLOW_MARKER, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, Mt as parseQueueAnswer, N as parseWorkBuddyAuth, Nt as queueNoticeText, O as defaultDesktopAuthPath, Ot as isBareBase64Image, P as preferredAccountName, Pt as NATIVE_MODALITY_BY_MODEL_ID, Q as openingEvidenceOfBody, Qt as resolveDshHome, R as CREDENTIAL_REJECTED_CODE, Rt as clearAtRestKeyCache, S as WORKBUDDY_AUTH_FILE_ENV, St as IMAGE_TAG_IMAGE_TO_IMAGE, T as authFileName, Tt as WorkBuddyImageError, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, Vt as fetchAtRestKeyPayload, W as declaredTools, Wt as isEncryptedFieldWrapper, X as knownRegionOf, Xt as workbuddyAppExecutableCandidates, Y as isFirstMessageGateBody, Yt as readAtRestKey, Z as modelIdOfBody, _ as deriveCatalog, _t as WorkBuddyVideoError, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as resolveVideoParams, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, en as writeFileAtomic, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as VIDEO_TAG_TEXT_TO_VIDEO, h as WorkBuddyCatalog, ht as VIDEO_TAG_IMAGE_TO_VIDEO, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as sniffImageMime, k as hasEncryptedCredentialFields, kt as parseImageDataUrl, 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 macosNestedAppBundles, 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 parseSupportedSecondsRange, w as WorkBuddyEncryptedCredentialError, wt as MAX_INPUT_IMAGE_BYTES, x as WORKBUDDY_AUTH_FILENAME, xt as DEFAULT_IMAGE_SIZE, y as withoutRouters, yt as pickVideoModel, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as deriveAtRestKey } from "./host-heartbeat-BYdr-pVq.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,14 +4899,14 @@ 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
- const FAILURE_COOLDOWN_MS = 3e4;
4893
- function asRecord(value) {
4905
+ const FAILURE_COOLDOWN_MS$1 = 3e4;
4906
+ function asRecord$1(value) {
4894
4907
  return typeof value === "object" && value !== null ? value : {};
4895
4908
  }
4896
- function asNonEmptyString(value) {
4909
+ function asNonEmptyString$1(value) {
4897
4910
  return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
4898
4911
  }
4899
4912
  /**
@@ -4904,7 +4917,7 @@ function asNonEmptyString(value) {
4904
4917
  * 这些失败码都发生在「请求已经发出去」之后——上游可能已经扣了积分或排了队,
4905
4918
  * 连续重试只会把它们再花一遍,所以进冷却。
4906
4919
  */
4907
- const BILLABLE_FAILURE_CODES = /* @__PURE__ */ new Set([
4920
+ const BILLABLE_FAILURE_CODES$1 = /* @__PURE__ */ new Set([
4908
4921
  "AUTH",
4909
4922
  "RATE_LIMIT",
4910
4923
  "NO_ROUTE",
@@ -4922,9 +4935,9 @@ const BILLABLE_FAILURE_CODES = /* @__PURE__ */ new Set([
4922
4935
  *
4923
4936
  * 现在由调用点显式置位:请求发出去了才置 `sent`,冷却只看它。
4924
4937
  */
4925
- function isBillableFailure(error, sent) {
4938
+ function isBillableFailure$1(error, sent) {
4926
4939
  if (!sent) return false;
4927
- if (error instanceof WorkBuddyImageError) return BILLABLE_FAILURE_CODES.has(error.code);
4940
+ if (error instanceof WorkBuddyImageError) return BILLABLE_FAILURE_CODES$1.has(error.code);
4928
4941
  return true;
4929
4942
  }
4930
4943
  /** 把 b64 图片落成一个临时文件,返回绝对路径。 */
@@ -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: {
@@ -4969,11 +5046,15 @@ function createImageTool(deps) {
4969
5046
  },
4970
5047
  size: {
4971
5048
  type: "string",
4972
- description: `图片尺寸,形如 1024x1024。默认 ${DEFAULT_IMAGE_SIZE}。`
5049
+ description: `图片尺寸,写成「宽x高」(像素)。比例由宽高决定,不限于方图。常用三档:方图 1024x1024、竖图 1024x1536、横图 1536x1024。要大图就按「总像素 419 万以内」放,出的是真实像素不是插值放大:2048x2048(4.19MP,方形上限)、2048x1536、2560x1440(16:9 桌面壁纸)、2560x1600。竖版同理,把长边放到 2048–2560 那一档。超过这个量级会被上游直接拒绝、不会悄悄降级(2560x2560、3072x3072、4096x4096 都失败),所以不要填 4K 尺寸,也不要指望传大数字能拿到更多细节。不填默认 ${DEFAULT_IMAGE_SIZE}(方图)。用户要竖版 / 横版 / 手机壁纸 / 封面 / 高清大图时,改这里的宽高,不要只在 prompt 里说。`
5050
+ },
5051
+ image: {
5052
+ type: "string",
5053
+ description: "输入图,可选。填了就是改图(image-to-image);不填就是文生图。接受本地绝对路径、file:// URL、http(s) URL、data URL 或裸 base64。上限 8 MB,不做缩放或格式转换。"
4973
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"
@@ -5017,16 +5103,17 @@ function createImageTool(deps) {
5017
5103
  additionalProperties: false
5018
5104
  },
5019
5105
  render(_args, value) {
5020
- const record = asRecord(value);
5021
- const url = asNonEmptyString(record["url"]) ?? "";
5022
- const model = asNonEmptyString(record["model"]) ?? "unknown";
5106
+ const record = asRecord$1(value);
5107
+ const url = asNonEmptyString$1(record["url"]) ?? "";
5108
+ const model = asNonEmptyString$1(record["model"]) ?? "unknown";
5109
+ const mode = record["mode"] === "edit" ? "改图" : "文生图";
5023
5110
  const credit = typeof record["credit"] === "number" ? record["credit"] : void 0;
5024
- const path = asNonEmptyString(record["path"]);
5025
- const hint = asNonEmptyString(record["hint"]) ?? "";
5111
+ const path = asNonEmptyString$1(record["path"]);
5112
+ const hint = asNonEmptyString$1(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);
@@ -5038,33 +5125,42 @@ function createImageTool(deps) {
5038
5125
  },
5039
5126
  timeoutMs: IMAGE_TOOL_TIMEOUT_MS,
5040
5127
  async execute(args, exec) {
5041
- const record = asRecord(args);
5042
- const prompt = asNonEmptyString(record["prompt"]);
5128
+ const record = asRecord$1(args);
5129
+ const prompt = asNonEmptyString$1(record["prompt"]);
5043
5130
  if (prompt === void 0) throw new WorkBuddyImageError(`${WORKBUDDY_IMAGE_TOOL_NAME}: 参数 prompt 必填,且必须是非空字符串。`, "INVALID_ARGS");
5044
- const size = asNonEmptyString(record["size"]) ?? "1024x1024";
5045
- const requestedModel = asNonEmptyString(record["model"]);
5131
+ const size = asNonEmptyString$1(record["size"]) ?? "1024x1024";
5132
+ const requestedModel = asNonEmptyString$1(record["model"]);
5133
+ const image = asNonEmptyString$1(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) {
@@ -5076,7 +5172,7 @@ function createImageTool(deps) {
5076
5172
  if (outcome.credit !== void 0) value["credit"] = outcome.credit;
5077
5173
  return value;
5078
5174
  } catch (error) {
5079
- if (isBillableFailure(error, sent)) cooldownUntil = Date.now() + FAILURE_COOLDOWN_MS;
5175
+ if (isBillableFailure$1(error, sent)) cooldownUntil = Date.now() + FAILURE_COOLDOWN_MS$1;
5080
5176
  throw error;
5081
5177
  }
5082
5178
  }
@@ -5097,6 +5193,379 @@ function registerWorkBuddyImageTool(tools, deps) {
5097
5193
  }
5098
5194
  }
5099
5195
  //#endregion
5196
+ //#region src/video-tool.ts
5197
+ /**
5198
+ * `workbuddy_draw_video`:把 WorkBuddy 的视频生成能力(国内区与国际区各自的
5199
+ * `text-to-video` / `image-to-video` 模型)暴露成一个 DSH agent 工具。
5200
+ *
5201
+ * 与生图工具(一次同步请求就拿回图片)不同,视频是**异步任务**:
5202
+ * 先 `submitVideo` 拿到 task id,再 `pollVideoTask` 轮询到 completed,
5203
+ * 最后 `downloadVideo` 把每条视频 URL 落成本地 `.mp4`。这一整段都照搬
5204
+ * 官方 CLI 的 `VideoService`(submitTask / pollTask / downloadVideos),
5205
+ * 详见 docs/VIDEO_GEN.md。
5206
+ *
5207
+ * 参数 / 选模型层与生图工具几乎完全一致(共用 MEDIA_TAGS、video 标签常量、
5208
+ * 媒体目录缓存),所以「接受参数」这部分本插件已经就绪;真正多写的是
5209
+ * 上面那段异步执行流程。
5210
+ *
5211
+ * 为什么手写 ToolDefinition(不 import `@deepseek-ai/dsh-tools` 的 `defineTool`):
5212
+ * 同 image-tool.ts 的注释——宿主 `Tools.register` 只校验 `output.render` 是函数、
5213
+ * `output.schema` 是受支持的 JSON Schema,`schemaOf()` 把 `parameters` 原样投影,
5214
+ * 手写换来零新 peer 依赖、不会触发 tests/host-peers.spec.ts 对可达图的断言。
5215
+ *
5216
+ * @module dsh-connect-workbuddy/video-tool
5217
+ */
5218
+ /** 工具名。宿主禁止 `run_code`,这个名字与其它 connect 插件的命名习惯一致。 */
5219
+ const WORKBUDDY_VIDEO_TOOL_NAME = "workbuddy_draw_video";
5220
+ /** 工具单次执行的上限:提交 + 轮询(最多 ~10 分钟)+ 下载,留足余量。 */
5221
+ const VIDEO_TOOL_TIMEOUT_MS = 6e5;
5222
+ /** 下载远程输入图(图生视频时)的上限,不含生视频本身。 */
5223
+ const VIDEO_INPUT_TIMEOUT_MS = 3e4;
5224
+ /** 失败后的冷却:视频按次扣积分且一次要几十秒,连续重试会烧掉大量积分。 */
5225
+ const FAILURE_COOLDOWN_MS = 6e4;
5226
+ function asRecord(value) {
5227
+ return typeof value === "object" && value !== null ? value : {};
5228
+ }
5229
+ function asNonEmptyString(value) {
5230
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
5231
+ }
5232
+ function asNumber(value) {
5233
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5234
+ }
5235
+ function asBoolean(value) {
5236
+ return typeof value === "boolean" ? value : void 0;
5237
+ }
5238
+ /**
5239
+ * 这些失败说明请求真的打到了上游、可能已经扣了积分,才值得进冷却。
5240
+ * 参数写错 / 未登录 / 目录里没有模型都还没花钱,轮询也已先花过提交那次的钱。
5241
+ *
5242
+ * 注意 `REJECTED` **不在**这里:提交阶段被上游 4xx 退回时任务根本没创建、
5243
+ * 积分没花,冷却只会让一次「参数写错了」变成白等一分钟。
5244
+ */
5245
+ const BILLABLE_FAILURE_CODES = /* @__PURE__ */ new Set([
5246
+ "AUTH",
5247
+ "RATE_LIMIT",
5248
+ "NO_ROUTE",
5249
+ "UPSTREAM",
5250
+ "EMPTY_RESULT",
5251
+ "TASK_FAILED",
5252
+ "POLL_TIMEOUT"
5253
+ ]);
5254
+ /**
5255
+ * 是否该进冷却——由「这一次到底有没有把生视频请求发出去」决定。
5256
+ * 读目录 / 选模型 / 参数校验全在「发请求」之前,失败了不花积分、不冷却。
5257
+ * 一旦 `submitVideo` 被调用,提交那次就花钱了,后续轮询 / 下载失败一律进冷却。
5258
+ */
5259
+ function isBillableFailure(error, sent) {
5260
+ if (!sent) return false;
5261
+ if (error instanceof WorkBuddyVideoError) return BILLABLE_FAILURE_CODES.has(error.code);
5262
+ return true;
5263
+ }
5264
+ /**
5265
+ * 把任意形态的输入图规整成上游唯一接受的形态:data URL 或 http(s) URL。
5266
+ *
5267
+ * 图生视频的 `image_url` 字段只接受上游可访问的 URL。本地文件路径必须
5268
+ * 读进来、编码成 data URL 再发,否则上游读到的是空响应,返回
5269
+ * "Input ImageUrl data is invalid (too short)"。与 `image-tool.ts` 的
5270
+ * `resolveImageInput` 同一套逻辑:data URL 校验后直传,http(s) 校验可达性
5271
+ * 后直传,裸 base64 / 本地路径 / file:// 读文件编成 data URL。不做缩放
5272
+ * 或格式转换——用户给什么就传什么。
5273
+ */
5274
+ async function resolveVideoImageInput(value, signal) {
5275
+ const text = value.trim();
5276
+ if (text.startsWith("data:")) {
5277
+ const parsed = parseImageDataUrl(text);
5278
+ if (parsed === void 0) throw new WorkBuddyVideoError("workbuddy video: image 看起来是 data URL,但格式不对(需要 data:image/*;base64,...)。", "INVALID_ARGS");
5279
+ assertVideoInputImageBytes(Buffer.from(parsed.base64, "base64").byteLength, "(内联 data URL)");
5280
+ return text;
5281
+ }
5282
+ if (/^https?:\/\//i.test(text)) {
5283
+ try {
5284
+ const response = await fetch(text, {
5285
+ method: "HEAD",
5286
+ signal: signal ?? AbortSignal.timeout(VIDEO_INPUT_TIMEOUT_MS)
5287
+ });
5288
+ if (!response.ok) throw new WorkBuddyVideoError(`workbuddy video: 输入图 URL 不可达(HTTP ${response.status}): ${text}`, "INVALID_ARGS");
5289
+ } catch (error) {
5290
+ if (error instanceof WorkBuddyVideoError) throw error;
5291
+ throw new WorkBuddyVideoError(`workbuddy video: 校验输入图 URL 失败(${text}):` + (error instanceof Error ? error.message : String(error)), "INVALID_ARGS");
5292
+ }
5293
+ return text;
5294
+ }
5295
+ if (isBareBase64Image(text)) return encodeVideoInputImage(Buffer.from(text, "base64"), "(内联 base64)");
5296
+ const path = text.startsWith("file:") ? fileURLToPath(text) : text;
5297
+ let bytes;
5298
+ try {
5299
+ bytes = await readFile(path);
5300
+ } catch (error) {
5301
+ throw new WorkBuddyVideoError(`workbuddy video: 读不到输入图「${path}」:` + (error instanceof Error ? error.message : String(error)), "INVALID_ARGS");
5302
+ }
5303
+ return encodeVideoInputImage(bytes, path);
5304
+ }
5305
+ /**
5306
+ * 体积与空文件闸门——所有输入图都从这里过,所以「空文件」和「超过 8 MB」
5307
+ * 的文案只有一份。与 `image-tool.ts` 的 `assertInputImageBytes` 同逻辑。
5308
+ *
5309
+ * `byteLength` 必须是解码后的字节数:base64 比原始字节长约 4/3。
5310
+ */
5311
+ function assertVideoInputImageBytes(byteLength, source) {
5312
+ if (byteLength === 0) throw new WorkBuddyVideoError(`workbuddy video: 输入图是空文件:${source}`, "INVALID_ARGS");
5313
+ if (byteLength > 8388608) {
5314
+ const limit = MAX_INPUT_IMAGE_BYTES / 1024 / 1024;
5315
+ const actual = (byteLength / 1024 / 1024).toFixed(1);
5316
+ throw new WorkBuddyVideoError(`workbuddy video: 输入图 ${actual} MB,超过 ${limit} MB 上限:${source}`, "INVALID_ARGS");
5317
+ }
5318
+ }
5319
+ /** 校验体积、定出 MIME,再把字节编成 data URL。 */
5320
+ function encodeVideoInputImage(bytes, source) {
5321
+ assertVideoInputImageBytes(bytes.byteLength, source);
5322
+ const mime = imageMimeForPath(source) ?? sniffImageMime(bytes);
5323
+ return imageDataUrl(mime, Buffer.from(bytes).toString("base64"));
5324
+ }
5325
+ /**
5326
+ * 造出工具定义。每次 `apply()` 只造一个实例,冷却状态与目录缓存都挂在闭包里,
5327
+ * 生命周期跟着插件走。
5328
+ */
5329
+ function createVideoTool(deps) {
5330
+ let mediaCache;
5331
+ let cooldownUntil = 0;
5332
+ /**
5333
+ * 上游在拒绝一次提交时顺带告诉我们的「这个模型支持几秒」。
5334
+ *
5335
+ * 目录里没有 `videoGeneration` 的账号(实测国内区部分账号的 `/v3/config`
5336
+ * 条目只有 `{id,name,tags}`)本地无从校验,只能让上游先拒一次。它拒的时候
5337
+ * 会把区间写在文案里,这里记下来:同一个进程里第二次给同样的越界值,就在本地
5338
+ * 被 `resolveVideoParams` 拦下——零请求、零冷却。键带上区域名,因为模型 id
5339
+ * 不跨区(同名 id 在两个区可能是不同模型)。
5340
+ */
5341
+ const learnedSeconds = /* @__PURE__ */ new Map();
5342
+ const secondsKey = (domain, modelId) => `${domain}|${modelId}`;
5343
+ const listModels = async (credential, signal) => {
5344
+ const now = Date.now();
5345
+ if (mediaCache !== void 0 && now - mediaCache.at < 3e5) return mediaCache.models;
5346
+ const models = await deps.listMediaModels(credential, signal);
5347
+ mediaCache = {
5348
+ at: now,
5349
+ models
5350
+ };
5351
+ return models;
5352
+ };
5353
+ return {
5354
+ name: WORKBUDDY_VIDEO_TOOL_NAME,
5355
+ description: "用当前账号的 WorkBuddy 视频生成能力生成一段短视频。不填 image 是文生视频(用该区带 text-to-video 标签的模型);填了 image 是图生视频(用带 image-to-video 标签的模型)。国内区与国际区都可用,模型取自该区产品配置(国内区 kling-video-v3、minimax-video-h3;国际区 kling-video-v3,两种 video tag 都有;以该区目录为准)。视频是异步任务:先提交、再轮询到完成、最后把视频下载到本地,单次可能要几十秒到几分钟。每次生成会真实消耗 WorkBuddy 积分,单价随区域与模型不同,以返回结果里的 credit 为准;不要在没必要时调用,也不要为了试错连续调用。返回的是本地 .mp4 文件路径(file:// URL),由宿主/客户端打开或下载。",
5356
+ parameters: {
5357
+ type: "object",
5358
+ properties: {
5359
+ prompt: {
5360
+ type: "string",
5361
+ description: "画面描述(中文或英文都可以),≤2500 字符。写清楚主体、动作、风格、镜头。"
5362
+ },
5363
+ image: {
5364
+ type: "string",
5365
+ description: "输入图,可选。填了就是图生视频(image-to-video);不填就是文生视频。接受本地绝对路径、file:// URL、http(s) URL、data URL 或裸 base64。上限 8 MB,不做缩放或格式转换。"
5366
+ },
5367
+ model: {
5368
+ type: "string",
5369
+ description: "模型 id,可选。不填就用用户在设置卡片上按模式钉住的模型;卡片上没钉就用该区目录里第一个带当前模式标签的模型(文生视频看 text-to-video,图生视频看 image-to-video)。填了但该 tag 组里没有会直接报错,不会盲发。"
5370
+ },
5371
+ seconds: {
5372
+ type: "number",
5373
+ description: "视频时长(秒),可选。不填用模型默认(通常 5s)。该模型在目录里给了 videoGeneration.minSeconds..maxSeconds 时,越界直接报错、不会静默夹取;目录没给区间时交给上游判断,被拒的错误里会带上游支持的范围,改完可立刻重试(不计失败冷却)。"
5374
+ },
5375
+ resolution: {
5376
+ type: "string",
5377
+ description: "分辨率,可选,如 720P / 1080P(大小写不敏感,按模型目录里的拼写发)。不填用模型默认。该模型在目录里给了 videoGeneration.resolutions 时,不在列内会回落到默认分辨率并给出告警;目录没给这份清单时原样透传,交给上游判断(与 seconds 同一条判据)。"
5378
+ },
5379
+ aspect_ratio: {
5380
+ type: "string",
5381
+ description: "画面比例,仅文生视频有效,可选:16:9 / 9:16 / 1:1。不填默认 16:9。图生视频由输入图决定比例,传了会被忽略。"
5382
+ },
5383
+ negative_prompt: {
5384
+ type: "string",
5385
+ description: "不希望出现的元素,可选,≤2500 字符。"
5386
+ },
5387
+ enable_audio: {
5388
+ type: "boolean",
5389
+ description: "是否生成带声音的视频,可选,默认 true。"
5390
+ }
5391
+ },
5392
+ required: ["prompt"],
5393
+ additionalProperties: false
5394
+ },
5395
+ output: {
5396
+ schema: {
5397
+ type: "object",
5398
+ properties: {
5399
+ videos: {
5400
+ type: "array",
5401
+ items: { type: "string" },
5402
+ description: "生成的视频本地路径列表(file:// URL,.mp4)。"
5403
+ },
5404
+ mode: {
5405
+ type: "string",
5406
+ description: "'text-to-video' 或 'image-to-video'。"
5407
+ },
5408
+ model: {
5409
+ type: "string",
5410
+ description: "实际使用的模型 id。"
5411
+ },
5412
+ prompt: {
5413
+ type: "string",
5414
+ description: "实际提交的画面描述。"
5415
+ },
5416
+ seconds: {
5417
+ type: "number",
5418
+ description: "实际使用的时长(秒)。"
5419
+ },
5420
+ resolution: {
5421
+ type: "string",
5422
+ description: "实际使用的分辨率。"
5423
+ },
5424
+ hint: {
5425
+ type: "string",
5426
+ description: "给模型的后续提示(如何打开、扣费)。"
5427
+ },
5428
+ credit: {
5429
+ type: "number",
5430
+ description: "本次实际消耗的 WorkBuddy 积分。"
5431
+ }
5432
+ },
5433
+ required: [
5434
+ "videos",
5435
+ "mode",
5436
+ "model",
5437
+ "prompt",
5438
+ "seconds",
5439
+ "resolution",
5440
+ "hint"
5441
+ ],
5442
+ additionalProperties: false
5443
+ },
5444
+ render(_args, value) {
5445
+ const record = asRecord(value);
5446
+ const videos = Array.isArray(record["videos"]) ? record["videos"].filter((v) => typeof v === "string") : [];
5447
+ const model = asNonEmptyString(record["model"]) ?? "unknown";
5448
+ const modeLabel = record["mode"] === "image-to-video" ? "图生视频" : "文生视频";
5449
+ const seconds = typeof record["seconds"] === "number" ? record["seconds"] : void 0;
5450
+ const resolution = asNonEmptyString(record["resolution"]);
5451
+ const credit = typeof record["credit"] === "number" ? record["credit"] : void 0;
5452
+ const hint = asNonEmptyString(record["hint"]) ?? "";
5453
+ const lines = [`${modeLabel} · 模型:${model}` + (seconds === void 0 ? "" : ` · ${seconds}s`) + (resolution === void 0 ? "" : ` · ${resolution}`) + (credit === void 0 ? "" : ` · 消耗 ${credit} 积分`), ""];
5454
+ if (videos.length === 0) lines.push("(没有返回任何视频文件)");
5455
+ else videos.forEach((v, i) => {
5456
+ lines.push(`视频 ${i + 1}: [${v}](${v})`);
5457
+ });
5458
+ if (hint !== "") lines.push("", hint);
5459
+ return [{
5460
+ type: "text",
5461
+ text: lines.join("\n")
5462
+ }];
5463
+ }
5464
+ },
5465
+ timeoutMs: VIDEO_TOOL_TIMEOUT_MS,
5466
+ async execute(args, exec) {
5467
+ const record = asRecord(args);
5468
+ const prompt = asNonEmptyString(record["prompt"]);
5469
+ if (prompt === void 0) throw new WorkBuddyVideoError(`${WORKBUDDY_VIDEO_TOOL_NAME}: 参数 prompt 必填,且必须是非空字符串。`, "INVALID_ARGS");
5470
+ const requestedModel = asNonEmptyString(record["model"]);
5471
+ const requestedSeconds = asNumber(record["seconds"]);
5472
+ const requestedResolution = asNonEmptyString(record["resolution"]);
5473
+ const requestedAspectRatio = asNonEmptyString(record["aspect_ratio"]);
5474
+ const requestedNegativePrompt = asNonEmptyString(record["negative_prompt"]);
5475
+ const requestedEnableAudio = asBoolean(record["enable_audio"]);
5476
+ const image = asNonEmptyString(record["image"]);
5477
+ const now = Date.now();
5478
+ if (now < cooldownUntil) {
5479
+ const seconds = Math.ceil((cooldownUntil - now) / 1e3);
5480
+ throw new WorkBuddyVideoError(`workbuddy video: 上一次生视频刚失败过,请 ${seconds} 秒后再试(失败冷却是为了避免连续重试白白烧积分)。`, "COOLDOWN");
5481
+ }
5482
+ const credential = await deps.resolveCredential();
5483
+ if (credential === void 0) throw new WorkBuddyVideoError("workbuddy video: 没有可用的 WorkBuddy 账号——要么两个区都没登录,要么已登录的那个区没有打开「允许模型自主生图」。请先在插件设置里登录该区账号,并打开那个开关。", "NOT_LOGGED_IN");
5484
+ const signal = exec.signal === void 0 ? AbortSignal.timeout(VIDEO_TOOL_TIMEOUT_MS) : AbortSignal.any([exec.signal, AbortSignal.timeout(VIDEO_TOOL_TIMEOUT_MS)]);
5485
+ let sent = false;
5486
+ let chosen;
5487
+ try {
5488
+ const models = await listModels(credential, signal);
5489
+ const imageInput = image === void 0 ? void 0 : await resolveVideoImageInput(image, signal);
5490
+ const tag = imageInput === void 0 ? VIDEO_TAG_TEXT_TO_VIDEO : VIDEO_TAG_IMAGE_TO_VIDEO;
5491
+ const pinned = requestedModel === void 0 ? deps.preferredVideoModel?.(credential, tag) : void 0;
5492
+ let model;
5493
+ try {
5494
+ model = pickVideoModel(models, requestedModel ?? pinned, tag);
5495
+ } catch (error) {
5496
+ if (requestedModel !== void 0 || pinned === void 0) throw error;
5497
+ model = pickVideoModel(models, void 0, tag);
5498
+ }
5499
+ chosen = model;
5500
+ const learned = learnedSeconds.get(secondsKey(credential.domain, model.id));
5501
+ const modelConfig = learned === void 0 ? model.videoGeneration : {
5502
+ ...model.videoGeneration,
5503
+ ...learned
5504
+ };
5505
+ const resolved = resolveVideoParams({
5506
+ model: model.id,
5507
+ prompt,
5508
+ seconds: requestedSeconds,
5509
+ negativePrompt: requestedNegativePrompt,
5510
+ image: imageInput,
5511
+ resolution: requestedResolution,
5512
+ aspectRatio: requestedAspectRatio,
5513
+ enableAudio: requestedEnableAudio
5514
+ }, modelConfig);
5515
+ const request = {
5516
+ model: model.id,
5517
+ prompt,
5518
+ seconds: resolved.seconds,
5519
+ negativePrompt: requestedNegativePrompt,
5520
+ image: imageInput,
5521
+ resolution: resolved.resolution,
5522
+ aspectRatio: resolved.aspectRatio,
5523
+ enableAudio: requestedEnableAudio
5524
+ };
5525
+ sent = true;
5526
+ const submission = await deps.submitVideo(credential, request, signal);
5527
+ const outcome = await deps.pollVideoTask(credential, submission.taskId, signal);
5528
+ const paths = [];
5529
+ for (const url of outcome.videos) {
5530
+ const local = await deps.downloadVideo(credential, url, signal);
5531
+ paths.push(pathToFileURL(local).href);
5532
+ }
5533
+ const mode = imageInput === void 0 ? "text-to-video" : "image-to-video";
5534
+ const value = {
5535
+ videos: paths,
5536
+ mode,
5537
+ model: model.id,
5538
+ prompt,
5539
+ seconds: resolved.seconds,
5540
+ resolution: resolved.resolution ?? "",
5541
+ hint: `这段视频来自 WorkBuddy(模型 ${model.id},${mode === "image-to-video" ? "图生视频" : "文生视频"})。按次扣积分,单价随区域与模型不同` + (outcome.credit === void 0 ? "(本次响应没带扣费字段,以额度面板为准)。" : `;本次实际扣费 ${outcome.credit} 积分。`) + (resolved.warnings.length > 0 ? ` 提示:${resolved.warnings.join(";")}。` : "") + "视频已下载到本地,请用 file:// 链接打开或下载保存。"
5542
+ };
5543
+ if (outcome.credit !== void 0) value["credit"] = outcome.credit;
5544
+ return value;
5545
+ } catch (error) {
5546
+ if (error instanceof WorkBuddyVideoError && error.code === "REJECTED" && chosen !== void 0) {
5547
+ const range = parseSupportedSecondsRange(error.message);
5548
+ if (range !== void 0) learnedSeconds.set(secondsKey(credential.domain, chosen.id), range);
5549
+ }
5550
+ if (isBillableFailure(error, sent)) cooldownUntil = Date.now() + FAILURE_COOLDOWN_MS;
5551
+ throw error;
5552
+ }
5553
+ }
5554
+ };
5555
+ }
5556
+ /**
5557
+ * 把工具注册进宿主。返回 disposer;调用方负责在插件卸载时调用它。
5558
+ * 注册本身是同步的;`tools` 服务由宿主提供,拿不到就直接返回 undefined 并记警告。
5559
+ */
5560
+ function registerWorkBuddyVideoTool(tools, deps) {
5561
+ try {
5562
+ return tools.register(createVideoTool(deps));
5563
+ } catch (error) {
5564
+ deps.logger.warn(`dsh-connect-workbuddy: 注册 ${WORKBUDDY_VIDEO_TOOL_NAME} 失败: ` + (error instanceof Error ? error.message : String(error)));
5565
+ return;
5566
+ }
5567
+ }
5568
+ //#endregion
5100
5569
  //#region src/cli-bridge/redact.ts
5101
5570
  /**
5102
5571
  * Scrubbing CLI output before it can reach a conversation.
@@ -9093,6 +9562,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
9093
9562
  const showCreditsInMainUi = deps.showCreditsInMainUi?.(region) ?? true;
9094
9563
  const creditPosition = deps.creditPosition?.(region) ?? "composer";
9095
9564
  const imageToolEnabled = deps.imageToolEnabled?.(region) ?? false;
9565
+ const videoToolEnabled = deps.videoToolEnabled?.(region) ?? false;
9566
+ const preferredVideoModels = deps.preferredVideoModels?.(region) ?? {};
9096
9567
  const cliBridgeEnabled = deps.cliBridgeEnabled?.() === true;
9097
9568
  const cliBridgeModel = deps.cliBridgeModel?.() ?? "";
9098
9569
  const cliBridgeEffort = deps.cliBridgeEffort?.() ?? "";
@@ -9107,6 +9578,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
9107
9578
  enabled,
9108
9579
  showCreditsInMainUi,
9109
9580
  imageToolEnabled,
9581
+ videoToolEnabled,
9582
+ preferredVideoModels,
9110
9583
  cliBridgeEnabled,
9111
9584
  cliBridgeModel,
9112
9585
  cliBridgeEffort,
@@ -9128,6 +9601,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
9128
9601
  showCreditsInMainUi,
9129
9602
  creditPosition,
9130
9603
  imageToolEnabled,
9604
+ videoToolEnabled,
9605
+ preferredVideoModels,
9131
9606
  cliBridgeEnabled,
9132
9607
  cliBridgeModel,
9133
9608
  cliBridgeEffort,
@@ -9150,6 +9625,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
9150
9625
  showCreditsInMainUi,
9151
9626
  creditPosition,
9152
9627
  imageToolEnabled,
9628
+ videoToolEnabled,
9629
+ preferredVideoModels,
9153
9630
  cliBridgeEnabled,
9154
9631
  cliBridgeModel,
9155
9632
  cliBridgeEffort,
@@ -9472,6 +9949,27 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
9472
9949
  }
9473
9950
  }
9474
9951
  });
9952
+ const disposeMediaModels = ctx.webServer.register({
9953
+ kind: "exact",
9954
+ path: WORKBUDDY_MEDIA_MODELS_PATH,
9955
+ handler: async (req, res) => {
9956
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
9957
+ if (!requireLoopback(req, res)) return;
9958
+ if (deps.mediaModels === void 0) return json(res, 503, { error: "media model list unavailable" });
9959
+ const region = requestRegion(req, res);
9960
+ if (region === void 0) return;
9961
+ try {
9962
+ const refresh = new URL(req.url ?? "/", "http://localhost").searchParams.get("refresh") === "1";
9963
+ json(res, 200, { models: (await deps.mediaModels(region, { refresh })).map((model) => ({
9964
+ id: model.id,
9965
+ name: model.name,
9966
+ tags: [...model.tags]
9967
+ })) });
9968
+ } catch (error) {
9969
+ json(res, 500, { error: safeMessage(error) });
9970
+ }
9971
+ }
9972
+ });
9475
9973
  const disposeProbe = ctx.webServer.register({
9476
9974
  kind: "exact",
9477
9975
  path: WORKBUDDY_PROBE_PATH,
@@ -9847,6 +10345,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
9847
10345
  return () => {
9848
10346
  disposeBenefitClaim();
9849
10347
  disposeRefresh();
10348
+ disposeMediaModels();
9850
10349
  disposeCheckin();
9851
10350
  disposeAccounts();
9852
10351
  disposeUsage();
@@ -9950,25 +10449,48 @@ const regionStateConfig = z.object({
9950
10449
  /**
9951
10450
  * Whether the model may call the image-generation tool (opt-IN, default off).
9952
10451
  *
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`).
10452
+ * Per-REGION like the credit switches, and it now means the same thing on both
10453
+ * tabs: each region's catalog carries its own `text-to-image` model (CN
10454
+ * `hunyuan-image-alpha`, international `hy-image-3.5`; measured 2026-10-25,
10455
+ * see `docs/IMAGE_GEN.md`). Whichever region is switched on AND opted in owns
10456
+ * the tool; CN wins when both qualify (see `imageToolRegionOf`).
9956
10457
  *
9957
10458
  * Every other spending path in this plugin requires a human action: a chat
9958
10459
  * request is one the user sent, and the per-model "test" button is one the
9959
10460
  * user clicked. `workbuddy_draw_image` is the first capability where the
9960
10461
  * 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`).
10462
+ * an image on CN and 0.03 on the international gateway (measured, not a price
10463
+ * list: the media catalog carries no cost, so the tool reports the
10464
+ * `usage.credit` the upstream actually settles), and there is no per-call
10465
+ * approval seam in the host to ask the user first (`needsApproval` /
10466
+ * `toolApproval` do not exist; verified against the shipped `app.asar`).
9964
10467
  *
9965
10468
  * So it is opt-in, the same way the account pool's credit-based rotation is
9966
10469
  * opt-in (3.0.0's reasoning): a feature that can move money on its own must be
9967
10470
  * 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.
10471
+ * means that region contributes nothing, and with no opted-in region at all
10472
+ * the tool is not REGISTERED, so the model never sees it and cannot attempt a
10473
+ * call that would be refused.
10474
+ */
10475
+ 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."),
10476
+ /**
10477
+ * Whether the model may call the video-generation tool (opt-IN, default off).
10478
+ * Per-REGION like `imageToolEnabled`, gated the same way: the region must be
10479
+ * switched on AND the user must opt in. CN wins when both qualify. A video
10480
+ * costs real credits per generation and the task runs tens of seconds to
10481
+ * minutes, so no per-call approval exists in the host — hence opt-in.
10482
+ */
10483
+ videoToolEnabled: z.boolean().default(false).description("Let the model call workbuddy_draw_video on its own (opt-in; a video is an async task that costs WorkBuddy credits and runs tens of seconds to minutes). Off by default, and off means no region registers the tool at all."),
10484
+ /**
10485
+ * The video model pinned for one tag (`text-to-video` / `image-to-video`),
10486
+ * as a model id from THIS region's catalog.
10487
+ *
10488
+ * Written by the card's per-mode picker. The tool reads it only when the call
10489
+ * itself did not name a model, and falls back to the group's first model when
10490
+ * the pinned id is no longer in this region's directory — see
10491
+ * `preferredVideoModel` in `src/video-tool.ts`.
9970
10492
  */
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."),
10493
+ preferredVideoModels: z.dict(z.string()).default({}).description("Video model pinned per tag (\"text-to-video\"/\"image-to-video\"), as a model id from this region catalog. Used only when a tool call names no model."),
9972
10494
  /**
9973
10495
  * Per-model override of whether thinking `off` is offered, keyed by model id
9974
10496
  * (issue #34). An ABSENT key means "use the built-in rule" — the upstream's
@@ -10111,6 +10633,41 @@ function regionEnabled(config, region) {
10111
10633
  /** Every region, in card tab order. */
10112
10634
  const REGION_KEYS = ["cn", "global"];
10113
10635
  /**
10636
+ * The region that owns the image tool right now, or `undefined` when no region
10637
+ * has it switched on.
10638
+ *
10639
+ * BOTH gates are required, and both are read live: the region's provider switch
10640
+ * must be ON (`regionEnabled`, opt-OUT) and the user must have opted IN to the
10641
+ * model spending credits there (`imageToolEnabled`, opt-IN). Absence of
10642
+ * `imageToolEnabled` is never consent.
10643
+ *
10644
+ * CN wins when both regions qualify, and that order is the only backward
10645
+ * compatible one: before the international gateway carried a `text-to-image`
10646
+ * model (measured 2026-10-25: `hy-image-3.5`) CN was the only region that could
10647
+ * ever be opted in, so a profile that opted in there keeps using CN even after
10648
+ * the global tab is switched on as well. That is exactly `REGION_KEYS`' order;
10649
+ * it is not incidental.
10650
+ */
10651
+ function imageToolRegionOf(config) {
10652
+ for (const region of REGION_KEYS) {
10653
+ if (!regionEnabled(config, region)) continue;
10654
+ if (regionStateOf(config, region).imageToolEnabled === true) return region;
10655
+ }
10656
+ }
10657
+ /**
10658
+ * Region that owns the video tool right now, or `undefined` when no region has
10659
+ * it switched on. Mirrors `imageToolRegionOf` but reads `videoToolEnabled`
10660
+ * (the video-generation opt-in). Both gates required, read live: region provider
10661
+ * switch ON (`regionEnabled`, opt-OUT) and the user opted IN (`videoToolEnabled`,
10662
+ * opt-IN). CN wins when both qualify (see `REGION_KEYS`).
10663
+ */
10664
+ function videoToolRegionOf(config) {
10665
+ for (const region of REGION_KEYS) {
10666
+ if (!regionEnabled(config, region)) continue;
10667
+ if (regionStateOf(config, region).videoToolEnabled === true) return region;
10668
+ }
10669
+ }
10670
+ /**
10114
10671
  * The account id a region should select, or `undefined` for the documented
10115
10672
  * default: follow whatever the WorkBuddy app is currently signed in as.
10116
10673
  *
@@ -10417,6 +10974,30 @@ function apply(ctx, config) {
10417
10974
  return client.fetchModels(credential, signal, keep);
10418
10975
  };
10419
10976
  /**
10977
+ * The region's MEDIA models, cached briefly for the card's read-only roster.
10978
+ *
10979
+ * Two callers ask the same question — the image tool (to validate a model
10980
+ * argument) and the card's roster block (to SHOW what exists) — and both get
10981
+ * the same answer from the same free GET. The cache exists because the card
10982
+ * re-reads it on every settings render, not because the answer is expensive.
10983
+ *
10984
+ * Region-keyed, and a failure is NOT cached: a gateway hiccup must not make
10985
+ * the roster read "no models" for five minutes.
10986
+ */
10987
+ const mediaCaches = /* @__PURE__ */ new Map();
10988
+ const readMediaModels = async (region, options) => {
10989
+ const cached = mediaCaches.get(region);
10990
+ const now = Date.now();
10991
+ if (options?.refresh !== true && cached !== void 0 && now - cached.at < 3e5) return cached.models;
10992
+ const credential = await stacks[region].store.resolve();
10993
+ const models = await client.fetchMediaModels(credential, options?.signal);
10994
+ mediaCaches.set(region, {
10995
+ at: Date.now(),
10996
+ models
10997
+ });
10998
+ return models;
10999
+ };
11000
+ /**
10420
11001
  * Stamp each model's LIVE load level onto the runtime roster.
10421
11002
  *
10422
11003
  * Deliberately NOT part of {@link discoverModels}: that reader feeds the
@@ -10487,6 +11068,7 @@ function apply(ctx, config) {
10487
11068
  })();
10488
11069
  syncRegionRegistration(value);
10489
11070
  syncImageTool(value);
11071
+ syncVideoTool(value);
10490
11072
  syncCliBridge(value);
10491
11073
  refreshRegionUsability();
10492
11074
  };
@@ -10552,19 +11134,24 @@ function apply(ctx, config) {
10552
11134
  let imageTools;
10553
11135
  let disposeImageTool;
10554
11136
  const syncImageTool = (value) => {
10555
- const wanted = regionEnabled(value, "cn") && regionStateOf(value, "cn").imageToolEnabled === true;
11137
+ const wanted = imageToolRegionOf(value) !== void 0;
10556
11138
  if (wanted && disposeImageTool === void 0 && imageTools !== void 0) {
10557
11139
  disposeImageTool = registerWorkBuddyImageTool(imageTools, {
10558
11140
  resolveCredential: async () => {
10559
- if (!regionEnabled(current(), "cn")) return void 0;
10560
- try {
10561
- return await stacks.cn.store.resolve();
10562
- } catch {
10563
- return;
11141
+ const live = current();
11142
+ for (const region of REGION_KEYS) {
11143
+ if (!regionEnabled(live, region)) continue;
11144
+ if (regionStateOf(live, region).imageToolEnabled !== true) continue;
11145
+ try {
11146
+ return await stacks[region].store.resolve();
11147
+ } catch {
11148
+ continue;
11149
+ }
10564
11150
  }
10565
11151
  },
10566
11152
  listMediaModels: (credential, signal) => client.fetchMediaModels(credential, signal),
10567
11153
  generateImage: (credential, request, signal) => client.generateImage(credential, request, signal),
11154
+ editImage: (credential, request, signal) => client.editImage(credential, request, signal),
10568
11155
  logger: ctx.logger
10569
11156
  });
10570
11157
  return;
@@ -10584,6 +11171,48 @@ function apply(ctx, config) {
10584
11171
  imageTools = void 0;
10585
11172
  });
10586
11173
  });
11174
+ let videoTools;
11175
+ let disposeVideoTool;
11176
+ const syncVideoTool = (value) => {
11177
+ const wanted = videoToolRegionOf(value) !== void 0;
11178
+ if (wanted && disposeVideoTool === void 0 && videoTools !== void 0) {
11179
+ disposeVideoTool = registerWorkBuddyVideoTool(videoTools, {
11180
+ resolveCredential: async () => {
11181
+ const live = current();
11182
+ for (const region of REGION_KEYS) {
11183
+ if (!regionEnabled(live, region)) continue;
11184
+ if (regionStateOf(live, region).videoToolEnabled !== true) continue;
11185
+ try {
11186
+ return await stacks[region].store.resolve();
11187
+ } catch {
11188
+ continue;
11189
+ }
11190
+ }
11191
+ },
11192
+ listMediaModels: (credential, signal) => client.fetchMediaModels(credential, signal),
11193
+ submitVideo: (credential, request, signal) => client.submitVideo(credential, request, signal),
11194
+ pollVideoTask: (credential, taskId, signal) => client.pollVideoTask(credential, taskId, signal),
11195
+ preferredVideoModel: (credential, tag) => regionStateOf(current(), regionOf(credential.domain)).preferredVideoModels?.[tag],
11196
+ downloadVideo: (credential, url, signal) => client.downloadVideo(credential, url, signal),
11197
+ logger: ctx.logger
11198
+ });
11199
+ return;
11200
+ }
11201
+ if (!wanted && disposeVideoTool !== void 0) {
11202
+ disposeVideoTool();
11203
+ disposeVideoTool = void 0;
11204
+ }
11205
+ };
11206
+ ctx.inject(["tools"], (toolsCtx) => {
11207
+ videoTools = toolsCtx.tools;
11208
+ if (videoTools === void 0) return;
11209
+ syncVideoTool(current());
11210
+ toolsCtx.effect(() => () => {
11211
+ disposeVideoTool?.();
11212
+ disposeVideoTool = void 0;
11213
+ videoTools = void 0;
11214
+ });
11215
+ });
10587
11216
  let cliTools;
10588
11217
  let cliCommands;
10589
11218
  let cliDirectoryOf;
@@ -10732,6 +11361,7 @@ function apply(ctx, config) {
10732
11361
  * counts as new — the one property that keeps the marker honest.
10733
11362
  */
10734
11363
  rosterSeen: (region) => stacks[region].rosterSeen.readout(),
11364
+ mediaModels: readMediaModels,
10735
11365
  regionEnabled: (region) => regionEnabled(current(), region),
10736
11366
  /**
10737
11367
  * The region's committed sidebar-credit flag, read from the same config the
@@ -10746,6 +11376,15 @@ function apply(ctx, config) {
10746
11376
  * is NOT consent to let the model spend.
10747
11377
  */
10748
11378
  imageToolEnabled: (region) => regionStateOf(current(), region).imageToolEnabled === true,
11379
+ /** Whether the model may call the video tool here. Same opt-IN read as above. */
11380
+ videoToolEnabled: (region) => regionStateOf(current(), region).videoToolEnabled === true,
11381
+ /**
11382
+ * The video model pinned per tag in this region, for the card's per-mode
11383
+ * picker. Read from the committed value like the switches above: the picker
11384
+ * must render what the TOOL will actually use, not what the browser mirror
11385
+ * happens to hold.
11386
+ */
11387
+ preferredVideoModels: (region) => regionStateOf(current(), region).preferredVideoModels ?? {},
10749
11388
  /**
10750
11389
  * Whether the optional CodeBuddy CLI route is mounted (`codebuddy_ask` tool
10751
11390
  * + `/codebuddy` command).
@@ -11733,4 +12372,4 @@ function apply(ctx, config) {
11733
12372
  });
11734
12373
  }
11735
12374
  //#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 };
12375
+ 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, videoToolRegionOf, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };