dsh-connect-workbuddy 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +141 -0
- package/README.en.md +1 -1
- package/README.md +3 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +480 -9
- package/lib/{host-heartbeat-C1tVU2IN.js → host-heartbeat-BYdr-pVq.js} +616 -33
- package/lib/index.d.ts +211 -5
- package/lib/index.js +688 -49
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -50,6 +50,18 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
const WORKBUDDY_BENEFIT_CLAIM_PATH = "/plugins/dsh-connect-workbuddy/benefit/claim";
|
|
51
51
|
/** Plugin-owned live model refresh endpoint. */
|
|
52
52
|
const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
|
|
53
|
+
/**
|
|
54
|
+
* Plugin-owned MEDIA model list endpoint (image + video, read from the region's
|
|
55
|
+
* `/v3/config`).
|
|
56
|
+
*
|
|
57
|
+
* Separate from {@link WORKBUDDY_MODELS_REFRESH_PATH} because it answers a
|
|
58
|
+
* different question from a different document projection: the refresh route
|
|
59
|
+
* returns the CHAT roster, from which `selectCliModels` drops every media
|
|
60
|
+
* entry, while this one keeps the `text-to-image`/`image-to-image`/
|
|
61
|
+
* `text-to-video`/`image-to-video` rows the card lists. Read-only and free —
|
|
62
|
+
* it is the same GET the image tool already performs.
|
|
63
|
+
*/
|
|
64
|
+
const WORKBUDDY_MEDIA_MODELS_PATH = "/plugins/dsh-connect-workbuddy/media-models";
|
|
53
65
|
/** Plugin-owned local account rescan endpoint. */
|
|
54
66
|
const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
|
|
55
67
|
/**
|
|
@@ -466,6 +478,44 @@ window.__ModuleLoader__.load({
|
|
|
466
478
|
function regionImageToolEnabledOf(value, region) {
|
|
467
479
|
return regionSlotOf(value, region)["imageToolEnabled"] === true;
|
|
468
480
|
}
|
|
481
|
+
function regionVideoToolEnabledOf(value, region) {
|
|
482
|
+
return regionSlotOf(value, region)["videoToolEnabled"] === true;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The video model pinned per tag in one region, or an empty record.
|
|
486
|
+
*
|
|
487
|
+
* A PIN, not an authorisation: reading it grants nothing, and an absent entry
|
|
488
|
+
* merely means the tool takes that tag group's first model. Values are model ids
|
|
489
|
+
* from the same region's catalog. A stale id is tolerated on purpose — a catalog
|
|
490
|
+
* can drop a model the user had pinned, and that must not turn into a failed
|
|
491
|
+
* generation (the tool falls back to the group's first model).
|
|
492
|
+
*/
|
|
493
|
+
function regionPreferredVideoModelsOf(value, region) {
|
|
494
|
+
const raw = regionSlotOf(value, region)["preferredVideoModels"];
|
|
495
|
+
if (typeof raw !== "object" || raw === null) return {};
|
|
496
|
+
const out = {};
|
|
497
|
+
for (const [tag, id] of Object.entries(raw)) if (typeof id === "string" && id !== "") out[tag] = id;
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Build the next `regions` settings value for one pinned video model.
|
|
502
|
+
*
|
|
503
|
+
* `modelId` of `undefined` or `''` CLEARS that tag's pin, which is why this is
|
|
504
|
+
* a three-state write rather than a boolean: the card's picker has a "no pin"
|
|
505
|
+
* row, and deleting the key is what makes the absence mean "nothing pinned"
|
|
506
|
+
* again (an empty string would be a value the tool would have to special-case).
|
|
507
|
+
*
|
|
508
|
+
* Same merge rule as the switches above: only this region's field changes.
|
|
509
|
+
*/
|
|
510
|
+
function nextRegionPreferredVideoModels(value, region, tag, modelId) {
|
|
511
|
+
const pinned = { ...regionPreferredVideoModelsOf(value, region) };
|
|
512
|
+
if (modelId === void 0 || modelId === "") delete pinned[tag];
|
|
513
|
+
else pinned[tag] = modelId;
|
|
514
|
+
return nextRegionSlots(regionsMapOf(value), region, {
|
|
515
|
+
...regionSlotOf(value, region),
|
|
516
|
+
preferredVideoModels: pinned
|
|
517
|
+
});
|
|
518
|
+
}
|
|
469
519
|
/** The seat a region's readout uses, defaulting to the composer row. */
|
|
470
520
|
function regionCreditPositionOf(value, region) {
|
|
471
521
|
return regionSlotOf(value, region)["creditPosition"] === "stats" ? "stats" : "composer";
|
|
@@ -508,6 +558,12 @@ window.__ModuleLoader__.load({
|
|
|
508
558
|
imageToolEnabled: enabled
|
|
509
559
|
});
|
|
510
560
|
}
|
|
561
|
+
function nextRegionVideoToolEnabled(value, region, enabled) {
|
|
562
|
+
return nextRegionSlots(regionsMapOf(value), region, {
|
|
563
|
+
...regionSlotOf(value, region),
|
|
564
|
+
videoToolEnabled: enabled
|
|
565
|
+
});
|
|
566
|
+
}
|
|
511
567
|
/**
|
|
512
568
|
* Project one card row into its persisted `lastCatalog` shape: the native
|
|
513
569
|
* context window becomes the stored `contextWindow`, and the card-only
|
|
@@ -1150,6 +1206,33 @@ window.__ModuleLoader__.load({
|
|
|
1150
1206
|
return stored !== null && stored !== void 0 && stored["imageToolEnabled"] === enabled;
|
|
1151
1207
|
});
|
|
1152
1208
|
}
|
|
1209
|
+
async function writeRegionVideoToolEnabled(scope, region, enabled, slot) {
|
|
1210
|
+
await writeField(scope, "regions", region, slot, (readBack) => {
|
|
1211
|
+
const stored = readBack;
|
|
1212
|
+
return stored !== null && stored !== void 0 && stored["videoToolEnabled"] === enabled;
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Pin (or clear) one region's video model for ONE mode tag, verifying it landed.
|
|
1217
|
+
*
|
|
1218
|
+
* `modelId` of `undefined`/`''` clears the pin, so the landed check accepts an
|
|
1219
|
+
* ABSENT key as success for that case — writing an empty string instead would
|
|
1220
|
+
* leave a value the tool has to keep special-casing forever.
|
|
1221
|
+
*
|
|
1222
|
+
* Routed through `writeField` like every other settings write (Host endpoint
|
|
1223
|
+
* first, then the landed read-back): on the affected 0.1.7 deployments a direct
|
|
1224
|
+
* `scope.set()` settles without storing anything, and a picker that silently
|
|
1225
|
+
* reverted would be worse than no picker at all.
|
|
1226
|
+
*/
|
|
1227
|
+
async function writeRegionPreferredVideoModels(scope, region, tag, modelId, slot) {
|
|
1228
|
+
await writeField(scope, "regions", region, slot, (readBack) => {
|
|
1229
|
+
const stored = readBack;
|
|
1230
|
+
if (stored === null || stored === void 0) return false;
|
|
1231
|
+
const pinned = stored["preferredVideoModels"];
|
|
1232
|
+
const current = typeof pinned === "object" && pinned !== null ? pinned[tag] : void 0;
|
|
1233
|
+
return modelId === void 0 || modelId === "" ? current === void 0 || current === "" : current === modelId;
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1153
1236
|
async function writeRegionCreditsShown(scope, region, shown, slot) {
|
|
1154
1237
|
await writeField(scope, "regions", region, slot, (readBack) => {
|
|
1155
1238
|
const stored = readBack;
|
|
@@ -4958,7 +5041,7 @@ window.__ModuleLoader__.load({
|
|
|
4958
5041
|
CONTRAST colour, so a ticked box painted itself as a heavy black pill — the
|
|
4959
5042
|
loudest object in a section made of plain 13px text. */
|
|
4960
5043
|
.dsm-workbuddy-pref input{flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
4961
|
-
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-cli-bridge-check,.dsm-workbuddy-upstream-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
5044
|
+
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-video-tool-check,.dsm-workbuddy-cli-bridge-check,.dsm-workbuddy-upstream-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
4962
5045
|
/* The upstream-identity switches are their own group, so they get a divider
|
|
4963
5046
|
above them instead of relying on spacing alone: they change the request the
|
|
4964
5047
|
whole plugin sends, not a per-region display choice, and reading them as the
|
|
@@ -4981,6 +5064,31 @@ window.__ModuleLoader__.load({
|
|
|
4981
5064
|
.dsm-workbuddy-pref-lead{display:flex;align-items:center;justify-content:flex-start;gap:10px;min-width:0}
|
|
4982
5065
|
.dsm-workbuddy-pref-lead .dsm-workbuddy-pref-label{flex:none}
|
|
4983
5066
|
.dsm-workbuddy-pref-hint{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:17px}
|
|
5067
|
+
/* The media roster: a READ-ONLY list, so it is drawn as quiet rows rather than
|
|
5068
|
+
as a table — it answers "what can this tab draw with" in passing, and must
|
|
5069
|
+
not compete with the switches above it for attention. The id is monospace
|
|
5070
|
+
and dimmer than the name because it is the string a tool call has to pass,
|
|
5071
|
+
not the thing a human reads; the tag chips carry the only colour, one hue
|
|
5072
|
+
per direction (text→image vs image→image), so the grouping is visible
|
|
5073
|
+
without a legend. */
|
|
5074
|
+
/* Aligned with the indented rows above it, but under its OWN class: the
|
|
5075
|
+
credit position picker owns .dsm-workbuddy-pref-indent, and the card suite
|
|
5076
|
+
resolves that selector expecting the picker. This block is a block (lead
|
|
5077
|
+
row + hint + list), not a single indented line. */
|
|
5078
|
+
.dsm-workbuddy-media{padding-left:25px}
|
|
5079
|
+
.dsm-workbuddy-media-list{margin:2px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
|
|
5080
|
+
.dsm-workbuddy-media-row{display:flex;align-items:baseline;flex-wrap:wrap;gap:6px;min-width:0}
|
|
5081
|
+
.dsm-workbuddy-media-name{color:var(--dsw-alias-label-secondary,#b8bcc4);font-size:12px}
|
|
5082
|
+
.dsm-workbuddy-media-id{color:var(--dsw-alias-label-tertiary,#8b8f98);font-size:11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;overflow-wrap:anywhere}
|
|
5083
|
+
.dsm-workbuddy-media-tags{display:inline-flex;flex-wrap:wrap;gap:4px}
|
|
5084
|
+
.dsm-workbuddy-media-tag{border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:0 6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8b8f98)}
|
|
5085
|
+
/* The per-mode video-model picker. It reuses .dsm-workbuddy-media-row so the
|
|
5086
|
+
tag chip and the select sit on one baseline, like a roster row does. */
|
|
5087
|
+
.dsm-workbuddy-video-model-select{font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary,#b8bcc4);background:var(--dsw-alias-bg-l1,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:1px 6px;max-width:100%;min-width:0}
|
|
5088
|
+
.dsm-workbuddy-video-model-select:disabled{opacity:.6}
|
|
5089
|
+
.dsm-workbuddy-media-empty,.dsm-workbuddy-media-error{font-size:12px;line-height:17px}
|
|
5090
|
+
.dsm-workbuddy-media-empty{color:var(--dsw-alias-label-tertiary,#8b8f98)}
|
|
5091
|
+
.dsm-workbuddy-media-error{color:var(--dsw-alias-label-error,#e5675f)}
|
|
4984
5092
|
/* The supplement to the host's fixed quota sentence (issue #63). The plugin no
|
|
4985
5093
|
longer claims the host's shell.quota-notice chain seat, so this no longer
|
|
4986
5094
|
paints into shell.overlay — that seat is the reason the previous build's
|
|
@@ -6062,6 +6170,32 @@ window.__ModuleLoader__.load({
|
|
|
6062
6170
|
* text-only model and an unverified one look identically broken next to their
|
|
6063
6171
|
* checked siblings.
|
|
6064
6172
|
*/
|
|
6173
|
+
/**
|
|
6174
|
+
* The media roster from the Host's JSON, tolerantly.
|
|
6175
|
+
*
|
|
6176
|
+
* Anything that is not a row with a string id and a name is DROPPED, never
|
|
6177
|
+
* rendered: this is a read-only list, and a half-shaped entry is not worth an
|
|
6178
|
+
* exception that would take down the whole card. A Host older than the route
|
|
6179
|
+
* (or any other answer without `models`) yields an empty list, which the block
|
|
6180
|
+
* shows as "this tab publishes none" instead of as a failure.
|
|
6181
|
+
*/
|
|
6182
|
+
function readMediaRoster(value) {
|
|
6183
|
+
const models = value?.models;
|
|
6184
|
+
if (!Array.isArray(models)) return void 0;
|
|
6185
|
+
const rows = [];
|
|
6186
|
+
for (const entry of models) {
|
|
6187
|
+
const record = entry;
|
|
6188
|
+
if (record === null || typeof record !== "object") continue;
|
|
6189
|
+
if (typeof record.id !== "string" || record.id === "") continue;
|
|
6190
|
+
const tags = Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : [];
|
|
6191
|
+
rows.push({
|
|
6192
|
+
id: record.id,
|
|
6193
|
+
name: typeof record.name === "string" ? record.name : record.id,
|
|
6194
|
+
tags
|
|
6195
|
+
});
|
|
6196
|
+
}
|
|
6197
|
+
return rows;
|
|
6198
|
+
}
|
|
6065
6199
|
function imageCheckboxHint(model, t) {
|
|
6066
6200
|
switch (nativeModalityOf(model.id)) {
|
|
6067
6201
|
case "multimodal": return t("row.modelImage");
|
|
@@ -6443,6 +6577,18 @@ window.__ModuleLoader__.load({
|
|
|
6443
6577
|
const [saveError, setSaveError] = (0, react.useState)(void 0);
|
|
6444
6578
|
const [switchingAccount, setSwitchingAccount] = (0, react.useState)(false);
|
|
6445
6579
|
/**
|
|
6580
|
+
* The region's MEDIA model roster (read-only), keyed by region.
|
|
6581
|
+
*
|
|
6582
|
+
* NOT part of `statusByRegion`: the usage document is the editable model
|
|
6583
|
+
* directory, and folding media rows into it would put them in the saved
|
|
6584
|
+
* `lastCatalog` — where the adapter would offer an image model as a chat
|
|
6585
|
+
* model. It is a separate, cheap, read-only read that only the capability
|
|
6586
|
+
* panel needs.
|
|
6587
|
+
*/
|
|
6588
|
+
const [mediaByRegion, setMediaByRegion] = (0, react.useState)({});
|
|
6589
|
+
const [mediaError, setMediaError] = (0, react.useState)(void 0);
|
|
6590
|
+
const [mediaBusy, setMediaBusy] = (0, react.useState)(false);
|
|
6591
|
+
/**
|
|
6446
6592
|
* Whether the account pool's save is in flight. Both sections write into one
|
|
6447
6593
|
* region slot and the Host merges per-region, so two concurrent saves can
|
|
6448
6594
|
* interleave on a stale base and revert each other while both report success
|
|
@@ -6462,6 +6608,15 @@ window.__ModuleLoader__.load({
|
|
|
6462
6608
|
*/
|
|
6463
6609
|
const [togglingCredits, setTogglingCredits] = (0, react.useState)(void 0);
|
|
6464
6610
|
/**
|
|
6611
|
+
* Which video-model pin is being written right now: the tag string, or
|
|
6612
|
+
* undefined.
|
|
6613
|
+
*
|
|
6614
|
+
* A SEPARATE state from {@link togglingCredits} for the same reason those two
|
|
6615
|
+
* switches keep separate busy flags: the pin lives in a different field of the
|
|
6616
|
+
* same region slot, so one in-flight write must not disable the other control.
|
|
6617
|
+
*/
|
|
6618
|
+
const [pinningVideoModel, setPinningVideoModel] = (0, react.useState)(void 0);
|
|
6619
|
+
/**
|
|
6465
6620
|
* The OPTIONAL CodeBuddy CLI route switch (global: one switch, one budget).
|
|
6466
6621
|
*
|
|
6467
6622
|
* `undefined` until the first Host answer arrives, so the box renders the
|
|
@@ -6843,6 +6998,35 @@ window.__ModuleLoader__.load({
|
|
|
6843
6998
|
return regionImageToolEnabledOf(settingsScope?.getSnapshot().value, item);
|
|
6844
6999
|
};
|
|
6845
7000
|
/**
|
|
7001
|
+
* Whether the model may call the video tool in this region. Mirrors
|
|
7002
|
+
* imageToolOn: read off the Host answer first, so a stale mirror cannot
|
|
7003
|
+
* leave the spending capability on. Absent is NOT consent.
|
|
7004
|
+
*/
|
|
7005
|
+
const videoToolOn = (item) => {
|
|
7006
|
+
const usage = statusByRegion[item];
|
|
7007
|
+
if (typeof usage?.videoToolEnabled === "boolean") return usage.videoToolEnabled;
|
|
7008
|
+
return regionVideoToolEnabledOf(settingsScope?.getSnapshot().value, item);
|
|
7009
|
+
};
|
|
7010
|
+
/**
|
|
7011
|
+
* The video model pinned for one mode tag in this region, or `''` for "no pin".
|
|
7012
|
+
*
|
|
7013
|
+
* Same read order as {@link videoToolOn} and for the same measured reason: the
|
|
7014
|
+
* Host's answer first, because this namespace's browser mirror is stale on the
|
|
7015
|
+
* affected 0.1.7 deployments — a picker reading the mirror alone would keep
|
|
7016
|
+
* showing a pin the tool no longer uses.
|
|
7017
|
+
*
|
|
7018
|
+
* `''` rather than `undefined` because that is what the `<select>` needs for its
|
|
7019
|
+
* "default" row; the write path turns it back into "remove the key".
|
|
7020
|
+
*/
|
|
7021
|
+
const preferredVideoModelOf = (item, tag) => {
|
|
7022
|
+
const fromHost = statusByRegion[item]?.preferredVideoModels;
|
|
7023
|
+
if (typeof fromHost === "object" && fromHost !== null) {
|
|
7024
|
+
const pinned = fromHost[tag];
|
|
7025
|
+
if (typeof pinned === "string") return pinned;
|
|
7026
|
+
}
|
|
7027
|
+
return regionPreferredVideoModelsOf(settingsScope?.getSnapshot().value, item)[tag] ?? "";
|
|
7028
|
+
};
|
|
7029
|
+
/**
|
|
6846
7030
|
* Switch one region's provider off or on. The write carries the region's
|
|
6847
7031
|
* whole slot through untouched — only `enabled` changes — so the user's
|
|
6848
7032
|
* directory, model picks, image opt-ins and budgets survive a round trip.
|
|
@@ -7112,6 +7296,107 @@ window.__ModuleLoader__.load({
|
|
|
7112
7296
|
if (mounted.current) setTogglingCredits(void 0);
|
|
7113
7297
|
}
|
|
7114
7298
|
};
|
|
7299
|
+
const toggleVideoTool = async (item, enabled) => {
|
|
7300
|
+
if (settingsScope === void 0) return;
|
|
7301
|
+
setTogglingCredits(item);
|
|
7302
|
+
try {
|
|
7303
|
+
const slot = nextRegionVideoToolEnabled(settingsScope.getSnapshot().value, item, enabled)[item];
|
|
7304
|
+
await writeRegionVideoToolEnabled(settingsScope, item, enabled, typeof slot === "object" && slot !== null ? slot : {});
|
|
7305
|
+
await refreshUsage(item);
|
|
7306
|
+
} catch (error) {
|
|
7307
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
7308
|
+
} finally {
|
|
7309
|
+
if (mounted.current) setTogglingCredits(void 0);
|
|
7310
|
+
}
|
|
7311
|
+
};
|
|
7312
|
+
/**
|
|
7313
|
+
* Pin (or clear) one region's video model for one mode tag.
|
|
7314
|
+
*
|
|
7315
|
+
* `undefined` means "no pin": the picker's default row, which the tool reads as
|
|
7316
|
+
* "take the first model of that group". Both directions go through the same
|
|
7317
|
+
* landed-checked write as every other settings control, then re-read the Host —
|
|
7318
|
+
* a pin that reverted silently would leave the user believing the tool would
|
|
7319
|
+
* use a model it never picks.
|
|
7320
|
+
*/
|
|
7321
|
+
const pinVideoModel = async (item, tag, modelId) => {
|
|
7322
|
+
if (settingsScope === void 0) return;
|
|
7323
|
+
setPinningVideoModel(tag);
|
|
7324
|
+
try {
|
|
7325
|
+
const slot = nextRegionPreferredVideoModels(settingsScope.getSnapshot().value, item, tag, modelId)[item];
|
|
7326
|
+
await writeRegionPreferredVideoModels(settingsScope, item, tag, modelId, typeof slot === "object" && slot !== null ? slot : {});
|
|
7327
|
+
await refreshUsage(item);
|
|
7328
|
+
} catch (error) {
|
|
7329
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
7330
|
+
} finally {
|
|
7331
|
+
if (mounted.current) setPinningVideoModel(void 0);
|
|
7332
|
+
}
|
|
7333
|
+
};
|
|
7334
|
+
/**
|
|
7335
|
+
* Read the active region's media roster. Read-only and free (the same GET the
|
|
7336
|
+
* image tool performs), so it is safe to run whenever the capability panel is
|
|
7337
|
+
* shown; `refresh` bypasses the Host's 5-minute cache for the explicit
|
|
7338
|
+
* button, because a user who just enabled an image model upstream should not
|
|
7339
|
+
* have to wait out a TTL to see it.
|
|
7340
|
+
*
|
|
7341
|
+
* A failure lands in `mediaError` INSIDE the block and nowhere else: it must
|
|
7342
|
+
* not touch `statusByRegion`, or one unreachable roster would replace the
|
|
7343
|
+
* whole panel with an error screen.
|
|
7344
|
+
*/
|
|
7345
|
+
const loadMediaModels = (0, react.useCallback)(async (region, refresh, signal) => {
|
|
7346
|
+
try {
|
|
7347
|
+
const url = refresh ? `${withWorkBuddyRegion(WORKBUDDY_MEDIA_MODELS_PATH, region)}&refresh=1` : withWorkBuddyRegion(WORKBUDDY_MEDIA_MODELS_PATH, region);
|
|
7348
|
+
const response = await fetch(url, {
|
|
7349
|
+
method: "POST",
|
|
7350
|
+
headers: { accept: "application/json" },
|
|
7351
|
+
credentials: "same-origin",
|
|
7352
|
+
...signal === void 0 ? {} : { signal }
|
|
7353
|
+
});
|
|
7354
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
7355
|
+
const models = readMediaRoster(await response.json().catch(() => void 0));
|
|
7356
|
+
if (models === void 0) throw new Error(`HTTP ${response.status}`);
|
|
7357
|
+
if (!mounted.current || signal?.aborted === true) return;
|
|
7358
|
+
setMediaByRegion((prev) => ({
|
|
7359
|
+
...prev,
|
|
7360
|
+
[region]: models
|
|
7361
|
+
}));
|
|
7362
|
+
setMediaError(void 0);
|
|
7363
|
+
} catch (error) {
|
|
7364
|
+
if (!mounted.current || signal?.aborted === true) return;
|
|
7365
|
+
setMediaError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
7366
|
+
}
|
|
7367
|
+
}, [t]);
|
|
7368
|
+
/**
|
|
7369
|
+
* Regions whose media roster has already been asked for, so showing the
|
|
7370
|
+
* capability panel does not re-read the roster on every render — and so a
|
|
7371
|
+
* FAILED read is not retried in a loop. The explicit "re-read" button is the
|
|
7372
|
+
* only thing that asks twice, because a user who just enabled a model
|
|
7373
|
+
* upstream is the one case where a fresh answer is the point.
|
|
7374
|
+
*/
|
|
7375
|
+
const mediaRequested = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
7376
|
+
/**
|
|
7377
|
+
* Read the media roster once per region, when the capability panel is opened.
|
|
7378
|
+
*
|
|
7379
|
+
* Lazy rather than alongside the usage fetch: the roster is only visible in
|
|
7380
|
+
* this one panel, and the usage fetch runs for BOTH regions on every open. If
|
|
7381
|
+
* the secret store happens to hold a credential for the region the user is not
|
|
7382
|
+
* looking at, an eager read would spend a request nobody asked for.
|
|
7383
|
+
*/
|
|
7384
|
+
(0, react.useEffect)(() => {
|
|
7385
|
+
if (!open || panel !== "capabilities" || !hasAccounts) return;
|
|
7386
|
+
if (mediaRequested.current.has(activeRegion)) return;
|
|
7387
|
+
mediaRequested.current.add(activeRegion);
|
|
7388
|
+
const controller = new AbortController();
|
|
7389
|
+
loadMediaModels(activeRegion, false, controller.signal);
|
|
7390
|
+
return () => {
|
|
7391
|
+
controller.abort();
|
|
7392
|
+
};
|
|
7393
|
+
}, [
|
|
7394
|
+
open,
|
|
7395
|
+
panel,
|
|
7396
|
+
activeRegion,
|
|
7397
|
+
hasAccounts,
|
|
7398
|
+
loadMediaModels
|
|
7399
|
+
]);
|
|
7115
7400
|
const refreshModels = async () => {
|
|
7116
7401
|
setBusy(true);
|
|
7117
7402
|
try {
|
|
@@ -7601,12 +7886,14 @@ window.__ModuleLoader__.load({
|
|
|
7601
7886
|
*
|
|
7602
7887
|
* It sits first because it is the only preference here that authorizes SPEND
|
|
7603
7888
|
* rather than display: the credit line is a readout, this one lets the model
|
|
7604
|
-
* buy an image
|
|
7605
|
-
*
|
|
7606
|
-
*
|
|
7607
|
-
* that can never do anything.
|
|
7889
|
+
* buy an image without asking again. Shown on BOTH tabs: each gateway now
|
|
7890
|
+
* publishes its own `text-to-image` model (CN `hunyuan-image-alpha`,
|
|
7891
|
+
* international `hy-image-3.5`, measured 2026-10-25), so the global box is no
|
|
7892
|
+
* longer a control that can never do anything. The flag is per-region and the
|
|
7893
|
+
* tool uses whichever region is BOTH enabled and switched on (CN first), so
|
|
7894
|
+
* ticking it here only ever authorizes spending on THIS tab.
|
|
7608
7895
|
*/
|
|
7609
|
-
const imageToolPreference =
|
|
7896
|
+
const imageToolPreference = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7610
7897
|
className: "dsm-workbuddy-pref",
|
|
7611
7898
|
title: t("row.imageToolHint"),
|
|
7612
7899
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -7627,7 +7914,158 @@ window.__ModuleLoader__.load({
|
|
|
7627
7914
|
children: t("row.imageToolHint")
|
|
7628
7915
|
})]
|
|
7629
7916
|
})]
|
|
7630
|
-
})
|
|
7917
|
+
});
|
|
7918
|
+
/**
|
|
7919
|
+
* The video-tool opt-in. Same shape and same stakes as imageToolPreference:
|
|
7920
|
+
* per-region, off means the tool is NOT registered at all (not "registered
|
|
7921
|
+
* but refusing"), so a tick here only ever authorizes spending on THIS tab.
|
|
7922
|
+
* The tool is an async task that runs tens of seconds to minutes.
|
|
7923
|
+
*/
|
|
7924
|
+
const videoToolPreference = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7925
|
+
className: "dsm-workbuddy-pref",
|
|
7926
|
+
title: t("row.videoToolHint"),
|
|
7927
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
7928
|
+
type: "checkbox",
|
|
7929
|
+
className: "dsm-workbuddy-video-tool-check",
|
|
7930
|
+
checked: activeRegionOn ? videoToolOn(activeRegion) : false,
|
|
7931
|
+
disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
|
|
7932
|
+
onChange: (event) => {
|
|
7933
|
+
toggleVideoTool(activeRegion, event.target.checked);
|
|
7934
|
+
}
|
|
7935
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7936
|
+
className: "dsm-workbuddy-pref-copy",
|
|
7937
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7938
|
+
className: "dsm-workbuddy-pref-label",
|
|
7939
|
+
children: t("row.videoToolLabel")
|
|
7940
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7941
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7942
|
+
children: t("row.videoToolHint")
|
|
7943
|
+
})]
|
|
7944
|
+
})]
|
|
7945
|
+
});
|
|
7946
|
+
/**
|
|
7947
|
+
* What this region can actually draw. The IMAGE rows stay read-only; the
|
|
7948
|
+
* VIDEO rows are pinnable in `videoModelPreference` below.
|
|
7949
|
+
*
|
|
7950
|
+
* The roster used to be read-only because every tag group held exactly one
|
|
7951
|
+
* model in both regions (measured 2026-10-25), so a dropdown would have been a
|
|
7952
|
+
* choice of one. That stopped being true for VIDEO: CN publishes two models
|
|
7953
|
+
* per video tag (`kling-video-v3` and `minimax-video-h3`) — the very
|
|
7954
|
+
* "两个模型能不能挑" question. The pin therefore lives in its own block below,
|
|
7955
|
+
* and the roster keeps answering "can this tab draw at all, and with what".
|
|
7956
|
+
*
|
|
7957
|
+
* Indented like the position picker, because it is a property of the switch
|
|
7958
|
+
* above it rather than a fourth independent setting.
|
|
7959
|
+
*/
|
|
7960
|
+
const mediaTagLabels = {
|
|
7961
|
+
"text-to-image": t("row.mediaTagTextToImage"),
|
|
7962
|
+
"image-to-image": t("row.mediaTagImageToImage"),
|
|
7963
|
+
"text-to-video": t("row.mediaTagTextToVideo"),
|
|
7964
|
+
"image-to-video": t("row.mediaTagImageToVideo")
|
|
7965
|
+
};
|
|
7966
|
+
const activeMedia = mediaByRegion[activeRegion];
|
|
7967
|
+
const videoModelPreference = !(activeRegionOn && videoToolOn(activeRegion)) || activeMedia === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7968
|
+
className: "dsm-workbuddy-pref dsm-workbuddy-media",
|
|
7969
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7970
|
+
className: "dsm-workbuddy-pref-copy",
|
|
7971
|
+
children: [
|
|
7972
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7973
|
+
className: "dsm-workbuddy-pref-label",
|
|
7974
|
+
children: t("row.videoModelLabel")
|
|
7975
|
+
}),
|
|
7976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7977
|
+
className: "dsm-workbuddy-pref-hint",
|
|
7978
|
+
children: t("row.videoModelHint")
|
|
7979
|
+
}),
|
|
7980
|
+
["text-to-video", "image-to-video"].map((tag) => {
|
|
7981
|
+
const group = activeMedia.filter((model) => model.tags.includes(tag));
|
|
7982
|
+
if (group.length === 0) return null;
|
|
7983
|
+
const pinned = preferredVideoModelOf(activeRegion, tag);
|
|
7984
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7985
|
+
className: "dsm-workbuddy-media-row",
|
|
7986
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7987
|
+
className: "dsm-workbuddy-media-tag",
|
|
7988
|
+
children: mediaTagLabels[tag] ?? tag
|
|
7989
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
7990
|
+
className: "dsm-workbuddy-video-model-select",
|
|
7991
|
+
value: group.some((model) => model.id === pinned) ? pinned : "",
|
|
7992
|
+
disabled: !canWrite || saving || poolBusy || pinningVideoModel !== void 0,
|
|
7993
|
+
onChange: (event) => {
|
|
7994
|
+
const next = event.target.value;
|
|
7995
|
+
pinVideoModel(activeRegion, tag, next === "" ? void 0 : next);
|
|
7996
|
+
},
|
|
7997
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
7998
|
+
value: "",
|
|
7999
|
+
children: t("row.videoModelDefault")
|
|
8000
|
+
}), group.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
8001
|
+
value: model.id,
|
|
8002
|
+
children: model.name
|
|
8003
|
+
}, model.id))]
|
|
8004
|
+
})]
|
|
8005
|
+
}, tag);
|
|
8006
|
+
})
|
|
8007
|
+
]
|
|
8008
|
+
})
|
|
8009
|
+
});
|
|
8010
|
+
const mediaModelsPreference = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8011
|
+
className: "dsm-workbuddy-pref dsm-workbuddy-media",
|
|
8012
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8013
|
+
className: "dsm-workbuddy-pref-copy",
|
|
8014
|
+
children: [
|
|
8015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8016
|
+
className: "dsm-workbuddy-pref-lead",
|
|
8017
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8018
|
+
className: "dsm-workbuddy-pref-label",
|
|
8019
|
+
children: t("row.mediaModelsLabel")
|
|
8020
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8021
|
+
type: "button",
|
|
8022
|
+
className: "dsm-btn dsm-btn-outline",
|
|
8023
|
+
disabled: mediaBusy || !activeRegionOn,
|
|
8024
|
+
onClick: () => {
|
|
8025
|
+
setMediaBusy(true);
|
|
8026
|
+
loadMediaModels(activeRegion, true).finally(() => {
|
|
8027
|
+
if (mounted.current) setMediaBusy(false);
|
|
8028
|
+
});
|
|
8029
|
+
},
|
|
8030
|
+
children: mediaBusy ? t("row.mediaModelsRefreshing") : t("row.mediaModelsRefresh")
|
|
8031
|
+
})]
|
|
8032
|
+
}),
|
|
8033
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8034
|
+
className: "dsm-workbuddy-pref-hint",
|
|
8035
|
+
children: t("row.mediaModelsHint")
|
|
8036
|
+
}),
|
|
8037
|
+
mediaError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8038
|
+
className: "dsm-workbuddy-media-error",
|
|
8039
|
+
children: t("row.mediaModelsError", { message: mediaError })
|
|
8040
|
+
}) : activeMedia === void 0 ? null : activeMedia.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8041
|
+
className: "dsm-workbuddy-media-empty",
|
|
8042
|
+
children: t("row.mediaModelsEmpty")
|
|
8043
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
8044
|
+
className: "dsm-workbuddy-media-list",
|
|
8045
|
+
children: activeMedia.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
8046
|
+
className: "dsm-workbuddy-media-row",
|
|
8047
|
+
children: [
|
|
8048
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8049
|
+
className: "dsm-workbuddy-media-name",
|
|
8050
|
+
children: model.name
|
|
8051
|
+
}),
|
|
8052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8053
|
+
className: "dsm-workbuddy-media-id",
|
|
8054
|
+
children: model.id
|
|
8055
|
+
}),
|
|
8056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8057
|
+
className: "dsm-workbuddy-media-tags",
|
|
8058
|
+
children: model.tags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8059
|
+
className: "dsm-workbuddy-media-tag",
|
|
8060
|
+
children: mediaTagLabels[tag] ?? tag
|
|
8061
|
+
}, tag))
|
|
8062
|
+
})
|
|
8063
|
+
]
|
|
8064
|
+
}, model.id))
|
|
8065
|
+
})
|
|
8066
|
+
]
|
|
8067
|
+
})
|
|
8068
|
+
});
|
|
7631
8069
|
/**
|
|
7632
8070
|
* The CLI-route switch.
|
|
7633
8071
|
*
|
|
@@ -7870,6 +8308,9 @@ window.__ModuleLoader__.load({
|
|
|
7870
8308
|
cliBridgePreference,
|
|
7871
8309
|
cliBridgeSwitchedOn() ? cliBridgePickers : null,
|
|
7872
8310
|
imageToolPreference,
|
|
8311
|
+
videoToolPreference,
|
|
8312
|
+
videoModelPreference,
|
|
8313
|
+
mediaModelsPreference,
|
|
7873
8314
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
7874
8315
|
className: "dsm-workbuddy-pref",
|
|
7875
8316
|
title: activeRegion === "cn" ? t("row.showCreditsCnHint") : t("row.showCreditsGlobalHint"),
|
|
@@ -10616,7 +11057,22 @@ window.__ModuleLoader__.load({
|
|
|
10616
11057
|
"row.cliBridgeRegionOrderGlobalOnly": "Global only",
|
|
10617
11058
|
"row.cliBridgePickersHint": "All three pickers write the global cliBridge block; \"CLI default\" on model and effort is an explicit empty value, not \"unset\" — the CLI then uses its own defaults; the region picker writes cliBridge.regionOrder.",
|
|
10618
11059
|
"row.imageToolLabel": "Let the model draw images",
|
|
10619
|
-
"row.imageToolHint": "Adds the workbuddy_draw_image tool, which the MODEL may call on its own — about 5.71 credits per image, with no per-call confirmation. Off means the tool is not registered at all.",
|
|
11060
|
+
"row.imageToolHint": "Adds the workbuddy_draw_image tool, which the MODEL may call on its own — about 5.71 credits per image on the CN gateway and about 0.03 on the international one, with no per-call confirmation. Ticking it here only authorizes THIS tab's region. Off everywhere means the tool is not registered at all.",
|
|
11061
|
+
"row.videoToolLabel": "Let the model generate videos",
|
|
11062
|
+
"row.videoToolHint": "Adds the workbuddy_draw_video tool, which the MODEL may call on its own — a video is an async task (submit, poll, then download) that costs WorkBuddy credits and runs tens of seconds to minutes, with no per-call confirmation. Ticking it here only authorizes THIS tab's region. Off everywhere means the tool is not registered at all.",
|
|
11063
|
+
"row.videoModelLabel": "Video model per mode",
|
|
11064
|
+
"row.videoModelHint": "Pinned separately for text-to-video and image-to-video, from THIS region's own catalog — a model id does not carry across regions. A call that names no model uses the pin; with nothing pinned the tool takes the first model of that group. A pin the catalog no longer carries quietly falls back to the first model instead of failing.",
|
|
11065
|
+
"row.videoModelDefault": "Default (first in the group)",
|
|
11066
|
+
"row.mediaModelsLabel": "Image models in this region",
|
|
11067
|
+
"row.mediaModelsHint": "Read from this account's product configuration. The draw tools pick their models from here: a prompt without an image uses a text-to-image model, one WITH an image uses an image-to-image model. Image rows are read-only (pass a model to the tool to change it); the VIDEO model is pinnable in the picker above.",
|
|
11068
|
+
"row.mediaModelsEmpty": "This region publishes no image or video model.",
|
|
11069
|
+
"row.mediaModelsError": "Could not read the media model list: {message}",
|
|
11070
|
+
"row.mediaModelsRefresh": "Re-read",
|
|
11071
|
+
"row.mediaModelsRefreshing": "Reading…",
|
|
11072
|
+
"row.mediaTagTextToImage": "text → image",
|
|
11073
|
+
"row.mediaTagImageToImage": "image → image",
|
|
11074
|
+
"row.mediaTagTextToVideo": "text → video",
|
|
11075
|
+
"row.mediaTagImageToVideo": "image → video",
|
|
10620
11076
|
"row.showCreditsCn": "Show credits in the composer toolbar",
|
|
10621
11077
|
"row.showCreditsGlobal": "Show credits in the composer toolbar",
|
|
10622
11078
|
"row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a one-click test of the checked accounts (the test spends real credits). The readout refreshes by itself every 5 minutes; the per-account table every 60 seconds while it is open.",
|
|
@@ -11031,7 +11487,22 @@ window.__ModuleLoader__.load({
|
|
|
11031
11487
|
"row.cliBridgeRegionOrderGlobalOnly": "仅国际",
|
|
11032
11488
|
"row.cliBridgePickersHint": "三个选择都写入全局 cliBridge 块;模型/档位的\"CLI 默认\"是显式的空值而非未设置,届时 CLI 使用自己的默认;区域选择写入 cliBridge.regionOrder。",
|
|
11033
11489
|
"row.imageToolLabel": "允许模型自主生图",
|
|
11034
|
-
"row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image
|
|
11490
|
+
"row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——国内区约 5.71、国际区约 0.03 积分/张,没有逐次确认。在这里勾选只授权当前这一区的账号。两区都关着时该工具根本不会注册。",
|
|
11491
|
+
"row.videoToolLabel": "允许模型自主生视频",
|
|
11492
|
+
"row.videoToolHint": "开启后模型可以自己调用 workbuddy_draw_video 生成短视频——视频是异步任务(提交、轮询到完成、再下载到本地),要几十秒到几分钟,没有逐次确认。在这里勾选只授权当前这一区的账号。两区都关着时该工具根本不会注册。",
|
|
11493
|
+
"row.videoModelLabel": "按模式指定视频模型",
|
|
11494
|
+
"row.videoModelHint": "文生视频与图生视频各钉一个,选项只来自当前这一区自己的目录——模型 id 不跨区。调用时不填 model 就用这里钉的;什么都没钉就用该组第一个模型。钉住的模型如果目录里已经没有了,会安静地回落到第一个,而不是让这次生成失败。",
|
|
11495
|
+
"row.videoModelDefault": "默认(组内第一个)",
|
|
11496
|
+
"row.mediaModelsLabel": "本区可用的图像模型",
|
|
11497
|
+
"row.mediaModelsHint": "读自当前账号的产品配置。生图工具就按这里挑模型:不填图片走「文生图」那一组,填了图片走「图生图」那一组。「图像」那几行是只读的(想换就在调用工具时指定模型);「视频」模型可以在上面的选择器里钉住。",
|
|
11498
|
+
"row.mediaModelsEmpty": "这一区的产品配置里没有图像或视频模型。",
|
|
11499
|
+
"row.mediaModelsError": "读取媒体模型清单失败:{message}",
|
|
11500
|
+
"row.mediaModelsRefresh": "重新读取",
|
|
11501
|
+
"row.mediaModelsRefreshing": "正在读取…",
|
|
11502
|
+
"row.mediaTagTextToImage": "文生图",
|
|
11503
|
+
"row.mediaTagImageToImage": "图生图",
|
|
11504
|
+
"row.mediaTagTextToVideo": "文生视频",
|
|
11505
|
+
"row.mediaTagImageToVideo": "图生视频",
|
|
11035
11506
|
"row.showCreditsCn": "在输入框工具栏显示积分读数",
|
|
11036
11507
|
"row.showCreditsGlobal": "在输入框工具栏显示积分读数",
|
|
11037
11508
|
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并一键测试勾选的账号(测试会真实消耗积分)。读数每 5 分钟自动刷新;各账号明细在面板打开时每 60 秒刷新。",
|