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/CHANGELOG.md +46 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +247 -8
- package/lib/{host-heartbeat-C1tVU2IN.js → host-heartbeat-_EeM2YDF.js} +176 -22
- package/lib/index.d.ts +65 -5
- package/lib/index.js +207 -32
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as
|
|
1
|
+
import { $ as parseCreditMultiplier, A as isEncryptedCredentialError, At as 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
|
|
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
|
|
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 <
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
-
``,
|
|
5028
5115
|
"",
|
|
5029
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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
|
|
9954
|
-
* catalog
|
|
9955
|
-
* international
|
|
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
|
|
9962
|
-
*
|
|
9963
|
-
*
|
|
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
|
|
9969
|
-
*
|
|
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;
|
|
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 =
|
|
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
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
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.
|
|
5
|
+
"version": "5.10.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|