dsh-connect-workbuddy 4.13.2 → 4.14.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 +50 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +327 -16
- package/lib/{host-heartbeat-BJAL9pEe.js → host-heartbeat-B1I95fKS.js} +307 -52
- package/lib/index.d.ts +214 -21
- package/lib/index.js +6 -17
- package/package.json +1 -1
|
@@ -1096,6 +1096,198 @@ function withEffectiveOff(models, selection) {
|
|
|
1096
1096
|
});
|
|
1097
1097
|
}
|
|
1098
1098
|
//#endregion
|
|
1099
|
+
//#region src/promo.ts
|
|
1100
|
+
/** A record-ish value, or undefined for anything else. */
|
|
1101
|
+
function record(value) {
|
|
1102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1103
|
+
}
|
|
1104
|
+
/** A trimmed non-empty string, or undefined. */
|
|
1105
|
+
function text(value) {
|
|
1106
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* `'HH:mm'` → minutes since midnight, or undefined for anything else.
|
|
1110
|
+
*
|
|
1111
|
+
* `24:00` is accepted and means exactly 1440 — the END of a day. The upstream
|
|
1112
|
+
* writes "all day" as `0:00`–`23:59` today, which leaves a one-minute hole
|
|
1113
|
+
* between 23:59 and midnight where a standing campaign reads as not running.
|
|
1114
|
+
* Accepting `24:00` lets such a window be expressed (and lets a future upstream
|
|
1115
|
+
* fix it) without that hole; as a START it is rejected, because a day that
|
|
1116
|
+
* begins at 24:00 is the next day and that is a different statement.
|
|
1117
|
+
*
|
|
1118
|
+
* Any other malformed value is left to the caller to drop — a wrong window
|
|
1119
|
+
* would hide a live promotion, and guessing is not this parser's job.
|
|
1120
|
+
*/
|
|
1121
|
+
function minutesOfClock(value) {
|
|
1122
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
|
1123
|
+
if (match === null) return void 0;
|
|
1124
|
+
const hours = Number(match[1]);
|
|
1125
|
+
const minutes = Number(match[2]);
|
|
1126
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return void 0;
|
|
1127
|
+
if (minutes > 59) return void 0;
|
|
1128
|
+
if (hours > 24) return void 0;
|
|
1129
|
+
if (hours === 24) return minutes === 0 ? 1440 : void 0;
|
|
1130
|
+
return hours * 60 + minutes;
|
|
1131
|
+
}
|
|
1132
|
+
/** An ISO-8601 instant → epoch milliseconds. Numbers are refused on purpose. */
|
|
1133
|
+
function parseInstant(value) {
|
|
1134
|
+
const raw = text(value);
|
|
1135
|
+
if (raw === void 0) return void 0;
|
|
1136
|
+
const ms = Date.parse(raw);
|
|
1137
|
+
return Number.isNaN(ms) ? void 0 : ms;
|
|
1138
|
+
}
|
|
1139
|
+
/** The upstream's `modelIds` → the model's ids, de-duplicated. */
|
|
1140
|
+
function parseModelIds(value) {
|
|
1141
|
+
if (!Array.isArray(value)) return void 0;
|
|
1142
|
+
const ids = [];
|
|
1143
|
+
for (const entry of value) {
|
|
1144
|
+
const id = text(entry);
|
|
1145
|
+
if (id !== void 0 && !ids.includes(id)) ids.push(id);
|
|
1146
|
+
}
|
|
1147
|
+
return ids.length === 0 ? void 0 : ids;
|
|
1148
|
+
}
|
|
1149
|
+
/** The upstream's `badge` → this plugin's badge shape. */
|
|
1150
|
+
function parseBadge(value) {
|
|
1151
|
+
const raw = record(value);
|
|
1152
|
+
if (raw === void 0) return void 0;
|
|
1153
|
+
const label = text(raw["label"]) ?? text(raw["labelEn"]) ?? text(raw["shortLabel"]) ?? text(raw["shortLabelEn"]);
|
|
1154
|
+
if (label === void 0) return void 0;
|
|
1155
|
+
const labelEn = text(raw["labelEn"]);
|
|
1156
|
+
const shortLabel = text(raw["shortLabel"]);
|
|
1157
|
+
const shortLabelEn = text(raw["shortLabelEn"]);
|
|
1158
|
+
const color = text(raw["color"]);
|
|
1159
|
+
const display = raw["display"] === "activeOnly" ? "activeOnly" : void 0;
|
|
1160
|
+
return {
|
|
1161
|
+
label,
|
|
1162
|
+
...labelEn === void 0 ? {} : { labelEn },
|
|
1163
|
+
...shortLabel === void 0 ? {} : { shortLabel },
|
|
1164
|
+
...shortLabelEn === void 0 ? {} : { shortLabelEn },
|
|
1165
|
+
...color === void 0 ? {} : { color },
|
|
1166
|
+
...display === void 0 ? {} : { display }
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/** The upstream's `hover` → this plugin's copy shape. */
|
|
1170
|
+
function parseHover(value) {
|
|
1171
|
+
const raw = record(value);
|
|
1172
|
+
if (raw === void 0) return void 0;
|
|
1173
|
+
const textZh = text(raw["textZh"]);
|
|
1174
|
+
const textEn = text(raw["textEn"]);
|
|
1175
|
+
if (textZh === void 0 && textEn === void 0) return void 0;
|
|
1176
|
+
return {
|
|
1177
|
+
...textZh === void 0 ? {} : { textZh },
|
|
1178
|
+
...textEn === void 0 ? {} : { textEn }
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/** The upstream's `schedule.daily` → the windows this plugin can evaluate. */
|
|
1182
|
+
function parseDaily(value) {
|
|
1183
|
+
if (!Array.isArray(value)) return void 0;
|
|
1184
|
+
const windows = [];
|
|
1185
|
+
for (const entry of value) {
|
|
1186
|
+
const raw = record(entry);
|
|
1187
|
+
if (raw === void 0) continue;
|
|
1188
|
+
const start = text(raw["start"]);
|
|
1189
|
+
const end = text(raw["end"]);
|
|
1190
|
+
if (start === void 0 || end === void 0) continue;
|
|
1191
|
+
if (minutesOfClock(start) === void 0 || minutesOfClock(end) === void 0) continue;
|
|
1192
|
+
windows.push({
|
|
1193
|
+
start,
|
|
1194
|
+
end
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return windows.length === 0 ? void 0 : windows;
|
|
1198
|
+
}
|
|
1199
|
+
/** The upstream's `schedule` → this plugin's schedule shape. */
|
|
1200
|
+
function parseSchedule(value) {
|
|
1201
|
+
const raw = record(value);
|
|
1202
|
+
if (raw === void 0) return void 0;
|
|
1203
|
+
const timezone = text(raw["timezone"]);
|
|
1204
|
+
const validFrom = parseInstant(raw["validFrom"]);
|
|
1205
|
+
const validUntil = parseInstant(raw["validUntil"]);
|
|
1206
|
+
const daily = parseDaily(raw["daily"]);
|
|
1207
|
+
const dailyUnusable = daily === void 0 && Array.isArray(raw["daily"]) && raw["daily"].length > 0;
|
|
1208
|
+
if (timezone === void 0 && validFrom === void 0 && validUntil === void 0 && daily === void 0 && !dailyUnusable) return void 0;
|
|
1209
|
+
return {
|
|
1210
|
+
...timezone === void 0 ? {} : { timezone },
|
|
1211
|
+
...validFrom === void 0 ? {} : { validFrom },
|
|
1212
|
+
...validUntil === void 0 ? {} : { validUntil },
|
|
1213
|
+
...daily === void 0 ? {} : { daily },
|
|
1214
|
+
...dailyUnusable ? { dailyUnusable: true } : {}
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/** One upstream entry → this plugin's promo shape, or undefined if it is not one. */
|
|
1218
|
+
function parsePromo(value, source) {
|
|
1219
|
+
const raw = record(value);
|
|
1220
|
+
if (raw === void 0) return void 0;
|
|
1221
|
+
if (raw["enabled"] === false) return void 0;
|
|
1222
|
+
const modelIds = parseModelIds(raw["modelIds"]);
|
|
1223
|
+
if (modelIds === void 0) return void 0;
|
|
1224
|
+
const badge = parseBadge(raw["badge"]);
|
|
1225
|
+
if (badge === void 0) return void 0;
|
|
1226
|
+
const hover = parseHover(raw["hover"]);
|
|
1227
|
+
const schedule = parseSchedule(raw["schedule"]);
|
|
1228
|
+
const priority = typeof raw["priority"] === "number" && Number.isFinite(raw["priority"]) ? raw["priority"] : 0;
|
|
1229
|
+
return {
|
|
1230
|
+
id: text(raw["id"]) ?? `${source}:${modelIds.join(",")}`,
|
|
1231
|
+
source,
|
|
1232
|
+
modelIds,
|
|
1233
|
+
badge,
|
|
1234
|
+
...hover === void 0 ? {} : { hover },
|
|
1235
|
+
...schedule === void 0 ? {} : { schedule },
|
|
1236
|
+
priority
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Parse the upstream's promotion and tier lists.
|
|
1241
|
+
*
|
|
1242
|
+
* Both lists are read from the SAME document the roster comes from, so this
|
|
1243
|
+
* never costs a request. Entries that are not promotions — no `modelIds`, no
|
|
1244
|
+
* `badge`, or `enabled: false` — are dropped here rather than downstream, so
|
|
1245
|
+
* that everything downstream can assume a promo is drawable.
|
|
1246
|
+
*
|
|
1247
|
+
* The promotions come first, so that a TIE on `priority` is broken in the
|
|
1248
|
+
* promotions' favour: a price badge ("Free now") is more actionable to a user
|
|
1249
|
+
* than an access-tier badge ("Pro Only"), and the upstream numbers them
|
|
1250
|
+
* equally (both 200 on the observed feed).
|
|
1251
|
+
*/
|
|
1252
|
+
function parseWorkBuddyPromos(rawPromotions, rawTiers) {
|
|
1253
|
+
const promos = [];
|
|
1254
|
+
for (const entry of Array.isArray(rawPromotions) ? rawPromotions : []) {
|
|
1255
|
+
const promo = parsePromo(entry, "promotion");
|
|
1256
|
+
if (promo !== void 0) promos.push(promo);
|
|
1257
|
+
}
|
|
1258
|
+
for (const entry of Array.isArray(rawTiers) ? rawTiers : []) {
|
|
1259
|
+
const tier = parsePromo(entry, "tier");
|
|
1260
|
+
if (tier !== void 0) promos.push(tier);
|
|
1261
|
+
}
|
|
1262
|
+
return promos;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Attach the promotions that name each model.
|
|
1266
|
+
*
|
|
1267
|
+
* The result is UNRESOLVED on purpose: every candidate that names the model
|
|
1268
|
+
* rides along, so the badge can be chosen at render time against the clock
|
|
1269
|
+
* (see {@link selectPromoBadge}). Resolving here would freeze the answer at
|
|
1270
|
+
* fetch time — the defect that makes a "night discount" badge keep claiming
|
|
1271
|
+
* the night after the night is over.
|
|
1272
|
+
*/
|
|
1273
|
+
function attachPromos(models, promos) {
|
|
1274
|
+
if (promos.length === 0) return [...models];
|
|
1275
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const promo of promos) for (const modelId of promo.modelIds) {
|
|
1277
|
+
const list = byModel.get(modelId);
|
|
1278
|
+
if (list === void 0) byModel.set(modelId, [promo]);
|
|
1279
|
+
else list.push(promo);
|
|
1280
|
+
}
|
|
1281
|
+
return models.map((model) => {
|
|
1282
|
+
const list = byModel.get(model.id);
|
|
1283
|
+
if (list === void 0) return model;
|
|
1284
|
+
return {
|
|
1285
|
+
...model,
|
|
1286
|
+
promos: [...list].sort((a, b) => b.priority - a.priority)
|
|
1287
|
+
};
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
//#endregion
|
|
1099
1291
|
//#region src/upstream.ts
|
|
1100
1292
|
/**
|
|
1101
1293
|
* WorkBuddy (CodeBuddy / copilot.tencent.com) upstream client: chat streaming,
|
|
@@ -1145,55 +1337,80 @@ const GLOBAL_BASE = "https://www.workbuddy.ai";
|
|
|
1145
1337
|
* Legacy model-catalog path, kept only as the CN region's FALLBACK.
|
|
1146
1338
|
*
|
|
1147
1339
|
* This is the document the WorkBuddy *plugin* used to read, and it is NOT the
|
|
1148
|
-
* document the WorkBuddy *app* reads
|
|
1149
|
-
* channel's roster,
|
|
1150
|
-
*
|
|
1151
|
-
* `hy4-preview-f`
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1340
|
+
* document the WorkBuddy *app* reads: the gateway answers it with the CLI
|
|
1341
|
+
* channel's roster, which carries neither the app's `auto` slot nor any
|
|
1342
|
+
* promotion feed. The free Hy4 tier that used to live here as a separate
|
|
1343
|
+
* `hy4-preview-f` record is gone as a record — measured 2026-10-21, both
|
|
1344
|
+
* channels list the paid `hy4-preview` (`x0.29`) — and the free tier is now
|
|
1345
|
+
* expressed as a promotion badge (`夜间免费`) in the app-channel document only.
|
|
1346
|
+
* So this path is strictly poorer than {@link GLOBAL_CONFIG_PATH}; it stays
|
|
1347
|
+
* only so that a gateway which stops serving the modern document degrades
|
|
1348
|
+
* instead of emptying the region.
|
|
1155
1349
|
*/
|
|
1156
1350
|
const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
|
|
1157
1351
|
/**
|
|
1158
|
-
* Remote product-config path
|
|
1159
|
-
*
|
|
1352
|
+
* Remote product-config path — the document the WorkBuddy app itself reads for
|
|
1353
|
+
* its model list, its prices AND its promotion badges.
|
|
1160
1354
|
*
|
|
1161
1355
|
* It is field-compatible with {@link MODELS_CATALOG_PATH} for every key
|
|
1162
|
-
* {@link parseUpstreamModel} reads, so one parser still serves both
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
* `hy4-preview-f`(x0.00); desktop token = 29 models, no `hy4-preview-f` and no
|
|
1171
|
-
* cheap free tier. That is why the global branch below pairs this path with
|
|
1172
|
-
* {@link DESKTOP_UA} and the CN branch must NOT.
|
|
1356
|
+
* {@link parseUpstreamModel} reads, so one parser still serves both, and it
|
|
1357
|
+
* additionally carries `modelPromotions` / `modelTiers` — the promotion feed
|
|
1358
|
+
* no other path publishes (see {@link withPromos}).
|
|
1359
|
+
*
|
|
1360
|
+
* Which document this path answers depends on the CHANNEL the request claims
|
|
1361
|
+
* to be (see {@link PRODUCT_UA}); it does not depend on the host: `www.workbuddy.cn`,
|
|
1362
|
+
* `copilot.tencent.com`, `wb.tencentbuddy.com` and `www.codebuddy.cn` answer
|
|
1363
|
+
* byte-identically under the same header.
|
|
1173
1364
|
*/
|
|
1174
1365
|
const GLOBAL_CONFIG_PATH = "/v3/config";
|
|
1366
|
+
/**
|
|
1367
|
+
* The chat channel's user agent — the identity the plugin's adapter speaks as.
|
|
1368
|
+
*
|
|
1369
|
+
* Chat is a different contract from configuration: the conversation-identity
|
|
1370
|
+
* triple (conversation id, session id, device id) is bound to this channel, and
|
|
1371
|
+
* the upstream bills and rate-limits it as the CLI agent. The plugin chats as
|
|
1372
|
+
* the CLI agent on purpose, so this stays {@link CLIENT_UA} everywhere the
|
|
1373
|
+
* request is a CHAT.
|
|
1374
|
+
*/
|
|
1175
1375
|
const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
|
|
1176
1376
|
/**
|
|
1177
|
-
*
|
|
1377
|
+
* The product-config channel's user agent: the WorkBuddy APP's own product
|
|
1378
|
+
* identity, byte-for-byte the header the app sends.
|
|
1379
|
+
*
|
|
1380
|
+
* The app builds it in `buildVlmGatewayHeaders` (WorkBuddy 5.8.3) as
|
|
1381
|
+
*
|
|
1382
|
+
* `User-Agent: WorkBuddy/<app> WorkBuddy/<app> CLI/<cli>`
|
|
1383
|
+
*
|
|
1384
|
+
* — the app version twice, the CLI version once — and sends it to
|
|
1385
|
+
* `/v3/config`; the app's own log reads
|
|
1386
|
+
* `fetch cloud product from url:https://www.workbuddy.cn/v3/config success`
|
|
1387
|
+
* with `{"user-agent":"WorkBuddy/5.8.3 WorkBuddy/5.8.3 CLI/2.161.0", …}`.
|
|
1178
1388
|
*
|
|
1179
|
-
*
|
|
1180
|
-
* channel
|
|
1181
|
-
*
|
|
1182
|
-
* identically). On the INTERNATIONAL gateway the split decides which models
|
|
1183
|
-
* exist at all:
|
|
1389
|
+
* It matters, because `/v3/config` answers a DIFFERENT product configuration
|
|
1390
|
+
* per client channel. Measured 2026-10-21, same token, same URL, only this
|
|
1391
|
+
* header differing (CN gateway):
|
|
1184
1392
|
*
|
|
1185
|
-
* -
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1393
|
+
* | User-Agent | `/v3/config` answer |
|
|
1394
|
+
* | --- | --- |
|
|
1395
|
+
* | `CLI/2.63.2 CodeBuddy/2.63.2` (CLI channel) | 5 keys, 35 models, NO promotion feed |
|
|
1396
|
+
* | `WorkBuddy/5.8.3` (app channel, no CLI part) | 18 keys, 37 models, no promotion feed |
|
|
1397
|
+
* | `WorkBuddy/5.8.3 WorkBuddy/5.8.3 CLI/2.161.0` | 22 keys, 62 models, 8 promos + 1 tier |
|
|
1190
1398
|
*
|
|
1191
|
-
* The
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1399
|
+
* The CLI channel's answer is a strict SUBSET of the app channel's: it
|
|
1400
|
+
* publishes neither `modelPromotions` nor `modelTiers`. That is precisely why
|
|
1401
|
+
* the CN card showed no promotion badge while the WorkBuddy app showed one on
|
|
1402
|
+
* the very same account — the plugin was reading a document the upstream
|
|
1403
|
+
* publishes WITHOUT the feed, not a feed that does not exist.
|
|
1404
|
+
*
|
|
1405
|
+
* The version suffixes are inert — `WorkBuddy/1.0.0 CLI/2.63.2
|
|
1406
|
+
* CodeBuddy/2.63.2` answers identically — so what selects the document is the
|
|
1407
|
+
* CHANNEL the header claims, not the version in it.
|
|
1408
|
+
*
|
|
1409
|
+
* Global answers the same document under either app-shaped header (17 keys,
|
|
1410
|
+
* 28 models, 5 promotions + 1 tier, identical roster), so both regions read
|
|
1411
|
+
* `/v3/config` as {@link PRODUCT_UA}.
|
|
1195
1412
|
*/
|
|
1196
|
-
const
|
|
1413
|
+
const PRODUCT_UA = "WorkBuddy/5.8.3 WorkBuddy/5.8.3 CLI/2.161.0";
|
|
1197
1414
|
const JSON_TIMEOUT_MS = 3e4;
|
|
1198
1415
|
const ERROR_BODY_LIMIT = 4096;
|
|
1199
1416
|
/**
|
|
@@ -2110,8 +2327,16 @@ function parseUpstreamModel(value) {
|
|
|
2110
2327
|
* selector serves them. Without a usable `cli` roster the whole parsed catalog
|
|
2111
2328
|
* is exposed rather than nothing: the roster is an upstream detail that may
|
|
2112
2329
|
* change, and an empty answer would silently disarm the provider.
|
|
2330
|
+
*
|
|
2331
|
+
* `keepIds` is the user's own selection riding along with the refresh. The
|
|
2332
|
+
* upstream roster may drop a model the user is actually running — the app
|
|
2333
|
+
* channel's `cli` roster dropped `minimax-m2.7` on 2026-10-21 while the SAME
|
|
2334
|
+
* document still publishes it (`x0.26`) and the chat endpoint still answers for
|
|
2335
|
+
* it (measured HTTP 200). A roster read is therefore the upstream's roster PLUS
|
|
2336
|
+
* the kept ids this document still lists: a refresh may add or withdraw
|
|
2337
|
+
* upstream models, but it never silently withdraws a model the user is on.
|
|
2113
2338
|
*/
|
|
2114
|
-
function selectCliModels(rawModels, agents) {
|
|
2339
|
+
function selectCliModels(rawModels, agents, keepIds = []) {
|
|
2115
2340
|
const byId = /* @__PURE__ */ new Map();
|
|
2116
2341
|
for (const model of Array.isArray(rawModels) ? rawModels : []) {
|
|
2117
2342
|
const parsed = parseUpstreamModel(model);
|
|
@@ -2125,9 +2350,32 @@ function selectCliModels(rawModels, agents) {
|
|
|
2125
2350
|
break;
|
|
2126
2351
|
}
|
|
2127
2352
|
}
|
|
2128
|
-
const
|
|
2353
|
+
const ids = cliIds !== void 0 && cliIds.length > 0 ? cliIds : [...byId.keys()];
|
|
2354
|
+
const models = ids.map((id) => byId.get(id)).filter((model) => model !== void 0);
|
|
2129
2355
|
if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
|
|
2130
|
-
|
|
2356
|
+
const kept = keepIds.filter((id) => !ids.includes(id)).map((id) => byId.get(id)).filter((model) => model !== void 0);
|
|
2357
|
+
return [...models, ...kept];
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* The roster plus the promotion badges the SAME document carries.
|
|
2361
|
+
*
|
|
2362
|
+
* `data.modelPromotions` and `data.modelTiers` sit beside `data.models` in
|
|
2363
|
+
* the upstream's `/v3/config` document, so the badges cost no extra request:
|
|
2364
|
+
* they are read out of the response the roster was read from. Which models
|
|
2365
|
+
* they name is resolved here; WHICH badge holds at a given moment is not
|
|
2366
|
+
* (see `src/promo.ts` — a badge is a function of the clock).
|
|
2367
|
+
*
|
|
2368
|
+
* BOTH regions publish the feed, and they publish it in the app channel's
|
|
2369
|
+
* document: measured 2026-10-21 on the same accounts the app reads, CN
|
|
2370
|
+
* `/v3/config` carries 8 promotions + 1 tier badge (Chinese labels such as
|
|
2371
|
+
* `夜间折扣` / `限时免费` / `订阅优先`, with `schedule.daily` night windows),
|
|
2372
|
+
* and the global gateway carries 5 promotions + 1 tier badge. The feed is
|
|
2373
|
+
* absent only from the CLI channel's answer, which the plugin never reads a
|
|
2374
|
+
* product configuration as (see {@link PRODUCT_UA}).
|
|
2375
|
+
*/
|
|
2376
|
+
function withPromos(models, data) {
|
|
2377
|
+
const promos = parseWorkBuddyPromos(data["modelPromotions"], data["modelTiers"]);
|
|
2378
|
+
return promos.length === 0 ? models : attachPromos(models, promos);
|
|
2131
2379
|
}
|
|
2132
2380
|
/**
|
|
2133
2381
|
* Upstream HTTP client. One instance serves the whole plugin; requests take
|
|
@@ -2269,13 +2517,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
2269
2517
|
/**
|
|
2270
2518
|
* Read the model directory for the credential's region.
|
|
2271
2519
|
*
|
|
2272
|
-
* Both regions
|
|
2273
|
-
* consumes,
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
*
|
|
2520
|
+
* Both regions read `/v3/config`, the document the WorkBuddy app itself
|
|
2521
|
+
* consumes, and both read it as the APP's product identity
|
|
2522
|
+
* ({@link PRODUCT_UA}) — the header the app sends, not the CLI channel's.
|
|
2523
|
+
* That is load-bearing, not cosmetic: the CLI channel's answer is a strict
|
|
2524
|
+
* subset of the app channel's and publishes neither `modelPromotions` nor
|
|
2525
|
+
* `modelTiers`, so reading `/v3/config` as the CLI channel is what left the
|
|
2526
|
+
* CN card without a single promotion badge while the app showed one on the
|
|
2527
|
+
* same account (see {@link PRODUCT_UA}).
|
|
2279
2528
|
*
|
|
2280
2529
|
* CN falls back to the legacy {@link MODELS_CATALOG_PATH} when `/v3` fails,
|
|
2281
2530
|
* returns a non-zero envelope, or resolves to no usable model, so a gateway
|
|
@@ -2286,8 +2535,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
2286
2535
|
*
|
|
2287
2536
|
* No user-side toggle is involved: the region comes from the credential's
|
|
2288
2537
|
* `domain`.
|
|
2538
|
+
*
|
|
2539
|
+
* `keepModelIds` is the user's own selection, passed through to
|
|
2540
|
+
* {@link selectCliModels}: a model the upstream roster dropped but the SAME
|
|
2541
|
+
* document still publishes stays in the roster, so reading the directory
|
|
2542
|
+
* never withdraws a model the user is running (issue #32's failure mode,
|
|
2543
|
+
* one step upstream of it).
|
|
2289
2544
|
*/
|
|
2290
|
-
async fetchModels(credential, signal) {
|
|
2545
|
+
async fetchModels(credential, signal, keepModelIds = []) {
|
|
2291
2546
|
const timeout = signal ?? AbortSignal.timeout(JSON_TIMEOUT_MS);
|
|
2292
2547
|
if (regionOf(credential.domain) === "global") {
|
|
2293
2548
|
const response = await fetch(`${globalBase(credential.domain)}${GLOBAL_CONFIG_PATH}`, {
|
|
@@ -2299,14 +2554,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
2299
2554
|
"X-Product": "SaaS",
|
|
2300
2555
|
"X-Requested-With": "XMLHttpRequest",
|
|
2301
2556
|
"Connection": "close",
|
|
2302
|
-
"User-Agent":
|
|
2557
|
+
"User-Agent": PRODUCT_UA
|
|
2303
2558
|
},
|
|
2304
2559
|
signal: timeout
|
|
2305
2560
|
});
|
|
2306
2561
|
const envelope = await readEnvelope(response);
|
|
2307
2562
|
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
2308
2563
|
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
2309
|
-
return selectCliModels(data["models"], data["agents"]);
|
|
2564
|
+
return withPromos(selectCliModels(data["models"], data["agents"], keepModelIds), data);
|
|
2310
2565
|
}
|
|
2311
2566
|
const readCatalog = async (path) => {
|
|
2312
2567
|
const response = await fetch(`${chatBase(credential)}${path}`, {
|
|
@@ -2315,14 +2570,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
2315
2570
|
"Accept": "application/json",
|
|
2316
2571
|
"Origin": originReferer(credential),
|
|
2317
2572
|
"Referer": `${originReferer(credential)}/`,
|
|
2318
|
-
"User-Agent":
|
|
2573
|
+
"User-Agent": PRODUCT_UA
|
|
2319
2574
|
},
|
|
2320
2575
|
signal: timeout
|
|
2321
2576
|
});
|
|
2322
2577
|
const envelope = await readEnvelope(response);
|
|
2323
2578
|
if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
|
|
2324
2579
|
const data = typeof envelope.data === "object" && envelope.data !== null ? envelope.data : {};
|
|
2325
|
-
return selectCliModels(data["models"], data["agents"]);
|
|
2580
|
+
return withPromos(selectCliModels(data["models"], data["agents"], keepModelIds), data);
|
|
2326
2581
|
};
|
|
2327
2582
|
try {
|
|
2328
2583
|
return await readCatalog(GLOBAL_CONFIG_PATH);
|
|
@@ -4515,7 +4770,7 @@ function generationOf(version) {
|
|
|
4515
4770
|
//#endregion
|
|
4516
4771
|
//#region src/version.ts
|
|
4517
4772
|
/** The npm package version this build was produced from. */
|
|
4518
|
-
const WORKBUDDY_CONNECT_VERSION = "4.
|
|
4773
|
+
const WORKBUDDY_CONNECT_VERSION = "4.14.0";
|
|
4519
4774
|
//#endregion
|
|
4520
4775
|
//#region src/host-heartbeat.ts
|
|
4521
4776
|
/**
|