dsh-connect-workbuddy 4.15.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.
@@ -1026,6 +1026,269 @@ function imageDefaultFor(info) {
1026
1026
  if (info.supportsImages === false) return false;
1027
1027
  return NATIVE_MODALITY_BY_MODEL_ID[info.id] === "multimodal";
1028
1028
  }
1029
+ /** Everything the app treats as a queue condition (`isQueueError`). */
1030
+ const WORKBUDDY_QUEUE_CODES = [
1031
+ 6020,
1032
+ 6021,
1033
+ 6022
1034
+ ];
1035
+ /** The app's default when the upstream sends no `retry_after` (`?? 5`). */
1036
+ const WORKBUDDY_QUEUE_DEFAULT_RETRY_SEC = 5;
1037
+ /** True when a business code is one of the app's three queue codes. */
1038
+ function isQueueCode(value) {
1039
+ return typeof value === "number" && WORKBUDDY_QUEUE_CODES.includes(value);
1040
+ }
1041
+ /** Read one queue condition out of a `data`-shaped object, or `undefined`. */
1042
+ function reportFrom(code, data) {
1043
+ if (!isQueueCode(code)) return void 0;
1044
+ const body = typeof data === "object" && data !== null ? data : {};
1045
+ const numberField = (key) => typeof body[key] === "number" && Number.isFinite(body[key]) ? body[key] : void 0;
1046
+ const position = numberField("queue_position");
1047
+ const size = numberField("queue_size");
1048
+ const retryAfter = numberField("retry_after");
1049
+ return {
1050
+ code,
1051
+ ...position === void 0 ? {} : { position },
1052
+ ...size === void 0 ? {} : { size },
1053
+ retryAfterSec: retryAfter !== void 0 && retryAfter > 0 ? retryAfter : WORKBUDDY_QUEUE_DEFAULT_RETRY_SEC
1054
+ };
1055
+ }
1056
+ /**
1057
+ * Read a queue condition out of one upstream body.
1058
+ *
1059
+ * Three shapes are accepted, because the gateway wraps the same fact three ways
1060
+ * depending on which channel answered:
1061
+ *
1062
+ * 1. the chat endpoint's own error envelope — `{"code":6020,"data":{…}}`;
1063
+ * 2. the app's agent-protocol envelope, where the condition rides as a JSON
1064
+ * STRING inside `_meta['codebuddy.ai/errorMessage']`
1065
+ * (`{"code":6020,"data":{…},"message":"…"}` — see the app's
1066
+ * `extractQueueInfoFromPromptResult`);
1067
+ * 3. a nested `data.code`, for a gateway that wraps one envelope in another.
1068
+ *
1069
+ * Anything else — a 14003 throttle, a quota refusal, a 500 — is NOT a queue and
1070
+ * returns `undefined`, so this can sit in the error path without changing it.
1071
+ */
1072
+ function parseQueueAnswer(text) {
1073
+ let parsed;
1074
+ try {
1075
+ parsed = JSON.parse(text);
1076
+ } catch {
1077
+ return;
1078
+ }
1079
+ const direct = readEnvelope$1(parsed);
1080
+ if (direct !== void 0) return direct;
1081
+ if (typeof parsed === "object" && parsed !== null) {
1082
+ const meta = parsed["_meta"];
1083
+ if (typeof meta === "object" && meta !== null) {
1084
+ const message = meta["codebuddy.ai/errorMessage"];
1085
+ if (typeof message === "string") try {
1086
+ return readEnvelope$1(JSON.parse(message));
1087
+ } catch {
1088
+ return;
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ /** `{code, data}` at the top level, or one `data` level down. */
1094
+ function readEnvelope$1(value) {
1095
+ if (typeof value !== "object" || value === null) return void 0;
1096
+ const envelope = value;
1097
+ const top = reportFrom(envelope["code"], envelope["data"]);
1098
+ if (top !== void 0) return top;
1099
+ const data = envelope["data"];
1100
+ if (typeof data === "object" && data !== null) {
1101
+ const inner = data;
1102
+ return reportFrom(inner["code"], inner["data"] ?? inner);
1103
+ }
1104
+ }
1105
+ /**
1106
+ * The sentence a queue condition is reported with — the app's own vocabulary
1107
+ * ("排队", "位"), and never the upstream's untranslated `message` key.
1108
+ *
1109
+ * `waiting` names both numbers because they answer different questions: the
1110
+ * POSITION is how long the wait is, the SIZE is whether it is moving at all.
1111
+ */
1112
+ function queueNoticeText(report) {
1113
+ if (report.code === 6020) return `模型排队中:你排在第 ${report.position === void 0 ? "未知" : `${report.position}`} 位${report.size === void 0 ? "" : `,队列共 ${report.size} 个请求`};约 ${report.retryAfterSec} 秒后自动重试`;
1114
+ if (report.code === 6021) return `模型队列已满${report.size === void 0 ? "" : `(共 ${report.size} 个请求排队)`},现在无法排队;请稍后重试或换一个模型`;
1115
+ return `你的排队任务已达上限${report.size === void 0 ? "" : `(共 ${report.size} 个请求排队)`},请等待已排队请求完成后再试`;
1116
+ }
1117
+ //#endregion
1118
+ //#region src/image-gen.ts
1119
+ /**
1120
+ * WorkBuddy 生图的纯逻辑层:媒体模型筛选、请求体构造、响应解析、失败描述。
1121
+ *
1122
+ * 这一层刻意不做任何网络 I/O、不引用宿主的任何包。`src/upstream.ts` 会被
1123
+ * `src/bin.ts` 静态可达(见 tests/host-peers.spec.ts 对 lib/bin.js 可达图的断言:
1124
+ * 除 `node:` 外不得出现任何裸说明符),所以任何被它引用的模块都必须保持零外部依赖。
1125
+ *
1126
+ * 上游契约照抄官方 CLI `codebuddy-lite-wb.mjs` 的 ImageService(实测 2026-10-21,
1127
+ * 国内区 `copilot.tencent.com` 与 `www.workbuddy.cn` 两个入口逐字段同答):
1128
+ *
1129
+ * POST {endpoint}/v2/images/generations
1130
+ * body = { model, prompt, size, ...按 model 前缀分支的字段 }
1131
+ * resp = { code, msg, request_id?, data: { created, data: [{ url } | { b64_json }],
1132
+ * usage: { credit } } }
1133
+ *
1134
+ * 前缀分支(`buildBaseRequestBody` 原文行为):
1135
+ * - `hunyuan-*`:额外只带 `n`(默认 1)/ `footnote` / `revise`
1136
+ * - `gemini-*` :什么都不加
1137
+ * - 其它 :`response_format: 'b64_json'` + 可选 `n` / `quality` / `style` / `background`
1138
+ *
1139
+ * 国内区产品配置里带 `text-to-image` 标签的只有 `hunyuan-image-alpha`,
1140
+ * 带 `image-to-image` 的只有 `hunyuan-image-alpha-edit`,因此实际走的就是
1141
+ * hunyuan 分支、响应里是 COS 签名 URL(24h 有效),单张约 5.71 credits。
1142
+ *
1143
+ * @module dsh-connect-workbuddy/image-gen
1144
+ */
1145
+ /** 产品配置里「文生图」模型的标签。 */
1146
+ const IMAGE_TAG_TEXT_TO_IMAGE = "text-to-image";
1147
+ /** 官方 CLI 的默认尺寸。 */
1148
+ const DEFAULT_IMAGE_SIZE = "1024x1024";
1149
+ /** 生图路由。 */
1150
+ const IMAGE_GENERATIONS_PATH = "/v2/images/generations";
1151
+ /** 生图链路的错误。带一个稳定的 `code`,避免调用方去匹配文案。 */
1152
+ var WorkBuddyImageError = class extends Error {
1153
+ code;
1154
+ constructor(message, code) {
1155
+ super(message);
1156
+ this.name = "WorkBuddyImageError";
1157
+ this.code = code;
1158
+ }
1159
+ };
1160
+ function asString(value) {
1161
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
1162
+ }
1163
+ function stringList(value) {
1164
+ if (!Array.isArray(value)) return [];
1165
+ const out = [];
1166
+ for (const item of value) {
1167
+ const text = asString(item);
1168
+ if (text !== void 0) out.push(text);
1169
+ }
1170
+ return out;
1171
+ }
1172
+ /**
1173
+ * 从产品配置的 `data.models` 里挑出带指定 tag 的媒体模型。
1174
+ *
1175
+ * 去重按 `id`:上游目录里同一个 id 只应出现一次,但目录是外部数据,
1176
+ * 重复条目不该让卡片或工具参数里出现两个同名选项。
1177
+ */
1178
+ function selectMediaModels(rawModels, tag = IMAGE_TAG_TEXT_TO_IMAGE) {
1179
+ if (!Array.isArray(rawModels)) return [];
1180
+ const out = [];
1181
+ const seen = /* @__PURE__ */ new Set();
1182
+ for (const entry of rawModels) {
1183
+ if (typeof entry !== "object" || entry === null) continue;
1184
+ const record = entry;
1185
+ const id = asString(record["id"]);
1186
+ if (id === void 0 || seen.has(id)) continue;
1187
+ const tags = stringList(record["tags"]);
1188
+ if (!tags.includes(tag)) continue;
1189
+ seen.add(id);
1190
+ out.push({
1191
+ id,
1192
+ name: asString(record["name"]) ?? id,
1193
+ tags
1194
+ });
1195
+ }
1196
+ return out;
1197
+ }
1198
+ /**
1199
+ * 选出生图模型:优先用调用方点名的那个,否则用目录里的第一个。
1200
+ *
1201
+ * 点名的模型必须在目录里——不在目录里就直接报错,而不是把它发上去让上游
1202
+ * 回一个「route config not found」。这样报错信息能指出真正的问题(模型名写错
1203
+ * 或该区没有这个模型),而不是一句上游 404。
1204
+ */
1205
+ function pickImageModel(models, requested) {
1206
+ if (requested !== void 0 && requested !== "") {
1207
+ const found = models.find((model) => model.id === requested);
1208
+ if (found === void 0) {
1209
+ const known = models.map((model) => model.id).join(", ");
1210
+ throw new WorkBuddyImageError(`workbuddy image: 目录里没有生图模型「${requested}」。可用:${known === "" ? "(空)" : known}`, "NO_IMAGE_MODEL");
1211
+ }
1212
+ return found;
1213
+ }
1214
+ const first = models[0];
1215
+ if (first === void 0) throw new WorkBuddyImageError("workbuddy image: 该区账号的产品配置里没有任何带 text-to-image 标签的模型。国内区应有 hunyuan-image-alpha;国际区当前没有生图模型。", "NO_IMAGE_MODEL");
1216
+ return first;
1217
+ }
1218
+ /** 按官方 CLI 的 `buildBaseRequestBody` 分支规则拼请求体。 */
1219
+ function buildImageBody(request) {
1220
+ const model = request.model;
1221
+ const body = {
1222
+ model,
1223
+ prompt: request.prompt,
1224
+ size: request.size ?? "1024x1024"
1225
+ };
1226
+ if (model.startsWith("hunyuan-")) {
1227
+ body["n"] = request.n ?? 1;
1228
+ return body;
1229
+ }
1230
+ if (model.startsWith("gemini-")) return body;
1231
+ body["response_format"] = "b64_json";
1232
+ if (request.n !== void 0) body["n"] = request.n;
1233
+ return body;
1234
+ }
1235
+ /**
1236
+ * 解析生图响应。
1237
+ *
1238
+ * 信封是 `{code, msg, request_id, data}`;`code !== 0` 是业务失败(与 HTTP 状态
1239
+ * 无关,上游可能 HTTP 200 但 code 非 0)。`data.data` 是数组,元素要么带 `url`
1240
+ * 要么带 `b64_json`;`data.usage.credit` 是本次实际扣的积分。
1241
+ */
1242
+ function parseImageResponse(payload) {
1243
+ if (typeof payload !== "object" || payload === null) throw new WorkBuddyImageError("workbuddy image: 上游返回的不是 JSON 对象", "UPSTREAM");
1244
+ const envelope = payload;
1245
+ const code = typeof envelope["code"] === "number" ? envelope["code"] : void 0;
1246
+ if (code !== void 0 && code !== 0) {
1247
+ const message = asString(envelope["msg"]) ?? "unknown error";
1248
+ const requestId = asString(envelope["request_id"]);
1249
+ throw new WorkBuddyImageError(`workbuddy image: 上游拒绝(code ${code}): ${message}` + (requestId === void 0 ? "" : `(request_id ${requestId})`), "UPSTREAM");
1250
+ }
1251
+ const data = envelope["data"];
1252
+ if (typeof data !== "object" || data === null) throw new WorkBuddyImageError("workbuddy image: 响应缺少 data 字段", "UPSTREAM");
1253
+ const record = data;
1254
+ const items = Array.isArray(record["data"]) ? record["data"] : [];
1255
+ const first = items.length > 0 ? items[0] : void 0;
1256
+ let url;
1257
+ let b64;
1258
+ if (typeof first === "object" && first !== null) {
1259
+ const item = first;
1260
+ url = asString(item["url"]);
1261
+ b64 = asString(item["b64_json"]);
1262
+ }
1263
+ if (url === void 0 && b64 === void 0) throw new WorkBuddyImageError("workbuddy image: 上游返回成功但 data.data 里既没有 url 也没有 b64_json", "EMPTY_RESULT");
1264
+ const usage = typeof record["usage"] === "object" && record["usage"] !== null ? record["usage"] : {};
1265
+ const credit = typeof usage["credit"] === "number" && Number.isFinite(usage["credit"]) ? usage["credit"] : void 0;
1266
+ const created = typeof record["created"] === "number" ? record["created"] : void 0;
1267
+ return {
1268
+ ...url === void 0 ? {} : { url },
1269
+ ...b64 === void 0 ? {} : { b64 },
1270
+ ...created === void 0 ? {} : { created },
1271
+ ...credit === void 0 ? {} : { credit }
1272
+ };
1273
+ }
1274
+ /** 把一次非 2xx 的响应翻成带 `code` 的错误,好让上层决定怎么提示。 */
1275
+ function describeImageFailure(status, bodyText) {
1276
+ const text = bodyText.trim();
1277
+ let detail = text.slice(0, 240);
1278
+ if (text.startsWith("{")) try {
1279
+ const parsed = JSON.parse(text);
1280
+ if (typeof parsed === "object" && parsed !== null) {
1281
+ const record = parsed;
1282
+ const message = asString(record["msg"]);
1283
+ const code = typeof record["code"] === "number" ? record["code"] : void 0;
1284
+ if (message !== void 0) detail = code === void 0 ? message : `code ${code}: ${message}`;
1285
+ }
1286
+ } catch {}
1287
+ if (status === 401 || status === 403) return new WorkBuddyImageError(`workbuddy image: 凭据被上游拒绝(HTTP ${status}),请在插件设置里重新登录 WorkBuddy。${detail === "" ? "" : ` ${detail}`}`, "AUTH");
1288
+ if (status === 429) return new WorkBuddyImageError(`workbuddy image: 触发上游限流(HTTP 429),稍后再试。${detail === "" ? "" : ` ${detail}`}`, "RATE_LIMIT");
1289
+ if (status === 404) return new WorkBuddyImageError(`workbuddy image: 上游没有这条生图路由(HTTP 404)。全球区没有生图模型,请用国内区账号。${detail === "" ? "" : ` ${detail}`}`, "NO_ROUTE");
1290
+ return new WorkBuddyImageError(`workbuddy image: 上游失败(HTTP ${status})${detail === "" ? "" : `: ${detail}`}`, "UPSTREAM");
1291
+ }
1029
1292
  //#endregion
1030
1293
  //#region src/off-thinking.ts
1031
1294
  /**
@@ -1353,6 +1616,126 @@ function attachPromos(models, promos) {
1353
1616
  });
1354
1617
  }
1355
1618
  //#endregion
1619
+ //#region src/busy.ts
1620
+ /** The upstream's enum, in the order its own bundle declares it. */
1621
+ const WORKBUDDY_BUSY_LEVELS = [
1622
+ "idle",
1623
+ "normal",
1624
+ "busy",
1625
+ "congested",
1626
+ "unavailable",
1627
+ "unknown"
1628
+ ];
1629
+ /** Coerce one `busy_level` value; everything the enum does not name is `unknown`. */
1630
+ function normalizeBusyLevel(value) {
1631
+ return typeof value === "string" && WORKBUDDY_BUSY_LEVELS.includes(value) ? value : "unknown";
1632
+ }
1633
+ /**
1634
+ * Read one probe answer into a model-id → level map.
1635
+ *
1636
+ * Tolerant by design: an entry that is not an object, or that names no model, is
1637
+ * skipped; a level the enum does not know becomes `unknown` rather than being
1638
+ * dropped, because "the upstream answered about this model but said something
1639
+ * we do not understand" is still a fact about that model.
1640
+ */
1641
+ function parseBusyLevels(entries) {
1642
+ const levels = /* @__PURE__ */ new Map();
1643
+ for (const entry of Array.isArray(entries) ? entries : []) {
1644
+ if (typeof entry !== "object" || entry === null) continue;
1645
+ const raw = entry;
1646
+ const id = typeof raw["model"] === "string" ? raw["model"] : "";
1647
+ if (id === "") continue;
1648
+ levels.set(id, normalizeBusyLevel(raw["busy_level"]));
1649
+ }
1650
+ return levels;
1651
+ }
1652
+ /**
1653
+ * Split ids into probe-sized batches, de-duplicated and order-preserving.
1654
+ *
1655
+ * De-duplication matters: the roster the plugin sends is the `cli` agent's
1656
+ * roster PLUS the user's kept ids, so the same id can legitimately appear twice,
1657
+ * and the upstream counts what it is asked for.
1658
+ */
1659
+ function batchModelIds(ids, size = 50) {
1660
+ const unique = [...new Set(ids.filter((id) => id !== ""))];
1661
+ const batches = [];
1662
+ for (let index = 0; index < unique.length; index += size) batches.push(unique.slice(index, index + size));
1663
+ return batches;
1664
+ }
1665
+ /** The app's own Chinese wording, per level. `normal` and `unknown` have none. */
1666
+ const BUSY_LABELS_ZH = {
1667
+ idle: "空闲",
1668
+ busy: "轻微拥堵",
1669
+ congested: "极度拥堵",
1670
+ unavailable: "不可用"
1671
+ };
1672
+ /** The app's own English wording, per level. */
1673
+ const BUSY_LABELS_EN = {
1674
+ idle: "Idle",
1675
+ busy: "Slightly congested",
1676
+ congested: "Extremely congested",
1677
+ unavailable: "Unavailable"
1678
+ };
1679
+ /**
1680
+ * The word this level is drawn with, or `undefined` when it is not worth drawing.
1681
+ *
1682
+ * `normal` returns `undefined` on purpose: the app's selector shows a busy line
1683
+ * only for a level that differs from the healthy default, and a badge that reads
1684
+ * "正常" on fifteen rows out of twenty is noise, not information. `unknown`
1685
+ * returns `undefined` for the same reason plus one more — the plugin must not
1686
+ * invent a claim the upstream never made.
1687
+ */
1688
+ function busyLabel(level, locale = "zh") {
1689
+ return (locale === "en" ? BUSY_LABELS_EN : BUSY_LABELS_ZH)[level];
1690
+ }
1691
+ /**
1692
+ * Suffix appended to a model's DSH-facing display name, prefixed with a middle
1693
+ * dot, or `''` when the level is not worth drawing.
1694
+ *
1695
+ * This is the "badge to the RIGHT of the model name" the DSH model selector
1696
+ * already does for its own variants (`deepseek-v4.1-flash · 1017`): DSH renders
1697
+ * `name` verbatim and offers no separate badge slot to a provider, so the only
1698
+ * way to put load beside the name — instead of in a second line, or inside the
1699
+ * name — is to extend the name. The name stays display-only: every DSH-side join
1700
+ * keys on the model ID, and `workBuddyDisplayName` is the single place that
1701
+ * builds it.
1702
+ *
1703
+ * ONLY A LEVEL THAT ASKS FOR A DECISION IS DRAWN — `busy`, `congested`,
1704
+ * `unavailable`. `idle` deliberately gets no suffix here even though it has a
1705
+ * word: on the measured CN roster seven of twenty models are idle, and a picker
1706
+ * whose every row carries a badge reads as decoration rather than as a warning.
1707
+ * That is also how the app's own selector behaves in its compact mode, where only
1708
+ * `congested` is drawn at all. The card's detail panel is where the full scale
1709
+ * belongs.
1710
+ */
1711
+ function busyDisplaySuffix(level, locale = "zh") {
1712
+ if (level === void 0 || !DRAWN_IN_PICKER.has(level)) return "";
1713
+ const label = busyLabel(level, locale);
1714
+ return label === void 0 ? "" : ` · ${label}`;
1715
+ }
1716
+ /** The levels that change a model-picking decision; see {@link busyDisplaySuffix}. */
1717
+ const DRAWN_IN_PICKER = /* @__PURE__ */ new Set([
1718
+ "busy",
1719
+ "congested",
1720
+ "unavailable"
1721
+ ]);
1722
+ /**
1723
+ * Attach each model's level to its catalog entry.
1724
+ *
1725
+ * A model the probe did not answer for keeps whatever it had — which, for a
1726
+ * freshly discovered roster, is nothing. Models are copied, never mutated: the
1727
+ * catalog entry is shared with the settings card's saved snapshot.
1728
+ */
1729
+ function attachBusyLevels(models, levels) {
1730
+ return models.map((model) => {
1731
+ const level = levels.get(model.id);
1732
+ return level === void 0 || level === model.busyLevel ? model : {
1733
+ ...model,
1734
+ busyLevel: level
1735
+ };
1736
+ });
1737
+ }
1738
+ //#endregion
1356
1739
  //#region src/upstream.ts
1357
1740
  /**
1358
1741
  * WorkBuddy (CodeBuddy / copilot.tencent.com) upstream client: chat streaming,
@@ -1429,6 +1812,22 @@ const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
1429
1812
  */
1430
1813
  const GLOBAL_CONFIG_PATH = "/v3/config";
1431
1814
  /**
1815
+ * The app's model-load probe (see `src/busy.ts`), read-only and free.
1816
+ *
1817
+ * Not part of any catalog document: this endpoint answers ONE fact per model id
1818
+ * — how busy it is right now — and answers it for up to 50 ids at a time. It is
1819
+ * host- and identity-insensitive on the CN side (measured 2026-10-21 on all
1820
+ * three CN hosts, under both the CLI and the app user agent: byte-identical),
1821
+ * so it is asked as the chat channel's own identity.
1822
+ */
1823
+ const MODEL_BUSY_PATH = "/v2/chat/busy";
1824
+ /**
1825
+ * The app's own ceiling for one probe (`timeout: 5e3` in its
1826
+ * `useModelBusyLevels`). Short on purpose: a slow load probe is worth less than
1827
+ * the refresh it is attached to, and the catalog must not wait on it.
1828
+ */
1829
+ const MODEL_BUSY_TIMEOUT_MS = 5e3;
1830
+ /**
1432
1831
  * The chat channel's user agent — the identity the plugin's adapter speaks as.
1433
1832
  *
1434
1833
  * Chat is a different contract from configuration: the conversation-identity
@@ -1492,6 +1891,15 @@ const ERROR_BODY_LIMIT = 4096;
1492
1891
  */
1493
1892
  const PROBE_TIMEOUT_MS = 1e4;
1494
1893
  /**
1894
+ * 生图请求的超时。
1895
+ *
1896
+ * 实测一张 1024x1024 的图在官方网关要 17–39 秒(`hunyuan-image-alpha`,
1897
+ * 2026-10-21,两个国内入口各测一次),比任何 JSON 接口都慢一个数量级,
1898
+ * 因此不能复用 {@link JSON_TIMEOUT_MS}。三分钟足够覆盖一次正常的排队+推理,
1899
+ * 又不至于让一个卡死的连接占住工具槽位太久。
1900
+ */
1901
+ const IMAGE_TIMEOUT_MS = 18e4;
1902
+ /**
1495
1903
  * Insufficient-credit markers.
1496
1904
  *
1497
1905
  * ENGLISH IS MATCHED CASE-INSENSITIVELY (see {@link classifyUpstreamError}) so
@@ -1708,6 +2116,7 @@ function classifyUpstreamError(status, body) {
1708
2116
  for (const marker of POLICY_REJECT_MARKERS) if (body.includes(marker)) return "policy_reject";
1709
2117
  if (isChannelRejectBody(body)) return "channel_reject";
1710
2118
  if (isModelBusyBody(body)) return "model_busy";
2119
+ if (parseQueueAnswer(body) !== void 0) return "model_queue";
1711
2120
  if (status === 429) return "soft_rate";
1712
2121
  if (status === 404) return "not_found";
1713
2122
  if (status >= 500) return "server";
@@ -1935,6 +2344,31 @@ function billingHeaders(credential) {
1935
2344
  return headers;
1936
2345
  }
1937
2346
  /**
2347
+ * 生图请求头。
2348
+ *
2349
+ * 实测口径(2026-10-21,国内区):`Authorization: Bearer` + `X-Product: SaaS` +
2350
+ * 用户/企业/域三连 + 会话身份三连 + `PRODUCT_UA`,在 `copilot.tencent.com` 与
2351
+ * `www.workbuddy.cn` 两个入口都返回 200。刻意**不带** `Origin` / `Referer`:
2352
+ * 探针当时没带,带了只是多一个可能被网关拒掉的变量,没有收益。
2353
+ *
2354
+ * 复用 {@link conversationHeaders}:官方 CLI 的 ImageService 也是每次现生成
2355
+ * 一组会话 UUID,且两个区走同一条代码路径(见该函数的注释)。
2356
+ */
2357
+ function imageHeaders(credential) {
2358
+ return {
2359
+ "Authorization": `Bearer ${credential.accessToken}`,
2360
+ "Accept": "application/json, text/plain, */*",
2361
+ "Content-Type": "application/json",
2362
+ "User-Agent": PRODUCT_UA,
2363
+ "X-Product": "SaaS",
2364
+ "X-Requested-With": "XMLHttpRequest",
2365
+ ...credential.uid === "" ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
2366
+ ...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
2367
+ ...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
2368
+ ...conversationHeaders()
2369
+ };
2370
+ }
2371
+ /**
1938
2372
  * Stand-in system message for a request that reached the wire carrying none.
1939
2373
  *
1940
2374
  * Not a stylistic default: both WorkBuddy gateways want the conversation to
@@ -3034,6 +3468,101 @@ var WorkBuddyUpstreamClient = class {
3034
3468
  return await readCatalog(MODELS_CATALOG_PATH);
3035
3469
  }
3036
3470
  }
3471
+ /**
3472
+ * Read how busy each model is right now, from the upstream's own probe.
3473
+ *
3474
+ * Batched at the upstream's ceiling (`WORKBUDDY_BUSY_BATCH_SIZE`), because the
3475
+ * probe takes a list of ids and answers only about the ids it was given.
3476
+ *
3477
+ * Throws on anything that is not a clean `code: 0`, like every other reader in
3478
+ * this class — the CALLER decides that a failed load probe is survivable and
3479
+ * catches it. Nothing here is persisted: the level belongs to the refresh that
3480
+ * asked for it.
3481
+ */
3482
+ async fetchModelBusy(credential, modelIds, signal) {
3483
+ const levels = /* @__PURE__ */ new Map();
3484
+ for (const batch of batchModelIds(modelIds)) {
3485
+ const response = await fetch(`${chatBase(credential)}${MODEL_BUSY_PATH}`, {
3486
+ method: "POST",
3487
+ headers: {
3488
+ ...commonHeaders(credential),
3489
+ "Authorization": `Bearer ${credential.accessToken}`,
3490
+ "Content-Type": "application/json"
3491
+ },
3492
+ body: JSON.stringify({ models: batch }),
3493
+ signal: signal ?? AbortSignal.timeout(MODEL_BUSY_TIMEOUT_MS)
3494
+ });
3495
+ const envelope = await readEnvelope(response);
3496
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
3497
+ for (const [id, level] of parseBusyLevels(envelope.data)) levels.set(id, level);
3498
+ }
3499
+ return levels;
3500
+ }
3501
+ /**
3502
+ * 读该账号所属区的产品配置,挑出带 `text-to-image` 标签的媒体模型。
3503
+ *
3504
+ * 走的是和 {@link fetchModels} 同一条 `/v3/config`(国内区与国际区都答),
3505
+ * 区别只在这里保留 `data.models` 的媒体条目、而不是投影成 CLI 聊天目录。
3506
+ * 国内区实测 62 条模型里只有 `hunyuan-image-alpha` 带 `text-to-image`;
3507
+ * 国际区一条都没有 —— 所以本方法对国际区返回空数组,而不是编一个模型出来。
3508
+ *
3509
+ * 这是一次 EXTRA 读取,不是「复用目录那一次」:`fetchModels` 的结果被投影成
3510
+ * 聊天目录就丢掉了媒体标签,所以这里必须再读一遍同一份文档。它是只读 GET、
3511
+ * 不花积分,且工具侧有 5 分钟缓存,但「零新增请求」的说法不成立,别那么写。
3512
+ * 国内区与 {@link fetchModels} 一样走 `/v3` → `/v2` 降级:上游一旦不答
3513
+ * `/v3`,聊天目录会降级而这里不能变成一个原始异常。
3514
+ */
3515
+ async fetchMediaModels(credential, signal) {
3516
+ const readMedia = async (path) => {
3517
+ const response = await fetch(`${chatBase(credential)}${path}`, {
3518
+ headers: {
3519
+ "Authorization": `Bearer ${credential.accessToken}`,
3520
+ "Accept": "application/json",
3521
+ "Origin": originReferer(credential),
3522
+ "Referer": `${originReferer(credential)}/`,
3523
+ "User-Agent": PRODUCT_UA
3524
+ },
3525
+ signal: signal ?? AbortSignal.timeout(JSON_TIMEOUT_MS)
3526
+ });
3527
+ const envelope = await readEnvelope(response);
3528
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
3529
+ return selectMediaModels((typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {})["models"], IMAGE_TAG_TEXT_TO_IMAGE);
3530
+ };
3531
+ try {
3532
+ return await readMedia(GLOBAL_CONFIG_PATH);
3533
+ } catch (error) {
3534
+ if (regionOf(credential.domain) === "global") throw error;
3535
+ this.onFallback?.(`CN media catalog ${GLOBAL_CONFIG_PATH} failed; falling back to ${MODELS_CATALOG_PATH}: ` + (error instanceof Error ? error.message : String(error)));
3536
+ return await readMedia(MODELS_CATALOG_PATH);
3537
+ }
3538
+ }
3539
+ /**
3540
+ * 发一次文生图请求。
3541
+ *
3542
+ * 端点、请求体、响应解析全部照抄官方 CLI 的 ImageService(见 `image-gen.ts`
3543
+ * 的模块注释)。实测单张 5.71 credits,产物是 24 小时有效的 COS 签名 URL。
3544
+ *
3545
+ * 失败分两层:HTTP 非 2xx 交给 {@link describeImageFailure} 翻成带 code 的
3546
+ * 错误(401/403 提示重新登录、404 提示该区没有生图路由、429 提示限流);
3547
+ * HTTP 200 但信封 `code !== 0` 由 {@link parseImageResponse} 处理。
3548
+ */
3549
+ async generateImage(credential, request, signal) {
3550
+ const response = await fetch(`${chatBase(credential)}${IMAGE_GENERATIONS_PATH}`, {
3551
+ method: "POST",
3552
+ headers: imageHeaders(credential),
3553
+ body: JSON.stringify(buildImageBody(request)),
3554
+ signal: signal ?? AbortSignal.timeout(IMAGE_TIMEOUT_MS)
3555
+ });
3556
+ const text = await response.text();
3557
+ if (!response.ok) throw describeImageFailure(response.status, text);
3558
+ let payload;
3559
+ try {
3560
+ payload = JSON.parse(text);
3561
+ } catch {
3562
+ throw new WorkBuddyImageError(`workbuddy image: 上游返回的不是 JSON(HTTP ${response.status}): ${text.slice(0, 200)}`, "UPSTREAM");
3563
+ }
3564
+ return parseImageResponse(payload);
3565
+ }
3037
3566
  /** Query today's check-in status without changing account state. */
3038
3567
  async fetchCheckinStatus(credential) {
3039
3568
  const response = await fetch(`${billingBase(credential)}/v2/billing/meter/checkin-activity-status`, {
@@ -5233,7 +5762,7 @@ function generationOf(version) {
5233
5762
  //#endregion
5234
5763
  //#region src/version.ts
5235
5764
  /** The npm package version this build was produced from. */
5236
- const WORKBUDDY_CONNECT_VERSION = "4.15.1";
5765
+ const WORKBUDDY_CONNECT_VERSION = "5.0.0";
5237
5766
  //#endregion
5238
5767
  //#region src/host-heartbeat.ts
5239
5768
  /**
@@ -5391,4 +5920,4 @@ async function clearHostHeartbeat() {
5391
5920
  await rm(workbuddyHostHeartbeatPath(), { force: true });
5392
5921
  }
5393
5922
  //#endregion
5394
- export { lastRefusalFor as $, workbuddyAccountId as A, isCredentialRejectedError as B, defaultDesktopAuthDirs as C, withFileLock as Ct, isVaultAccountId as D, isEncryptedCredentialError as E, WorkBuddyUpstreamClient as F, parseCreditMultiplier as G, knownRegionOf as H, classifyUpstreamError as I, perturbChannelFingerprints as J, parseReasoning as K, declaredTools as L, CREDENTIAL_REJECTED_CODE as M, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as N, legacyWorkbuddyOwnAuthPath as O, WorkBuddyCredentialRejectedError as P, defaultOffModelIds as Q, forceNeutralHead as R, defaultDesktopAuthCandidates as S, resolveDshHome as St, hasEncryptedCredentialFields as T, modelIdOfBody as U, isFirstMessageGateBody as V, openingEvidenceOfBody as W, regionOf as X, prepareChatBody as Y, REFUSES_OFF_MODEL_IDS as Z, WORKBUDDY_AUTH_FILENAME as _, macosNestedAppBundles as _t, readHostHeartbeat as a, imageDefaultFor as at, WorkBuddyEncryptedCredentialError as b, workbuddyAppExecutableCandidates as bt, WORKBUDDY_CONNECT_VERSION as c, clearAtRestKeyCache as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, fetchAtRestKeyPayload as dt, offDefaultFor as et, WorkBuddyCatalog as f, findWorkbuddyAppExecutable as ft, ENCRYPTED_CREDENTIAL_CODE as g, macosBundleExecutable as gt, fallbackModelsFor as h, isWorkbuddyBundle as ht, processStartTimeMs as i, NATIVE_MODALITY_BY_MODEL_ID as it, workbuddyOwnAuthPath as j, parseWorkBuddyAuth as k, piAiRuntimeInfo as l, deriveAtRestKey as lt, deriveCatalog as m, isEncryptedFieldWrapper as mt, clearHostHeartbeat as n, recordRefusal as nt, workbuddyHostHeartbeatPath as o, nativeModalityOf as ot, applyContextBudgets as p, findWorkbuddyAppExecutableWithSource as pt, parseUpstreamModel as q, isHeartbeatProcessAlive as r, withEffectiveOff as rt, writeHostHeartbeat as s, WORKBUDDY_APP_EXECUTABLE_ENV as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, offRefused as tt, FALLBACK_WORKBUDDY_MODELS as u, deriveAtRestKeyId as ut, WORKBUDDY_AUTH_FILE_ENV as v, openEncryptedField as vt, defaultDesktopAuthPath as w, writeFileAtomic as wt, authFileName as x, hostPeersInfo as xt, WorkBuddyCredentialStore as y, readAtRestKey as yt, isConversationIdGateBody as z };
5923
+ export { REFUSES_OFF_MODEL_IDS as $, workbuddyAccountId as A, withFileLock as At, isCredentialRejectedError as B, defaultDesktopAuthDirs as C, macosBundleExecutable as Ct, isVaultAccountId as D, workbuddyAppExecutableCandidates as Dt, isEncryptedCredentialError as E, readAtRestKey as Et, WorkBuddyUpstreamClient as F, parseCreditMultiplier as G, knownRegionOf as H, classifyUpstreamError as I, perturbChannelFingerprints as J, parseReasoning as K, declaredTools as L, CREDENTIAL_REJECTED_CODE as M, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as N, legacyWorkbuddyOwnAuthPath as O, hostPeersInfo as Ot, WorkBuddyCredentialRejectedError as P, busyDisplaySuffix as Q, forceNeutralHead as R, defaultDesktopAuthCandidates as S, isWorkbuddyBundle as St, hasEncryptedCredentialFields as T, openEncryptedField as Tt, modelIdOfBody as U, isFirstMessageGateBody as V, openingEvidenceOfBody as W, regionOf as X, prepareChatBody as Y, attachBusyLevels as Z, WORKBUDDY_AUTH_FILENAME as _, deriveAtRestKeyId as _t, readHostHeartbeat as a, withEffectiveOff as at, WorkBuddyEncryptedCredentialError as b, findWorkbuddyAppExecutableWithSource as bt, WORKBUDDY_CONNECT_VERSION as c, pickImageModel as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, NATIVE_MODALITY_BY_MODEL_ID as dt, defaultOffModelIds as et, WorkBuddyCatalog as f, imageDefaultFor as ft, ENCRYPTED_CREDENTIAL_CODE as g, deriveAtRestKey as gt, fallbackModelsFor as h, clearAtRestKeyCache as ht, processStartTimeMs as i, recordRefusal as it, workbuddyOwnAuthPath as j, writeFileAtomic as jt, parseWorkBuddyAuth as k, resolveDshHome as kt, piAiRuntimeInfo as l, parseQueueAnswer as lt, deriveCatalog as m, WORKBUDDY_APP_EXECUTABLE_ENV as mt, clearHostHeartbeat as n, offDefaultFor as nt, workbuddyHostHeartbeatPath as o, DEFAULT_IMAGE_SIZE as ot, applyContextBudgets as p, nativeModalityOf as pt, parseUpstreamModel as q, isHeartbeatProcessAlive as r, offRefused as rt, writeHostHeartbeat as s, WorkBuddyImageError as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, lastRefusalFor as tt, FALLBACK_WORKBUDDY_MODELS as u, queueNoticeText as ut, WORKBUDDY_AUTH_FILE_ENV as v, fetchAtRestKeyPayload as vt, defaultDesktopAuthPath as w, macosNestedAppBundles as wt, authFileName as x, isEncryptedFieldWrapper as xt, WorkBuddyCredentialStore as y, findWorkbuddyAppExecutable as yt, isConversationIdGateBody as z };