dsh-connect-workbuddy 4.14.1 → 5.0.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 +183 -0
- package/README.en.md +22 -4
- package/README.md +22 -4
- package/lib/bin.js +1 -1
- package/lib/client.js +463 -109
- package/lib/{host-heartbeat-D1WnqQY2.js → host-heartbeat-D313ENPV.js} +952 -53
- package/lib/index.d.ts +182 -1
- package/lib/index.js +434 -10
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as REFUSES_OFF_MODEL_IDS, A as workbuddyAccountId, At as withFileLock, B as isCredentialRejectedError, C as defaultDesktopAuthDirs, Ct as macosBundleExecutable, D as isVaultAccountId, Dt as workbuddyAppExecutableCandidates, E as isEncryptedCredentialError, Et as readAtRestKey, F as WorkBuddyUpstreamClient, G as parseCreditMultiplier, H as knownRegionOf, I as classifyUpstreamError, J as perturbChannelFingerprints, K as parseReasoning, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as busyDisplaySuffix, R as forceNeutralHead, S as defaultDesktopAuthCandidates, St as isWorkbuddyBundle, T as hasEncryptedCredentialFields, Tt as openEncryptedField, U as modelIdOfBody, V as isFirstMessageGateBody, W as openingEvidenceOfBody, X as regionOf, Y as prepareChatBody, Z as attachBusyLevels, _ as WORKBUDDY_AUTH_FILENAME, _t as deriveAtRestKeyId, a as readHostHeartbeat, at as withEffectiveOff, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as pickImageModel, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as NATIVE_MODALITY_BY_MODEL_ID, et as defaultOffModelIds, f as WorkBuddyCatalog, ft as imageDefaultFor, g as ENCRYPTED_CREDENTIAL_CODE, gt as deriveAtRestKey, h as fallbackModelsFor, ht as clearAtRestKeyCache, i as processStartTimeMs, it as recordRefusal, j as workbuddyOwnAuthPath, jt as writeFileAtomic, k as parseWorkBuddyAuth, kt as resolveDshHome, l as piAiRuntimeInfo, lt as parseQueueAnswer, m as deriveCatalog, mt as WORKBUDDY_APP_EXECUTABLE_ENV, n as clearHostHeartbeat, nt as offDefaultFor, o as workbuddyHostHeartbeatPath, ot as DEFAULT_IMAGE_SIZE, p as applyContextBudgets, pt as nativeModalityOf, q as parseUpstreamModel, r as isHeartbeatProcessAlive, rt as offRefused, s as writeHostHeartbeat, st as WorkBuddyImageError, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as lastRefusalFor, u as FALLBACK_WORKBUDDY_MODELS, ut as queueNoticeText, v as WORKBUDDY_AUTH_FILE_ENV, vt as fetchAtRestKeyPayload, w as defaultDesktopAuthPath, wt as macosNestedAppBundles, x as authFileName, xt as isEncryptedFieldWrapper, y as WorkBuddyCredentialStore, yt as findWorkbuddyAppExecutable, z as isConversationIdGateBody } from "./host-heartbeat-D313ENPV.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { createAssistantMessageEventStream, createProvider, isContextOverflow } from "@earendil-works/pi-ai";
|
|
7
8
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
8
9
|
import { ReasoningEffortId, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
9
10
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
10
12
|
import { createServer } from "node:http";
|
|
11
13
|
import { Readable } from "node:stream";
|
|
12
14
|
//#region src/credential-recovery.ts
|
|
@@ -296,7 +298,7 @@ function workBuddyModelInput(info) {
|
|
|
296
298
|
* `x0.00`, matching WorkBuddy's rendering of free models.
|
|
297
299
|
*/
|
|
298
300
|
function workBuddyDisplayName(info) {
|
|
299
|
-
return info.creditMultiplier === void 0 ? info.name : `${info.name} · x${info.creditMultiplier.toFixed(2)}`;
|
|
301
|
+
return `${info.creditMultiplier === void 0 ? info.name : `${info.name} · x${info.creditMultiplier.toFixed(2)}`}${busyDisplaySuffix(info.busyLevel)}`;
|
|
300
302
|
}
|
|
301
303
|
/**
|
|
302
304
|
* Map only levels advertised by WorkBuddy; undeclared DSH levels stay unavailable.
|
|
@@ -2277,6 +2279,7 @@ const KIND_STATUS = {
|
|
|
2277
2279
|
session_dead: 401,
|
|
2278
2280
|
policy_reject: 403,
|
|
2279
2281
|
channel_reject: 400,
|
|
2282
|
+
model_queue: 429,
|
|
2280
2283
|
not_found: 502,
|
|
2281
2284
|
server: 502,
|
|
2282
2285
|
client: 400
|
|
@@ -2960,6 +2963,43 @@ function clippedUpstreamText(text, limit) {
|
|
|
2960
2963
|
return `${cut.trimEnd()}…`;
|
|
2961
2964
|
}
|
|
2962
2965
|
/**
|
|
2966
|
+
* The channel-gate reply's evidence sentence: what the gateway actually read.
|
|
2967
|
+
*
|
|
2968
|
+
* Read from the HOST's body, not from the prepared one, because the prepared
|
|
2969
|
+
* body has the plugin's neutral head in front of every opening it recognises —
|
|
2970
|
+
* so the first line it could report is the plugin's own placeholder, which is
|
|
2971
|
+
* exactly the false lead issue #98's reporters were given. They were told the
|
|
2972
|
+
* gateway had refused `You are a helpful assistant.`, a line that returns 200
|
|
2973
|
+
* when sent alone.
|
|
2974
|
+
*
|
|
2975
|
+
* Each entry is labelled with its ROLE (`system` / `assistant`) because both are
|
|
2976
|
+
* scanned and the difference is what tells the reader where to look: an
|
|
2977
|
+
* `assistant` entry is a history message, i.e. the session is poisoned and every
|
|
2978
|
+
* later turn will fail the same way until that line is changed.
|
|
2979
|
+
*
|
|
2980
|
+
* Each entry also says whether the plugin's own neutralisation already covers
|
|
2981
|
+
* it. An entry marked "not in the plugin's list" is the actionable one: that
|
|
2982
|
+
* opening goes on the wire verbatim, so either the host wrote it or the
|
|
2983
|
+
* gateway's list grew.
|
|
2984
|
+
*
|
|
2985
|
+
* Capped at {@link CHANNEL_EVIDENCE_LIMIT} entries: a session can carry dozens
|
|
2986
|
+
* of system/assistant messages, and a wall of them would bury the one that
|
|
2987
|
+
* matters. The remainder is counted instead of listed.
|
|
2988
|
+
*/
|
|
2989
|
+
function channelEvidenceText(bodyJson) {
|
|
2990
|
+
const entries = openingEvidenceOfBody(bodyJson);
|
|
2991
|
+
if (entries.length === 0) return "(请求里没有可读的 system 或 assistant 消息开头。)";
|
|
2992
|
+
const shown = entries.slice(0, CHANNEL_EVIDENCE_LIMIT);
|
|
2993
|
+
const rendered = shown.map((entry) => {
|
|
2994
|
+
const label = entry.neutralized ? "(插件已加中性头)" : "(不在插件已知名单里,网关可能新增了词条)";
|
|
2995
|
+
return `「${entry.role}:${entry.opening}」${label}`;
|
|
2996
|
+
}).join("、");
|
|
2997
|
+
const hidden = entries.length - shown.length;
|
|
2998
|
+
return `请求里被网关扫描的消息开头:${rendered}${hidden > 0 ? `……另有 ${hidden} 条未列出。` : "。"}`;
|
|
2999
|
+
}
|
|
3000
|
+
/** How many message openings the channel-gate reply lists. */
|
|
3001
|
+
const CHANNEL_EVIDENCE_LIMIT = 4;
|
|
3002
|
+
/**
|
|
2963
3003
|
* The line prepended to an answer that came from a failover account.
|
|
2964
3004
|
*
|
|
2965
3005
|
* Block-quoted and bracketed so it reads as machinery rather than as the model's
|
|
@@ -3256,7 +3296,7 @@ function createWorkBuddyShim(options) {
|
|
|
3256
3296
|
* which another account may well survive.
|
|
3257
3297
|
*/
|
|
3258
3298
|
function isFailoverWorthy(kind) {
|
|
3259
|
-
return kind !== "client" && kind !== "model_busy" && kind !== "channel_reject";
|
|
3299
|
+
return kind !== "client" && kind !== "model_busy" && kind !== "channel_reject" && kind !== "model_queue";
|
|
3260
3300
|
}
|
|
3261
3301
|
async function chatCompletions(req, res) {
|
|
3262
3302
|
if (!isJsonContentType(req)) {
|
|
@@ -3284,6 +3324,56 @@ function createWorkBuddyShim(options) {
|
|
|
3284
3324
|
const controller = new AbortController();
|
|
3285
3325
|
res.on("close", () => controller.abort());
|
|
3286
3326
|
let result = await client.chatStream(credential, prepared, controller.signal);
|
|
3327
|
+
/**
|
|
3328
|
+
* The retry ladder that does not depend on knowing the fingerprint
|
|
3329
|
+
* (issue #98, both halves).
|
|
3330
|
+
*
|
|
3331
|
+
* The gateway refuses the request for data the plugin cannot see: a list of
|
|
3332
|
+
* refused `system`/`assistant` OPENINGS (the plugin holds a measured
|
|
3333
|
+
* SNAPSHOT, `UNAPPROVED_CHANNEL_OPENINGS`) and, since 2026-10-21, at least
|
|
3334
|
+
* one long substring INSIDE the prompt. Every account and every region
|
|
3335
|
+
* sends the same bytes, so without a rescue the only outcome is a dead end
|
|
3336
|
+
* the reader cannot get out of.
|
|
3337
|
+
*
|
|
3338
|
+
* Two rungs, both on the SAME credential (the gate reads the request, not
|
|
3339
|
+
* the account, which is why `channel_reject` is not failover-worthy):
|
|
3340
|
+
*
|
|
3341
|
+
* 1. `forceNeutralHead` — move every scanned opening behind the neutral
|
|
3342
|
+
* head. Measured: whatever the banned opening is, moving it off the
|
|
3343
|
+
* start of the content answers 200.
|
|
3344
|
+
* 2. `perturbChannelFingerprints` — insert zero-width spaces at the
|
|
3345
|
+
* sentence boundaries of the scanned messages, which breaks an
|
|
3346
|
+
* exact-substring fingerprint without changing a word the model reads.
|
|
3347
|
+
* This is the rung that rescues the inside-the-prompt fingerprints,
|
|
3348
|
+
* where rung 1 is a no-op.
|
|
3349
|
+
*
|
|
3350
|
+
* Both return `undefined` when they would change nothing; that is the
|
|
3351
|
+
* signal not to spend a request on a body that would be refused identically.
|
|
3352
|
+
*
|
|
3353
|
+
* Nothing has been written to the client at this point (`!ok` means the
|
|
3354
|
+
* upstream answered non-2xx before any stream started), so a retry cannot
|
|
3355
|
+
* splice two responses together. The first attempt's failure is NOT booked
|
|
3356
|
+
* as an account failure: it was the request that was refused, not the
|
|
3357
|
+
* credential.
|
|
3358
|
+
*/
|
|
3359
|
+
let retriedWithForcedHead = false;
|
|
3360
|
+
let retriedWithPerturbation = false;
|
|
3361
|
+
if (!result.ok && result.kind === "channel_reject" && !controller.signal.aborted) {
|
|
3362
|
+
const forced = forceNeutralHead(prepared);
|
|
3363
|
+
if (forced !== void 0) {
|
|
3364
|
+
logger?.warn("dsh-connect-workbuddy: the channel gate refused the body; retrying once with the neutral head forced in front of every opening");
|
|
3365
|
+
result = await client.chatStream(credential, forced, controller.signal);
|
|
3366
|
+
retriedWithForcedHead = true;
|
|
3367
|
+
}
|
|
3368
|
+
if (!result.ok && result.kind === "channel_reject" && !controller.signal.aborted) {
|
|
3369
|
+
const rescued = perturbChannelFingerprints(forced ?? prepared);
|
|
3370
|
+
if (rescued !== void 0) {
|
|
3371
|
+
logger?.warn("dsh-connect-workbuddy: the channel gate refused the body again; retrying once with an invisible perturbation of the scanned text");
|
|
3372
|
+
result = await client.chatStream(credential, rescued, controller.signal);
|
|
3373
|
+
retriedWithPerturbation = true;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3287
3377
|
const triedAccountIds = [];
|
|
3288
3378
|
/**
|
|
3289
3379
|
* The account the request STARTED on, and the reason it was abandoned.
|
|
@@ -3364,6 +3454,11 @@ function createWorkBuddyShim(options) {
|
|
|
3364
3454
|
const why = shortfall === void 0 ? "" : `:${shortfall}`;
|
|
3365
3455
|
const refusedReport = refusedTierReport(result, requestModelId);
|
|
3366
3456
|
if (refusedReport !== void 0) reportRefusedTier(refusedReport);
|
|
3457
|
+
const queue = result.kind === "model_queue" ? parseQueueAnswer(result.message) : void 0;
|
|
3458
|
+
if (queue !== void 0) {
|
|
3459
|
+
writeOpenAIError(res, 429, "model_queue", `${queueNoticeText(queue)}(上游 code ${queue.code})${note}`);
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3367
3462
|
if (result.kind === "policy_reject") {
|
|
3368
3463
|
const detail = result.detail;
|
|
3369
3464
|
const lead = detail?.displayMsg ?? clippedUpstreamText(result.message, 200);
|
|
@@ -3390,9 +3485,12 @@ function createWorkBuddyShim(options) {
|
|
|
3390
3485
|
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3391
3486
|
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
3392
3487
|
const stamp = meta.length === 0 ? "" : `(${meta.join(",")})`;
|
|
3393
|
-
const
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3488
|
+
const evidence = channelEvidenceText(raw);
|
|
3489
|
+
const retries = [];
|
|
3490
|
+
if (retriedWithForcedHead) retries.push("强制中性头");
|
|
3491
|
+
if (retriedWithPerturbation) retries.push("一次不可见扰动");
|
|
3492
|
+
const retryNote = retries.length === 0 ? "" : `,并已用${retries.join("与")}重试过`;
|
|
3493
|
+
const message = `workbuddy 上游失败:${failoverReasonText(result)}(http ${result.status})${note}${stamp}。${evidence}该请求被 WorkBuddy 网关的渠道校验拦截,上游把它描述成「不是官方客户端发出的」,但那是误导性文案:网关匹配的是每一条 system 与 assistant 消息的开头,与账号、积分、额度、网络都无关,换账号或换区域也不会改变结果。插件已对已知的第三方 agent 指纹做归一化` + retryNote + ";若上面标着「不在插件已知名单里」的开头来自宿主或其他插件,请修改该提示;新的指纹可反馈到 issue 里";
|
|
3396
3494
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
|
|
3397
3495
|
return;
|
|
3398
3496
|
}
|
|
@@ -3401,10 +3499,11 @@ function createWorkBuddyShim(options) {
|
|
|
3401
3499
|
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3402
3500
|
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
3403
3501
|
const stamp = meta.length === 0 ? "" : `(${meta.join(",")})`;
|
|
3502
|
+
const siblingGate = isConversationIdGateBody(result.message) ? "该请求被上游以「conversation id missing」拒绝:网关要求每个请求带上会话 id 头(X-Conversation-ID、X-Conversation-Request-ID、X-Conversation-Message-ID),插件每次都会带上这三个头;仍出现说明这次请求没有走插件的聊天通道,请升级插件或反馈 issue" : isFirstMessageGateBody(result.message) ? "该请求被上游以「first message is not system prompt」拒绝:网关要求首条消息是 system 消息;插件在请求带消息时会保证这一点,所以出现这条说明这次请求本身没有消息,或没有走插件的聊天通道,请升级插件或反馈 issue" : void 0;
|
|
3404
3503
|
const refusedTier = reasoningTierRefusal(result);
|
|
3405
3504
|
const receipt = refusedTier ?? detail?.tips ?? detail?.displayMsg ?? clippedUpstreamText(result.message, 400);
|
|
3406
3505
|
const advice = refusedTier !== void 0 ? "这不是账号或网络问题:该模型实测拒绝「off」推理档位,请到模型卡片里取消该行的「可关闭思考」勾选后重试" : result.kind === "client" ? "该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果,请按上面的提示调整后重试" : shortfall === void 0 ? "" : "建议换一个模型继续,或稍后等待该账号的额度恢复";
|
|
3407
|
-
const body = advice === "" ? receipt : `${receipt.replace(/[。.]$/u, "")}。${advice}`;
|
|
3506
|
+
const body = siblingGate !== void 0 ? siblingGate : advice === "" ? receipt : `${receipt.replace(/[。.]$/u, "")}。${advice}`;
|
|
3408
3507
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy 上游失败:${failoverReasonText(result)}(http ${result.status})${note}${why}${stamp}。${body}`, hostErrorCode(result.kind));
|
|
3409
3508
|
return;
|
|
3410
3509
|
}
|
|
@@ -3465,6 +3564,237 @@ function createWorkBuddyShim(options) {
|
|
|
3465
3564
|
};
|
|
3466
3565
|
}
|
|
3467
3566
|
//#endregion
|
|
3567
|
+
//#region src/image-tool.ts
|
|
3568
|
+
/**
|
|
3569
|
+
* `workbuddy_draw_image`:把 WorkBuddy 国内区的文生图能力暴露成一个 DSH agent 工具。
|
|
3570
|
+
*
|
|
3571
|
+
* 为什么手写 ToolDefinition 而不 import `@deepseek-ai/dsh-tools` 的 `defineTool`:
|
|
3572
|
+
* 宿主 `Tools.register` 只校验两件事——`output.render` 是函数、`output.schema` 是
|
|
3573
|
+
* 受支持的 JSON Schema;`schemaOf()` 把 `parameters` **原样**投影给模型。也就是说
|
|
3574
|
+
* raw JSON Schema 完全够用,而 `defineTool` 额外提供的只有「按 spec 编译 schema +
|
|
3575
|
+
* 代校验参数」这一层包装。手写换来的是:零新 peer 依赖、不需要动态 import 兜底、
|
|
3576
|
+
* 不会碰到 tests/host-peers.spec.ts 对 `lib/bin.js` 可达图的断言(那里禁止除
|
|
3577
|
+
* `node:` 外的任何裸说明符)。
|
|
3578
|
+
*
|
|
3579
|
+
* 代价是参数校验要自己做——见 {@link createImageTool} 的 execute。
|
|
3580
|
+
*
|
|
3581
|
+
* @module dsh-connect-workbuddy/image-tool
|
|
3582
|
+
*/
|
|
3583
|
+
/** 工具名。宿主禁止 `run_code`,这个名字与其它 connect 插件的命名习惯一致。 */
|
|
3584
|
+
const WORKBUDDY_IMAGE_TOOL_NAME = "workbuddy_draw_image";
|
|
3585
|
+
/** 工具单次执行的上限。实测一张图 17–39 秒,留足余量。 */
|
|
3586
|
+
const IMAGE_TOOL_TIMEOUT_MS = 18e4;
|
|
3587
|
+
/** 媒体模型目录的缓存时长:目录变化很慢,不必每张图都重新读一遍。 */
|
|
3588
|
+
const MEDIA_MODEL_TTL_MS = 3e5;
|
|
3589
|
+
/** 失败后的冷却:生图按张扣积分,连续重试会在几秒内烧掉几十积分。 */
|
|
3590
|
+
const FAILURE_COOLDOWN_MS = 3e4;
|
|
3591
|
+
function asRecord(value) {
|
|
3592
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
3593
|
+
}
|
|
3594
|
+
function asNonEmptyString(value) {
|
|
3595
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* 这些失败说明请求真的打到了上游、可能已经扣了积分,才值得进冷却。
|
|
3599
|
+
* 参数写错 / 未登录 / 目录里没有模型都还没花钱。
|
|
3600
|
+
*/
|
|
3601
|
+
/**
|
|
3602
|
+
* 这些失败码都发生在「请求已经发出去」之后——上游可能已经扣了积分或排了队,
|
|
3603
|
+
* 连续重试只会把它们再花一遍,所以进冷却。
|
|
3604
|
+
*/
|
|
3605
|
+
const BILLABLE_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
3606
|
+
"AUTH",
|
|
3607
|
+
"RATE_LIMIT",
|
|
3608
|
+
"NO_ROUTE",
|
|
3609
|
+
"UPSTREAM",
|
|
3610
|
+
"EMPTY_RESULT"
|
|
3611
|
+
]);
|
|
3612
|
+
/**
|
|
3613
|
+
* 是否该进冷却——由「这一次到底有没有把生图请求发出去」决定,而不是由异常
|
|
3614
|
+
* 的类型反推。
|
|
3615
|
+
*
|
|
3616
|
+
* 早先的写法是「不是 WorkBuddyImageError 就算花过钱」,方向正好错了两头:
|
|
3617
|
+
* 读目录(`fetchMediaModels` 的那次只读 GET)失败抛的是 upstream.ts 的
|
|
3618
|
+
* `envelopeError`/网络错误,一分钱没花却把工具冷却 30 秒;而参数写错这类真正
|
|
3619
|
+
* 免费的失败又因为带着 `INVALID_ARGS` 才被放行——判据和事实不在一根轴上。
|
|
3620
|
+
*
|
|
3621
|
+
* 现在由调用点显式置位:请求发出去了才置 `sent`,冷却只看它。
|
|
3622
|
+
*/
|
|
3623
|
+
function isBillableFailure(error, sent) {
|
|
3624
|
+
if (!sent) return false;
|
|
3625
|
+
if (error instanceof WorkBuddyImageError) return BILLABLE_FAILURE_CODES.has(error.code);
|
|
3626
|
+
return true;
|
|
3627
|
+
}
|
|
3628
|
+
/** 把 b64 图片落成一个临时文件,返回绝对路径。 */
|
|
3629
|
+
async function writeTempImage(b64, model) {
|
|
3630
|
+
const dir = join(tmpdir(), "workbuddy-image-gen");
|
|
3631
|
+
await mkdir(dir, { recursive: true });
|
|
3632
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3633
|
+
const safeModel = model.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
3634
|
+
const path = join(dir, `${safeModel}-${stamp}.png`);
|
|
3635
|
+
await writeFile(path, Buffer.from(b64, "base64"));
|
|
3636
|
+
return path;
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* 造出工具定义。
|
|
3640
|
+
*
|
|
3641
|
+
* 每次 `apply()` 只造一个实例,所以冷却状态与目录缓存都挂在闭包里,
|
|
3642
|
+
* 生命周期跟着插件走。
|
|
3643
|
+
*/
|
|
3644
|
+
function createImageTool(deps) {
|
|
3645
|
+
const regionLabel = deps.regionLabel ?? "国内区";
|
|
3646
|
+
let mediaCache;
|
|
3647
|
+
let cooldownUntil = 0;
|
|
3648
|
+
const listModels = async (credential, signal) => {
|
|
3649
|
+
const now = Date.now();
|
|
3650
|
+
if (mediaCache !== void 0 && now - mediaCache.at < MEDIA_MODEL_TTL_MS) return mediaCache.models;
|
|
3651
|
+
const models = await deps.listMediaModels(credential, signal);
|
|
3652
|
+
mediaCache = {
|
|
3653
|
+
at: now,
|
|
3654
|
+
models
|
|
3655
|
+
};
|
|
3656
|
+
return models;
|
|
3657
|
+
};
|
|
3658
|
+
return {
|
|
3659
|
+
name: WORKBUDDY_IMAGE_TOOL_NAME,
|
|
3660
|
+
description: "用当前账号的 WorkBuddy 生图能力画一张图(文生图)。只在国内区账号下可用;国际区没有生图模型。每张图会真实消耗 WorkBuddy 积分(实测约 5.71 积分/张),不要在没必要时调用,也不要为了试错连续调用。返回的是 COS 签名 URL,约 24 小时后失效;需要展示给用户时,用 markdown 图片语法引用该 URL。",
|
|
3661
|
+
parameters: {
|
|
3662
|
+
type: "object",
|
|
3663
|
+
properties: {
|
|
3664
|
+
prompt: {
|
|
3665
|
+
type: "string",
|
|
3666
|
+
description: "画面描述(中文或英文都可以)。写清楚主体、风格、构图、光线。"
|
|
3667
|
+
},
|
|
3668
|
+
size: {
|
|
3669
|
+
type: "string",
|
|
3670
|
+
description: `图片尺寸,形如 1024x1024。默认 ${DEFAULT_IMAGE_SIZE}。`
|
|
3671
|
+
},
|
|
3672
|
+
model: {
|
|
3673
|
+
type: "string",
|
|
3674
|
+
description: "生图模型 id,可选。不填就用产品配置里第一个带 text-to-image 标签的模型(国内区即 hunyuan-image-alpha)。填了但目录里没有会直接报错,不会盲发。"
|
|
3675
|
+
}
|
|
3676
|
+
},
|
|
3677
|
+
required: ["prompt"],
|
|
3678
|
+
additionalProperties: false
|
|
3679
|
+
},
|
|
3680
|
+
output: {
|
|
3681
|
+
schema: {
|
|
3682
|
+
type: "object",
|
|
3683
|
+
properties: {
|
|
3684
|
+
url: {
|
|
3685
|
+
type: "string",
|
|
3686
|
+
description: "图片地址(COS 签名 URL,或本地 file:// 地址)。"
|
|
3687
|
+
},
|
|
3688
|
+
model: {
|
|
3689
|
+
type: "string",
|
|
3690
|
+
description: "实际使用的生图模型 id。"
|
|
3691
|
+
},
|
|
3692
|
+
prompt: {
|
|
3693
|
+
type: "string",
|
|
3694
|
+
description: "实际提交的画面描述。"
|
|
3695
|
+
},
|
|
3696
|
+
hint: {
|
|
3697
|
+
type: "string",
|
|
3698
|
+
description: "给模型的后续提示(如何展示、链接时效)。"
|
|
3699
|
+
},
|
|
3700
|
+
credit: {
|
|
3701
|
+
type: "number",
|
|
3702
|
+
description: "本次实际消耗的 WorkBuddy 积分。"
|
|
3703
|
+
},
|
|
3704
|
+
path: {
|
|
3705
|
+
type: "string",
|
|
3706
|
+
description: "上游返回 base64 时,落盘的本地绝对路径。"
|
|
3707
|
+
}
|
|
3708
|
+
},
|
|
3709
|
+
required: [
|
|
3710
|
+
"url",
|
|
3711
|
+
"model",
|
|
3712
|
+
"prompt",
|
|
3713
|
+
"hint"
|
|
3714
|
+
],
|
|
3715
|
+
additionalProperties: false
|
|
3716
|
+
},
|
|
3717
|
+
render(_args, value) {
|
|
3718
|
+
const record = asRecord(value);
|
|
3719
|
+
const url = asNonEmptyString(record["url"]) ?? "";
|
|
3720
|
+
const model = asNonEmptyString(record["model"]) ?? "unknown";
|
|
3721
|
+
const credit = typeof record["credit"] === "number" ? record["credit"] : void 0;
|
|
3722
|
+
const path = asNonEmptyString(record["path"]);
|
|
3723
|
+
const hint = asNonEmptyString(record["hint"]) ?? "";
|
|
3724
|
+
const lines = [
|
|
3725
|
+
``,
|
|
3726
|
+
"",
|
|
3727
|
+
`模型:\`${model}\`${credit === void 0 ? "" : ` · 消耗 ${credit} 积分`}`
|
|
3728
|
+
];
|
|
3729
|
+
if (path !== void 0) lines.push(`本地文件:${path}`);
|
|
3730
|
+
if (hint !== "") lines.push("", hint);
|
|
3731
|
+
return [{
|
|
3732
|
+
type: "text",
|
|
3733
|
+
text: lines.join("\n")
|
|
3734
|
+
}];
|
|
3735
|
+
}
|
|
3736
|
+
},
|
|
3737
|
+
timeoutMs: IMAGE_TOOL_TIMEOUT_MS,
|
|
3738
|
+
async execute(args, exec) {
|
|
3739
|
+
const record = asRecord(args);
|
|
3740
|
+
const prompt = asNonEmptyString(record["prompt"]);
|
|
3741
|
+
if (prompt === void 0) throw new WorkBuddyImageError(`${WORKBUDDY_IMAGE_TOOL_NAME}: 参数 prompt 必填,且必须是非空字符串。`, "INVALID_ARGS");
|
|
3742
|
+
const size = asNonEmptyString(record["size"]) ?? "1024x1024";
|
|
3743
|
+
const requestedModel = asNonEmptyString(record["model"]);
|
|
3744
|
+
const now = Date.now();
|
|
3745
|
+
if (now < cooldownUntil) {
|
|
3746
|
+
const seconds = Math.ceil((cooldownUntil - now) / 1e3);
|
|
3747
|
+
throw new WorkBuddyImageError(`workbuddy image: 上一次生图刚失败过,请 ${seconds} 秒后再试(失败冷却是为了避免连续重试白白烧积分)。`, "COOLDOWN");
|
|
3748
|
+
}
|
|
3749
|
+
const credential = await deps.resolveCredential();
|
|
3750
|
+
if (credential === void 0) throw new WorkBuddyImageError(`workbuddy image: ${regionLabel}未登录 WorkBuddy。请先在插件设置里登录国内区账号,再调用生图工具。`, "NOT_LOGGED_IN");
|
|
3751
|
+
const signal = exec.signal === void 0 ? AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS) : AbortSignal.any([exec.signal, AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS)]);
|
|
3752
|
+
let sent = false;
|
|
3753
|
+
try {
|
|
3754
|
+
const models = await listModels(credential, signal);
|
|
3755
|
+
const model = pickImageModel(models, requestedModel);
|
|
3756
|
+
sent = true;
|
|
3757
|
+
const outcome = await deps.generateImage(credential, {
|
|
3758
|
+
model: model.id,
|
|
3759
|
+
prompt,
|
|
3760
|
+
size
|
|
3761
|
+
}, signal);
|
|
3762
|
+
const value = {
|
|
3763
|
+
model: model.id,
|
|
3764
|
+
prompt,
|
|
3765
|
+
hint: `这张图来自 WorkBuddy(模型 ${model.id}),每张约消耗 5.71 积分。COS 签名 URL 约 24 小时失效;要长期保留请及时下载。` + (outcome.credit === void 0 ? "" : `本次实际扣费 ${outcome.credit} 积分。`)
|
|
3766
|
+
};
|
|
3767
|
+
if (outcome.url !== void 0) value["url"] = outcome.url;
|
|
3768
|
+
else if (outcome.b64 !== void 0) {
|
|
3769
|
+
const path = await writeTempImage(outcome.b64, model.id);
|
|
3770
|
+
value["url"] = pathToFileURL(path).href;
|
|
3771
|
+
value["path"] = path;
|
|
3772
|
+
value["hint"] = `${value["hint"]} 上游返回的是内联 base64,已落盘到本地。`;
|
|
3773
|
+
} else throw new WorkBuddyImageError("workbuddy image: 上游既没有返回 url 也没有返回 b64_json。", "EMPTY_RESULT");
|
|
3774
|
+
if (outcome.credit !== void 0) value["credit"] = outcome.credit;
|
|
3775
|
+
return value;
|
|
3776
|
+
} catch (error) {
|
|
3777
|
+
if (isBillableFailure(error, sent)) cooldownUntil = Date.now() + FAILURE_COOLDOWN_MS;
|
|
3778
|
+
throw error;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* 把工具注册进宿主。返回 disposer;调用方负责在插件卸载时调用它。
|
|
3785
|
+
*
|
|
3786
|
+
* 注册本身是同步的;`tools` 服务由宿主提供,拿不到就直接返回 undefined
|
|
3787
|
+
* 并记一条警告——生图工具缺失不该让整个插件起不来。
|
|
3788
|
+
*/
|
|
3789
|
+
function registerWorkBuddyImageTool(tools, deps) {
|
|
3790
|
+
try {
|
|
3791
|
+
return tools.register(createImageTool(deps));
|
|
3792
|
+
} catch (error) {
|
|
3793
|
+
deps.logger.warn(`dsh-connect-workbuddy: 注册 ${WORKBUDDY_IMAGE_TOOL_NAME} 失败: ` + (error instanceof Error ? error.message : String(error)));
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
//#endregion
|
|
3468
3798
|
//#region src/probe.ts
|
|
3469
3799
|
/**
|
|
3470
3800
|
* How long to bench a MODEL that answered 14003 with no stated time.
|
|
@@ -5517,6 +5847,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
|
|
|
5517
5847
|
const enabled = deps.regionEnabled(region);
|
|
5518
5848
|
const showCreditsInMainUi = deps.showCreditsInMainUi?.(region) ?? true;
|
|
5519
5849
|
const creditPosition = deps.creditPosition?.(region) ?? "composer";
|
|
5850
|
+
const imageToolEnabled = deps.imageToolEnabled?.(region) ?? false;
|
|
5520
5851
|
const authStatus = await store.status();
|
|
5521
5852
|
const selectionExplicit = store.hasExplicitSelection();
|
|
5522
5853
|
if (authStatus.state !== "signed-out" && accounts.length === 0) return {
|
|
@@ -5524,7 +5855,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
|
|
|
5524
5855
|
accounts: [],
|
|
5525
5856
|
selectionExplicit,
|
|
5526
5857
|
enabled,
|
|
5527
|
-
showCreditsInMainUi
|
|
5858
|
+
showCreditsInMainUi,
|
|
5859
|
+
imageToolEnabled
|
|
5528
5860
|
};
|
|
5529
5861
|
let credential;
|
|
5530
5862
|
try {
|
|
@@ -5540,6 +5872,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
|
|
|
5540
5872
|
enabled,
|
|
5541
5873
|
showCreditsInMainUi,
|
|
5542
5874
|
creditPosition,
|
|
5875
|
+
imageToolEnabled,
|
|
5543
5876
|
...await store.selectionLost() ? { selectionLost: true } : {},
|
|
5544
5877
|
...webAccounts.length === 0 ? await searchedPaths(store) : {}
|
|
5545
5878
|
};
|
|
@@ -5556,6 +5889,7 @@ async function workBuddyWebStatus(deps, region, options = {}) {
|
|
|
5556
5889
|
enabled,
|
|
5557
5890
|
showCreditsInMainUi,
|
|
5558
5891
|
creditPosition,
|
|
5892
|
+
imageToolEnabled,
|
|
5559
5893
|
accounts: accounts.map(toWebAccount),
|
|
5560
5894
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))).map((model) => model.reasoning === void 0 || lastRefusalFor(model.id) === void 0 ? model : {
|
|
5561
5895
|
...model,
|
|
@@ -6259,6 +6593,28 @@ const regionStateConfig = z.object({
|
|
|
6259
6593
|
enabledModelIds: z.array(z.string()).default([]),
|
|
6260
6594
|
imageModelIds: z.array(z.string()).default([]),
|
|
6261
6595
|
/**
|
|
6596
|
+
* Whether the model may call the image-generation tool (opt-IN, default off).
|
|
6597
|
+
*
|
|
6598
|
+
* Per-REGION like the credit switches, and only meaningful on `cn`: the image
|
|
6599
|
+
* catalog has a `text-to-image` model on the CN gateway and none at all on the
|
|
6600
|
+
* international one (measured, see `docs/IMAGE_GEN.md`).
|
|
6601
|
+
*
|
|
6602
|
+
* Every other spending path in this plugin requires a human action: a chat
|
|
6603
|
+
* request is one the user sent, and the per-model "test" button is one the
|
|
6604
|
+
* user clicked. `workbuddy_draw_image` is the first capability where the
|
|
6605
|
+
* MODEL decides to spend — it calls the tool on its own, at about 5.71 credits
|
|
6606
|
+
* an image, and there is no per-call approval seam in the host to ask the user
|
|
6607
|
+
* first (`needsApproval` / `toolApproval` do not exist; verified against the
|
|
6608
|
+
* shipped `app.asar`).
|
|
6609
|
+
*
|
|
6610
|
+
* So it is opt-in, the same way the account pool's credit-based rotation is
|
|
6611
|
+
* opt-in (3.0.0's reasoning): a feature that can move money on its own must be
|
|
6612
|
+
* one the user switched on, not one that arrives with an upgrade. Off also
|
|
6613
|
+
* means the tool is not REGISTERED at all, so the model never sees it and
|
|
6614
|
+
* cannot attempt a call that would be refused.
|
|
6615
|
+
*/
|
|
6616
|
+
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."),
|
|
6617
|
+
/**
|
|
6262
6618
|
* Per-model override of whether thinking `off` is offered, keyed by model id
|
|
6263
6619
|
* (issue #34). An ABSENT key means "use the built-in rule" — the upstream's
|
|
6264
6620
|
* `canDisableThinking` declaration minus the ids known to refuse `off` (see
|
|
@@ -6575,6 +6931,31 @@ function apply(ctx, config) {
|
|
|
6575
6931
|
const keep = regionStateOf(current(), region).enabledModelIds ?? [];
|
|
6576
6932
|
return client.fetchModels(credential, signal, keep);
|
|
6577
6933
|
};
|
|
6934
|
+
/**
|
|
6935
|
+
* Stamp each model's LIVE load level onto the runtime roster.
|
|
6936
|
+
*
|
|
6937
|
+
* Deliberately NOT part of {@link discoverModels}: that reader feeds the
|
|
6938
|
+
* settings card's "Refresh from WorkBuddy" draft, which is SAVED into
|
|
6939
|
+
* `lastCatalog` — and a saved load level is a lie the moment the settings page
|
|
6940
|
+
* is closed (`src/busy.ts`). So the level is stamped only on the path that
|
|
6941
|
+
* builds the RUNTIME catalog, and it dies with the process.
|
|
6942
|
+
*
|
|
6943
|
+
* Fail-soft on purpose: a refresh that cannot read the load probe still
|
|
6944
|
+
* delivers the roster. Losing a badge is nothing; losing the model list
|
|
6945
|
+
* because a 5-second probe timed out would be the same defect this plugin
|
|
6946
|
+
* spends most of its comments avoiding.
|
|
6947
|
+
*/
|
|
6948
|
+
const withBusyLevels = async (region, models, signal) => {
|
|
6949
|
+
if (models.length === 0) return models;
|
|
6950
|
+
try {
|
|
6951
|
+
const credential = await stacks[region].store.resolve();
|
|
6952
|
+
const levels = await client.fetchModelBusy(credential, models.map((model) => model.id), signal);
|
|
6953
|
+
return levels.size === 0 ? models : attachBusyLevels(models, levels);
|
|
6954
|
+
} catch (error) {
|
|
6955
|
+
ctx.logger.warn("dsh-connect-workbuddy: model load probe unavailable; refreshing the roster without load levels", error);
|
|
6956
|
+
return models;
|
|
6957
|
+
}
|
|
6958
|
+
};
|
|
6578
6959
|
/** Push the current config into every region's store selection and catalog. */
|
|
6579
6960
|
const applySelection = (value) => {
|
|
6580
6961
|
const authFile = unwrapVolatile(value.authFile);
|
|
@@ -6585,6 +6966,7 @@ function apply(ctx, config) {
|
|
|
6585
6966
|
}
|
|
6586
6967
|
invalidateCatalog();
|
|
6587
6968
|
syncRegionRegistration(value);
|
|
6969
|
+
syncImageTool(value);
|
|
6588
6970
|
refreshRegionUsability();
|
|
6589
6971
|
};
|
|
6590
6972
|
/**
|
|
@@ -6646,6 +7028,41 @@ function apply(ctx, config) {
|
|
|
6646
7028
|
declared: false
|
|
6647
7029
|
})));
|
|
6648
7030
|
};
|
|
7031
|
+
let imageTools;
|
|
7032
|
+
let disposeImageTool;
|
|
7033
|
+
const syncImageTool = (value) => {
|
|
7034
|
+
const wanted = regionEnabled(value, "cn") && regionStateOf(value, "cn").imageToolEnabled === true;
|
|
7035
|
+
if (wanted && disposeImageTool === void 0 && imageTools !== void 0) {
|
|
7036
|
+
disposeImageTool = registerWorkBuddyImageTool(imageTools, {
|
|
7037
|
+
resolveCredential: async () => {
|
|
7038
|
+
if (!regionEnabled(current(), "cn")) return void 0;
|
|
7039
|
+
try {
|
|
7040
|
+
return await stacks.cn.store.resolve();
|
|
7041
|
+
} catch {
|
|
7042
|
+
return;
|
|
7043
|
+
}
|
|
7044
|
+
},
|
|
7045
|
+
listMediaModels: (credential, signal) => client.fetchMediaModels(credential, signal),
|
|
7046
|
+
generateImage: (credential, request, signal) => client.generateImage(credential, request, signal),
|
|
7047
|
+
logger: ctx.logger
|
|
7048
|
+
});
|
|
7049
|
+
return;
|
|
7050
|
+
}
|
|
7051
|
+
if (!wanted && disposeImageTool !== void 0) {
|
|
7052
|
+
disposeImageTool();
|
|
7053
|
+
disposeImageTool = void 0;
|
|
7054
|
+
}
|
|
7055
|
+
};
|
|
7056
|
+
ctx.inject(["tools"], (toolsCtx) => {
|
|
7057
|
+
imageTools = toolsCtx.tools;
|
|
7058
|
+
if (imageTools === void 0) return;
|
|
7059
|
+
syncImageTool(current());
|
|
7060
|
+
toolsCtx.effect(() => () => {
|
|
7061
|
+
disposeImageTool?.();
|
|
7062
|
+
disposeImageTool = void 0;
|
|
7063
|
+
imageTools = void 0;
|
|
7064
|
+
});
|
|
7065
|
+
});
|
|
6649
7066
|
ctx.inject(["webServer"], (webCtx) => registerWorkBuddyStatusRoute(webCtx, {
|
|
6650
7067
|
store: (region) => stacks[region].store,
|
|
6651
7068
|
client,
|
|
@@ -6684,6 +7101,12 @@ function apply(ctx, config) {
|
|
|
6684
7101
|
*/
|
|
6685
7102
|
showCreditsInMainUi: (region) => regionStateOf(current(), region).showCreditsInMainUi !== false,
|
|
6686
7103
|
/**
|
|
7104
|
+
* Whether the model may call the image tool here. Read from the committed
|
|
7105
|
+
* value like the flag above, and deliberately `=== true`: an absent setting
|
|
7106
|
+
* is NOT consent to let the model spend.
|
|
7107
|
+
*/
|
|
7108
|
+
imageToolEnabled: (region) => regionStateOf(current(), region).imageToolEnabled === true,
|
|
7109
|
+
/**
|
|
6687
7110
|
* WHERE the readout sits, resolved here rather than read from the browser
|
|
6688
7111
|
* settings mirror, for the same measured reason as the flag above: on the
|
|
6689
7112
|
* affected 0.1.7 deployments that mirror never picks up this plugin's
|
|
@@ -7554,8 +7977,9 @@ function apply(ctx, config) {
|
|
|
7554
7977
|
if (region === void 0) return [];
|
|
7555
7978
|
if (!regionEnabled(current(), region)) return [];
|
|
7556
7979
|
const discovered = await discoverModels(region, signal);
|
|
7980
|
+
const live = await withBusyLevels(region, discovered, signal);
|
|
7557
7981
|
const state = regionStateOf(current(), region);
|
|
7558
|
-
return withImageSelection(deriveCatalog(
|
|
7982
|
+
return withImageSelection(deriveCatalog(live, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])).map((model) => ({
|
|
7559
7983
|
id: model.id,
|
|
7560
7984
|
name: workBuddyDisplayName(model),
|
|
7561
7985
|
contextWindow: model.contextWindow,
|
|
@@ -7575,7 +7999,7 @@ function apply(ctx, config) {
|
|
|
7575
7999
|
try {
|
|
7576
8000
|
const credential = await stacks[region].store.resolve();
|
|
7577
8001
|
if (stopped) return;
|
|
7578
|
-
const models = await client.fetchModels(credential, void 0, regionStateOf(current(), region).enabledModelIds ?? []);
|
|
8002
|
+
const models = await withBusyLevels(region, await client.fetchModels(credential, void 0, regionStateOf(current(), region).enabledModelIds ?? []));
|
|
7579
8003
|
if (stopped) return;
|
|
7580
8004
|
const state = regionStateOf(current(), region);
|
|
7581
8005
|
stacks[region].catalog.set(withEffectiveOff(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])), new Set(savedOffModelIds(state, models))));
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "WorkBuddy Model Access",
|
|
4
|
+
"description": "Connects locally signed-in WorkBuddy models, CN and international alike, to DSH, reading each model's credit multiplier from the upstream catalog on every refresh, with per-model image input toggles, account switching backed by an auto-failing-over pool, manual daily check-in, and read-only credit overviews shown as compact pills on the card and in the composer."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
ADDED
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
|
+
"version": "5.0.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"./client": "./lib/client.js",
|
|
33
33
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
34
|
+
"./locale/*.json": "./locale/*.json",
|
|
34
35
|
"./package.json": "./package.json"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"files": [
|
|
40
41
|
"lib",
|
|
41
42
|
"icons",
|
|
43
|
+
"locale",
|
|
42
44
|
"docs/assets",
|
|
43
45
|
"screenshots.json",
|
|
44
46
|
"cordis.patch.yml",
|