dsh-connect-workbuddy 4.5.1 → 4.6.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 +96 -0
- package/docs/assets/quota-notice-inline-mock.html +96 -0
- package/docs/assets/quota-notice-inline-mock.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +436 -158
- package/lib/{host-heartbeat-Vd2tNdsv.js → host-heartbeat-BxBvvAZq.js} +1 -1
- package/lib/index.d.ts +31 -12
- package/lib/index.js +182 -88
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -692,8 +692,6 @@ window.__ModuleLoader__.load({
|
|
|
692
692
|
targetModelId: preferences.targetModelId,
|
|
693
693
|
memberAccountIds: preferences.memberAccountIds
|
|
694
694
|
};
|
|
695
|
-
const overrides = Object.entries(preferences.targetModelIdByAccount ?? {}).filter(([, modelId]) => modelId !== "");
|
|
696
|
-
if (overrides.length > 0) slot["targetModelIdByAccount"] = Object.fromEntries(overrides);
|
|
697
695
|
if (preferences.probeInputTokens !== void 0) slot["probeInputTokens"] = preferences.probeInputTokens;
|
|
698
696
|
const next = nextRegionPool(scope.getSnapshot().value, region, slot)[region];
|
|
699
697
|
await writeField(scope, "regions", region, next, (readBack) => {
|
|
@@ -706,11 +704,7 @@ window.__ModuleLoader__.load({
|
|
|
706
704
|
if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
|
|
707
705
|
const wanted = new Set(preferences.memberAccountIds);
|
|
708
706
|
if (!stored.every((id) => typeof id === "string" && wanted.has(id))) return false;
|
|
709
|
-
|
|
710
|
-
if (overrides.length === 0) return storedModels === void 0 || typeof storedModels === "object" && storedModels !== null && !Array.isArray(storedModels) && Object.keys(storedModels).length === 0;
|
|
711
|
-
if (typeof storedModels !== "object" || storedModels === null || Array.isArray(storedModels)) return false;
|
|
712
|
-
const storedMap = storedModels;
|
|
713
|
-
return Object.keys(storedMap).length === overrides.length && overrides.every(([accountId, modelId]) => storedMap[accountId] === modelId);
|
|
707
|
+
return true;
|
|
714
708
|
});
|
|
715
709
|
}
|
|
716
710
|
//#endregion
|
|
@@ -987,6 +981,17 @@ window.__ModuleLoader__.load({
|
|
|
987
981
|
5e4,
|
|
988
982
|
1e5
|
|
989
983
|
];
|
|
984
|
+
/**
|
|
985
|
+
* The `<timestamp> UTC±N` core of the upstream's reset sentence.
|
|
986
|
+
*
|
|
987
|
+
* Shared by both spellings below so the two can never drift into accepting
|
|
988
|
+
* different timestamps: the offset handling and the calendar round-trip check in
|
|
989
|
+
* {@link parseUpstreamResetAt} are what make a parsed value trustworthy, and two
|
|
990
|
+
* copies of this shape would be two chances to loosen one of them.
|
|
991
|
+
*/
|
|
992
|
+
const RESET_STAMP = String.raw`(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})\s*UTC\s*([+-])(\d{1,2})(?::?(\d{2}))?`;
|
|
993
|
+
new RegExp(`将在?\\s*${RESET_STAMP}[\\s\\S]*?重置`, "u");
|
|
994
|
+
new RegExp(`(?:reset|resets|refresh|refreshes)\\s+(?:at|on)\\s+${RESET_STAMP}`, "iu");
|
|
990
995
|
//#endregion
|
|
991
996
|
//#region src/client/remaining.ts
|
|
992
997
|
const MINUTE_MS = 6e4;
|
|
@@ -1250,6 +1255,43 @@ window.__ModuleLoader__.load({
|
|
|
1250
1255
|
const wanted = /* @__PURE__ */ new Set([...effectiveMembers, ...draftMembers]);
|
|
1251
1256
|
return accounts.filter((account) => wanted.has(account.accountId) && account.excludedBy === void 0).map((account) => account.accountId);
|
|
1252
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Whether EVERY member is blocked by the same MODEL — issue #64's pool-level
|
|
1260
|
+
* verdict.
|
|
1261
|
+
*
|
|
1262
|
+
* The reporter's case: four members, all measured `model-busy`, and the card
|
|
1263
|
+
* only showed four rows each saying "模型繁忙". Summed up by the reader that
|
|
1264
|
+
* reads as "my pool is broken", and the natural next move — switch accounts —
|
|
1265
|
+
* is the one action that cannot help, because the limit follows the model. The
|
|
1266
|
+
* per-row labels are each true and the aggregate conclusion is false, which is
|
|
1267
|
+
* why this needs its own decision rather than another row label.
|
|
1268
|
+
*
|
|
1269
|
+
* The conditions, and where each is enforced — NOT just "some member is
|
|
1270
|
+
* model-busy":
|
|
1271
|
+
*
|
|
1272
|
+
* - **at least one member.** Enforced by the length check: an empty pool has
|
|
1273
|
+
* no model-wide story to tell, and the plain "no usable account" sentence is
|
|
1274
|
+
* the honest one there (nothing is checked).
|
|
1275
|
+
* - **no member usable, and every blocked member's reason IS `model-busy`.**
|
|
1276
|
+
* Both fall out of the single `every` predicate, because a usable member has
|
|
1277
|
+
* `excludedBy === undefined`. See the note at the return statement: they are
|
|
1278
|
+
* genuinely one condition in code, and writing them as two produced an
|
|
1279
|
+
* unreachable branch that no test could hold.
|
|
1280
|
+
*
|
|
1281
|
+
* A pool where one account is rate-limited and the rest are model-busy is NOT a
|
|
1282
|
+
* model verdict: the rate limit really is about that account, and saying "it is
|
|
1283
|
+
* the model, not your accounts" would be the same class of false comfort this
|
|
1284
|
+
* helper exists to remove.
|
|
1285
|
+
*
|
|
1286
|
+
* Returned as a boolean rather than a prebuilt sentence so the caller keeps
|
|
1287
|
+
* every user-visible string in the locale table.
|
|
1288
|
+
*/
|
|
1289
|
+
function allMembersBlockedByModel(accounts, effectiveMembers, draftMembers) {
|
|
1290
|
+
const wanted = /* @__PURE__ */ new Set([...effectiveMembers, ...draftMembers]);
|
|
1291
|
+
const members = accounts.filter((account) => wanted.has(account.accountId));
|
|
1292
|
+
if (members.length === 0) return false;
|
|
1293
|
+
return members.every((member) => member.excludedBy === "model-busy");
|
|
1294
|
+
}
|
|
1253
1295
|
//#endregion
|
|
1254
1296
|
//#region src/client/AccountPool.tsx
|
|
1255
1297
|
/**
|
|
@@ -1297,21 +1339,6 @@ window.__ModuleLoader__.load({
|
|
|
1297
1339
|
const wanted = new Set(right);
|
|
1298
1340
|
return left.every((id) => wanted.has(id));
|
|
1299
1341
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Whether two per-account test-model maps are equal (issue #61).
|
|
1302
|
-
*
|
|
1303
|
-
* By CONTENT, not by size: moving an account from one model to another keeps the
|
|
1304
|
-
* count and changes the meaning, and clearing an entry is a change a size
|
|
1305
|
-
* comparison would miss entirely when another account was added at the same
|
|
1306
|
-
* time. Order is irrelevant for the same reason it is in {@link sameIds} — the
|
|
1307
|
-
* map is written by key, not by click sequence.
|
|
1308
|
-
*/
|
|
1309
|
-
function sameModelOverrides(left, right) {
|
|
1310
|
-
const leftKeys = Object.keys(left);
|
|
1311
|
-
const rightKeys = Object.keys(right);
|
|
1312
|
-
if (leftKeys.length !== rightKeys.length) return false;
|
|
1313
|
-
return leftKeys.every((key) => left[key] === right[key]);
|
|
1314
|
-
}
|
|
1315
1342
|
/** Whether a probe outcome means the model answered. */
|
|
1316
1343
|
function outcomeOk(outcome) {
|
|
1317
1344
|
return outcome === "ok";
|
|
@@ -1398,8 +1425,7 @@ window.__ModuleLoader__.load({
|
|
|
1398
1425
|
enabled: pool?.enabled ?? false,
|
|
1399
1426
|
targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
|
|
1400
1427
|
probeInputTokens: pool?.probeInputTokens ?? 0,
|
|
1401
|
-
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1402
|
-
targetModelIdByAccount: { ...pool?.targetModelIdByAccount }
|
|
1428
|
+
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1403
1429
|
};
|
|
1404
1430
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1405
1431
|
/**
|
|
@@ -1420,7 +1446,7 @@ window.__ModuleLoader__.load({
|
|
|
1420
1446
|
* the pending edit.
|
|
1421
1447
|
*/
|
|
1422
1448
|
const poolRunning = saved.enabled;
|
|
1423
|
-
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !
|
|
1449
|
+
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1424
1450
|
/**
|
|
1425
1451
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
1426
1452
|
*
|
|
@@ -1581,25 +1607,6 @@ window.__ModuleLoader__.load({
|
|
|
1581
1607
|
};
|
|
1582
1608
|
});
|
|
1583
1609
|
}, [editDraft]);
|
|
1584
|
-
/**
|
|
1585
|
-
* Set ONE account's test model (issue #61).
|
|
1586
|
-
*
|
|
1587
|
-
* `''` REMOVES the entry rather than storing an empty string: "follow the
|
|
1588
|
-
* region target" is the absence of a choice, which is the same shape the host
|
|
1589
|
-
* reads and the same shape an untouched profile has — so nothing about an
|
|
1590
|
-
* older profile changes when this setting is never used.
|
|
1591
|
-
*/
|
|
1592
|
-
const setRowModel = (0, react.useCallback)((accountId, modelId) => {
|
|
1593
|
-
editDraft((current) => {
|
|
1594
|
-
const next = { ...current.targetModelIdByAccount };
|
|
1595
|
-
if (modelId === "") delete next[accountId];
|
|
1596
|
-
else next[accountId] = modelId;
|
|
1597
|
-
return {
|
|
1598
|
-
...current,
|
|
1599
|
-
targetModelIdByAccount: next
|
|
1600
|
-
};
|
|
1601
|
-
});
|
|
1602
|
-
}, [editDraft]);
|
|
1603
1610
|
/** Check every listed account, or none of them. */
|
|
1604
1611
|
const selectAll = (0, react.useCallback)((next) => {
|
|
1605
1612
|
editDraft((current) => ({
|
|
@@ -1889,7 +1896,10 @@ window.__ModuleLoader__.load({
|
|
|
1889
1896
|
className: "dsm-btn dsm-btn-outline",
|
|
1890
1897
|
disabled: rescanning === true,
|
|
1891
1898
|
onClick: onRescan
|
|
1892
|
-
}, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !poolRunning ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolEnabledHint")) : null, poolRunning && usable.length === 0 ? (0, react.createElement)("p", {
|
|
1899
|
+
}, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !poolRunning ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolEnabledHint")) : null, poolRunning && usable.length === 0 ? allMembersBlockedByModel(pool?.accounts ?? [], effectiveMembers, active.memberAccountIds) ? (0, react.createElement)("p", {
|
|
1900
|
+
className: "dsm-workbuddy-pool-warn",
|
|
1901
|
+
role: "status"
|
|
1902
|
+
}, `${t("row.poolModelWideBusy")} ${t("row.poolModelWideBusyHint")}`) : (0, react.createElement)("p", {
|
|
1893
1903
|
className: "dsm-workbuddy-pool-warn",
|
|
1894
1904
|
role: "status"
|
|
1895
1905
|
}, `${t("row.poolNoCandidate")} ${t("row.poolNoCandidateHint")}`) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-head" }, (0, react.createElement)("div", null, (0, react.createElement)("h4", { className: "dsm-workbuddy-pool-members-title" }, t("row.poolMembersTitle")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolMembersHint"))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-actions" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolSelectedCount", {
|
|
@@ -1920,10 +1930,7 @@ window.__ModuleLoader__.load({
|
|
|
1920
1930
|
},
|
|
1921
1931
|
onRemove: () => {
|
|
1922
1932
|
runRowRemove(account.accountId, account.accountName);
|
|
1923
|
-
}
|
|
1924
|
-
modelOverride: active.targetModelIdByAccount[account.accountId] ?? "",
|
|
1925
|
-
catalog: pool.catalog ?? [],
|
|
1926
|
-
onModelChange: (modelId) => setRowModel(account.accountId, modelId)
|
|
1933
|
+
}
|
|
1927
1934
|
}))), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
|
|
1928
1935
|
className: "dsm-workbuddy-pool-warn",
|
|
1929
1936
|
role: "status"
|
|
@@ -1991,7 +1998,6 @@ window.__ModuleLoader__.load({
|
|
|
1991
1998
|
/** One account's row. */
|
|
1992
1999
|
function renderAccountRow(input) {
|
|
1993
2000
|
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove } = input;
|
|
1994
|
-
const { modelOverride, catalog, onModelChange } = input;
|
|
1995
2001
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
1996
2002
|
const excluded = exclusionText(t, account.excludedBy);
|
|
1997
2003
|
const probe = account.probe;
|
|
@@ -2032,20 +2038,7 @@ window.__ModuleLoader__.load({
|
|
|
2032
2038
|
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age), probe === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-model" }, probe.modelId === void 0 ? t("row.poolProbeModelUnknown") : t("row.poolProbeModel", { model: probe.modelId })))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
|
|
2033
2039
|
className: "dsm-workbuddy-pool-unknown",
|
|
2034
2040
|
title: t("row.poolNeverTested")
|
|
2035
|
-
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("
|
|
2036
|
-
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-row-model",
|
|
2037
|
-
value: modelOverride,
|
|
2038
|
-
disabled: !canEdit,
|
|
2039
|
-
title: t("row.poolRowModelHint"),
|
|
2040
|
-
"aria-label": t("row.poolRowModelAria", { accountName: name }),
|
|
2041
|
-
onChange: (event) => onModelChange(event.currentTarget.value)
|
|
2042
|
-
}, (0, react.createElement)("option", { value: "" }, t("row.poolRowModelInherit")), modelOverride !== "" && !catalog.some((model) => model.id === modelOverride) ? (0, react.createElement)("option", {
|
|
2043
|
-
value: modelOverride,
|
|
2044
|
-
disabled: true
|
|
2045
|
-
}, t("row.poolTargetStaleOption", { model: modelOverride })) : null, ...catalog.map((model) => (0, react.createElement)("option", {
|
|
2046
|
-
value: model.id,
|
|
2047
|
-
key: model.id
|
|
2048
|
-
}, model.name))), (0, react.createElement)("button", {
|
|
2041
|
+
}, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("button", {
|
|
2049
2042
|
type: "button",
|
|
2050
2043
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
2051
2044
|
disabled: busy || testDisabled,
|
|
@@ -3081,13 +3074,16 @@ window.__ModuleLoader__.load({
|
|
|
3081
3074
|
/* Its OWN label class, deliberately not the switch's: a query for "the
|
|
3082
3075
|
sidebar-credit switch" must not be able to pick this control up, and the two
|
|
3083
3076
|
are different widgets (a checkbox and a two-way picker). */
|
|
3084
|
-
/* The supplement to the host's fixed quota sentence (issue #63).
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
.
|
|
3089
|
-
|
|
3090
|
-
.
|
|
3077
|
+
/* The supplement to the host's fixed quota sentence (issue #63). The plugin no
|
|
3078
|
+
longer claims the host's shell.quota-notice chain seat, so this no longer
|
|
3079
|
+
paints into shell.overlay — that seat is the reason the previous build's
|
|
3080
|
+
inline span parked at the overlay's TOP-LEFT as a box the maintainer had
|
|
3081
|
+
never seen before. It is kept inline and box-free for the surface that
|
|
3082
|
+
renders it INSIDE a line of conversation: the moment it paints a box it stops
|
|
3083
|
+
being a parenthetical and becomes the dialog that was already rejected once.
|
|
3084
|
+
The body is one step quieter than the text it follows. */
|
|
3085
|
+
.dsm-workbuddy-quota-notice{display:inline}
|
|
3086
|
+
.dsm-workbuddy-quota-notice-body{color:var(--dsw-alias-label-tertiary,#8b8f98);font-size:12px}
|
|
3091
3087
|
.dsm-workbuddy-credit-position-switch{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3092
3088
|
.dsm-workbuddy-credit-position{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3093
3089
|
.dsm-workbuddy-credits-switch-row .dsm-workbuddy-credit-position-switch{margin-left:12px}
|
|
@@ -3160,7 +3156,16 @@ window.__ModuleLoader__.load({
|
|
|
3160
3156
|
meta line, because a cooldown sentence ("rate limited - the upstream gave no
|
|
3161
3157
|
time") does not fit next to the context/output chips, and truncating it would
|
|
3162
3158
|
hide exactly the part the user needs. */
|
|
3163
|
-
|
|
3159
|
+
/* Three buttons now live here (refresh + the two sweeps), and the sweep labels are
|
|
3160
|
+
long in Chinese (一键测试所有模型). flex:none alone let the group run past the
|
|
3161
|
+
header on a narrow card, so the group may WRAP and each label refuses to break
|
|
3162
|
+
mid-word — the same failure the pool row's action buttons had. */
|
|
3163
|
+
.dsm-workbuddy-models-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
|
|
3164
|
+
.dsm-workbuddy-models-head-actions .dsm-btn{white-space:nowrap}
|
|
3165
|
+
/* The sweep buttons keep the outline weight of the refresh button beside them, so
|
|
3166
|
+
the header reads as one group of equal actions. */
|
|
3167
|
+
.dsm-workbuddy-models-sweep{white-space:nowrap}
|
|
3168
|
+
.dsm-workbuddy-models-sweep-summary{margin:6px 0 0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3164
3169
|
.dsm-workbuddy-model-probe{flex:none;padding:3px 10px;font-size:11px;line-height:16px}
|
|
3165
3170
|
.dsm-workbuddy-model-probe-result{margin:0;font-size:12px;line-height:18px;word-break:break-word}
|
|
3166
3171
|
.dsm-workbuddy-model-probe-result-ok{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
@@ -3301,11 +3306,16 @@ window.__ModuleLoader__.load({
|
|
|
3301
3306
|
.dsm-workbuddy-pool-log-info{color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3302
3307
|
.dsm-workbuddy-pool-log-empty{padding:14px;font-size:12px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
3303
3308
|
/* Per-row actions (test / remove), right side of each pool row. */
|
|
3309
|
+
/* Per-row actions (test / remove), right side of each pool row.
|
|
3310
|
+
The nowrap is NOT cosmetic: a two-character label (测试) belongs on one line,
|
|
3311
|
+
and this row's track is a fraction of the table — in a narrow window it let
|
|
3312
|
+
the label break into 测 / 试. Clipping is the right failure for a button
|
|
3313
|
+
whose label is fixed and tiny. */
|
|
3304
3314
|
.dsm-workbuddy-pool-row-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end;flex:none}
|
|
3315
|
+
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{white-space:nowrap}
|
|
3305
3316
|
.dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{padding:2px 10px;font-size:12px}
|
|
3306
3317
|
/* This account's own test model (issue #61). Narrower than the section-level
|
|
3307
3318
|
picker so a long model name cannot push the two buttons out of the row. */
|
|
3308
|
-
.dsm-workbuddy-pool-row-model{max-width:150px;padding:2px 6px;font-size:12px}
|
|
3309
3319
|
@media (max-width:760px){.dsm-workbuddy-pool-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-workbuddy-pool-select{max-width:180px}}
|
|
3310
3320
|
/* Pool switch, namespaced under .dsm-workbuddy-pool so it cannot collide with host styles. */
|
|
3311
3321
|
.dsm-workbuddy-pool .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
|
|
@@ -3939,6 +3949,19 @@ window.__ModuleLoader__.load({
|
|
|
3939
3949
|
const [probing, setProbing] = (0, react.useState)({});
|
|
3940
3950
|
/** Probe failure that is not attributable to one model (a dead route, say). */
|
|
3941
3951
|
const [probeError, setProbeError] = (0, react.useState)(void 0);
|
|
3952
|
+
/**
|
|
3953
|
+
* The active region's probe results, mirrored into a ref.
|
|
3954
|
+
*
|
|
3955
|
+
* The sweep loop reads each verdict as soon as it lands, and state read inside
|
|
3956
|
+
* an async loop is the value captured when the loop STARTED — so counting from
|
|
3957
|
+
* `probes` would tally the previous run's answers. The ref is updated in the
|
|
3958
|
+
* same place the state is, so the two cannot drift.
|
|
3959
|
+
*/
|
|
3960
|
+
const probesRef = (0, react.useRef)({});
|
|
3961
|
+
/** Progress of an in-flight sweep, or undefined when none is running. */
|
|
3962
|
+
const [sweeping, setSweeping] = (0, react.useState)(void 0);
|
|
3963
|
+
/** The last sweep's tally, cleared when the next one starts. */
|
|
3964
|
+
const [sweepSummary, setSweepSummary] = (0, react.useState)(void 0);
|
|
3942
3965
|
const mounted = (0, react.useRef)(true);
|
|
3943
3966
|
(0, react.useEffect)(() => {
|
|
3944
3967
|
mounted.current = true;
|
|
@@ -4367,13 +4390,16 @@ window.__ModuleLoader__.load({
|
|
|
4367
4390
|
});
|
|
4368
4391
|
};
|
|
4369
4392
|
/**
|
|
4370
|
-
* Probe
|
|
4393
|
+
* Probe ONE model (the route refuses more than one — see `web-status.ts`,
|
|
4394
|
+
* where the cap is the gate that keeps a whole-roster sweep from becoming a
|
|
4395
|
+
* single unaccounted spend). The HOST owns the request: the browser half sends
|
|
4396
|
+
* only a model id and gets an outcome back, because the credential must never
|
|
4397
|
+
* cross to the page.
|
|
4371
4398
|
*
|
|
4372
|
-
* The
|
|
4373
|
-
*
|
|
4374
|
-
*
|
|
4375
|
-
*
|
|
4376
|
-
* race the shared `probing` map and make the batch button's state unknowable.
|
|
4399
|
+
* The plural name is historical: this used to post a whole list in one request
|
|
4400
|
+
* before 3.0.0 removed the batch button. It is kept because callers that
|
|
4401
|
+
* already hold a list (the two sweep buttons below) can still express "test
|
|
4402
|
+
* these" without a second code path — it just sends them one at a time.
|
|
4377
4403
|
*
|
|
4378
4404
|
* Probing is deliberately independent of `dirty`: a test answers "does this
|
|
4379
4405
|
* model work right now", which is true of the SAVED selection and the draft
|
|
@@ -4401,9 +4427,12 @@ window.__ModuleLoader__.load({
|
|
|
4401
4427
|
const body = await response.json().catch(() => void 0);
|
|
4402
4428
|
if (!response.ok || !Array.isArray(body?.results)) throw new Error(body?.error ?? `HTTP ${String(response.status)}`);
|
|
4403
4429
|
if (!mounted.current) return;
|
|
4430
|
+
const landedResults = body.results;
|
|
4431
|
+
probesRef.current = { ...probesRef.current };
|
|
4432
|
+
for (const result of landedResults) probesRef.current[result.modelId] = result;
|
|
4404
4433
|
setProbes((previous) => {
|
|
4405
4434
|
const regionResults = { ...previous[activeRegion] ?? {} };
|
|
4406
|
-
for (const result of
|
|
4435
|
+
for (const result of landedResults) regionResults[result.modelId] = result;
|
|
4407
4436
|
return {
|
|
4408
4437
|
...previous,
|
|
4409
4438
|
[activeRegion]: regionResults
|
|
@@ -4419,6 +4448,86 @@ window.__ModuleLoader__.load({
|
|
|
4419
4448
|
});
|
|
4420
4449
|
}
|
|
4421
4450
|
};
|
|
4451
|
+
/**
|
|
4452
|
+
* ONE-CLICK availability sweep over several models (issue #61 follow-up, by
|
|
4453
|
+
* request of the maintainer after comparing with the sibling `trae` plugin).
|
|
4454
|
+
*
|
|
4455
|
+
* WHY IT IS A CLIENT-SIDE LOOP. The route accepts exactly ONE model id and
|
|
4456
|
+
* refuses more, and that cap is deliberate: it is what stops a stale bundle or
|
|
4457
|
+
* a hand-rolled client from turning one click into an unaccounted spend over a
|
|
4458
|
+
* whole roster (3.0.0's reason for deleting the original batch button). A
|
|
4459
|
+
* sweep is therefore expressed the way trae expresses it — one model per
|
|
4460
|
+
* request, from here — which keeps that gate intact and has a second benefit
|
|
4461
|
+
* the host-side batch never had: each row's verdict appears the moment it is
|
|
4462
|
+
* known, instead of the card sitting blank until the slowest model answers.
|
|
4463
|
+
*
|
|
4464
|
+
* COST is stated in the button's own hint rather than in a confirmation
|
|
4465
|
+
* dialog: a probe sends a real-volume request, so a sweep over a whole catalog
|
|
4466
|
+
* spends real credit (measured 0.01 per probe at the current default size, on
|
|
4467
|
+
* this machine — see the hint text). The user asked for the hint form, and it
|
|
4468
|
+
* matches the sibling plugin.
|
|
4469
|
+
*
|
|
4470
|
+
* SEQUENTIAL, not parallel: a probe exists to discover rate limits, and firing
|
|
4471
|
+
* a roster's worth of requests at once is the surest way to create one — the
|
|
4472
|
+
* same reason the host's own loop was serial.
|
|
4473
|
+
*
|
|
4474
|
+
* STOPS ON THE FIRST AUTH FAILURE: a rejected credential answers identically
|
|
4475
|
+
* for every remaining model, so continuing would only spend credit to collect
|
|
4476
|
+
* the same answer again. Other failures (a rate limit on one model) do NOT
|
|
4477
|
+
* stop the sweep — that is information about that model, and stopping would
|
|
4478
|
+
* hide it.
|
|
4479
|
+
*
|
|
4480
|
+
* The summary counts what the run actually established, so a stopped or
|
|
4481
|
+
* failed sweep cannot read as a complete green roster.
|
|
4482
|
+
*/
|
|
4483
|
+
const sweepModels = async (modelIds) => {
|
|
4484
|
+
if (modelIds.length === 0) return;
|
|
4485
|
+
setProbeError(void 0);
|
|
4486
|
+
setSweepSummary(void 0);
|
|
4487
|
+
setSweeping({
|
|
4488
|
+
done: 0,
|
|
4489
|
+
total: modelIds.length
|
|
4490
|
+
});
|
|
4491
|
+
let available = 0;
|
|
4492
|
+
let measured = 0;
|
|
4493
|
+
try {
|
|
4494
|
+
for (const [index, modelId] of modelIds.entries()) {
|
|
4495
|
+
if (!mounted.current) return;
|
|
4496
|
+
await probeModels([modelId]);
|
|
4497
|
+
const result = probesRef.current[modelId];
|
|
4498
|
+
measured += 1;
|
|
4499
|
+
if (result?.outcome === "ok") available += 1;
|
|
4500
|
+
if (result?.outcome === "credential-rejected") {
|
|
4501
|
+
if (mounted.current) setProbeError(t("row.modelsTestAuth"));
|
|
4502
|
+
break;
|
|
4503
|
+
}
|
|
4504
|
+
if (mounted.current) setSweeping({
|
|
4505
|
+
done: index + 1,
|
|
4506
|
+
total: modelIds.length
|
|
4507
|
+
});
|
|
4508
|
+
}
|
|
4509
|
+
if (mounted.current) setSweepSummary(t("row.modelsTestDone", {
|
|
4510
|
+
available,
|
|
4511
|
+
total: measured
|
|
4512
|
+
}));
|
|
4513
|
+
} finally {
|
|
4514
|
+
if (mounted.current) setSweeping(void 0);
|
|
4515
|
+
}
|
|
4516
|
+
};
|
|
4517
|
+
/** Sweep the models this region has ENABLED (the current draft's selection). */
|
|
4518
|
+
const sweepEnabledModels = () => {
|
|
4519
|
+
sweepModels(visibleModels.filter((model) => activeEnabledIds.has(model.id)).map((model) => model.id));
|
|
4520
|
+
};
|
|
4521
|
+
/**
|
|
4522
|
+
* Sweep EVERY model in the region's live catalog, enabled or not.
|
|
4523
|
+
*
|
|
4524
|
+
* Deliberately the whole VISIBLE catalog and nothing else: those are the ids
|
|
4525
|
+
* the host can validate against its own catalog, so this cannot spend credit
|
|
4526
|
+
* on an id that only exists in a config file.
|
|
4527
|
+
*/
|
|
4528
|
+
const sweepAllModels = () => {
|
|
4529
|
+
sweepModels(visibleModels.map((model) => model.id));
|
|
4530
|
+
};
|
|
4422
4531
|
const saveModels = async () => {
|
|
4423
4532
|
if (settingsScope === void 0) return;
|
|
4424
4533
|
if (status.status !== "signed-in") return;
|
|
@@ -4731,17 +4840,41 @@ window.__ModuleLoader__.load({
|
|
|
4731
4840
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4732
4841
|
className: "dsm-workbuddy-models-summary",
|
|
4733
4842
|
children: t("row.modelsSummary", { count: activeEnabledIds.size })
|
|
4734
|
-
})] }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4843
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4735
4844
|
className: "dsm-workbuddy-models-head-actions",
|
|
4736
|
-
children:
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4845
|
+
children: [
|
|
4846
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4847
|
+
type: "button",
|
|
4848
|
+
className: "dsm-btn dsm-btn-outline",
|
|
4849
|
+
disabled: busy,
|
|
4850
|
+
onClick: () => {
|
|
4851
|
+
refreshModels();
|
|
4852
|
+
},
|
|
4853
|
+
children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
|
|
4854
|
+
}),
|
|
4855
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4856
|
+
type: "button",
|
|
4857
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-models-sweep",
|
|
4858
|
+
disabled: busy || sweeping !== void 0 || activeEnabledIds.size === 0,
|
|
4859
|
+
title: t("row.modelsTestEnabledHint"),
|
|
4860
|
+
onClick: sweepEnabledModels,
|
|
4861
|
+
children: sweeping === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
|
|
4862
|
+
done: sweeping.done,
|
|
4863
|
+
total: sweeping.total
|
|
4864
|
+
})
|
|
4865
|
+
}),
|
|
4866
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4867
|
+
type: "button",
|
|
4868
|
+
className: "dsm-btn dsm-btn-outline dsm-workbuddy-models-sweep",
|
|
4869
|
+
disabled: busy || sweeping !== void 0 || visibleModels.length === 0,
|
|
4870
|
+
title: t("row.modelsTestAllHint"),
|
|
4871
|
+
onClick: sweepAllModels,
|
|
4872
|
+
children: sweeping === void 0 ? t("row.modelsTestAll") : t("row.modelsTesting", {
|
|
4873
|
+
done: sweeping.done,
|
|
4874
|
+
total: sweeping.total
|
|
4875
|
+
})
|
|
4876
|
+
})
|
|
4877
|
+
]
|
|
4745
4878
|
})]
|
|
4746
4879
|
}),
|
|
4747
4880
|
probeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -4749,6 +4882,11 @@ window.__ModuleLoader__.load({
|
|
|
4749
4882
|
role: "alert",
|
|
4750
4883
|
children: t("row.probeError", { message: probeError })
|
|
4751
4884
|
}),
|
|
4885
|
+
sweepSummary === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4886
|
+
className: "dsm-workbuddy-models-sweep-summary",
|
|
4887
|
+
role: "status",
|
|
4888
|
+
children: sweepSummary
|
|
4889
|
+
}),
|
|
4752
4890
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4753
4891
|
className: "dsm-workbuddy-model-list",
|
|
4754
4892
|
children: visibleModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5131,12 +5269,34 @@ window.__ModuleLoader__.load({
|
|
|
5131
5269
|
}
|
|
5132
5270
|
}
|
|
5133
5271
|
/**
|
|
5134
|
-
* Automatic refresh period. Five minutes, matching a credit
|
|
5135
|
-
* rate of change: it moves on the scale of model calls, not
|
|
5136
|
-
* read is a read-only upstream call.
|
|
5272
|
+
* Automatic refresh period for the READOUT. Five minutes, matching a credit
|
|
5273
|
+
* balance's actual rate of change: it moves on the scale of model calls, not
|
|
5274
|
+
* seconds, and each read is a read-only upstream call.
|
|
5137
5275
|
*/
|
|
5138
5276
|
const COMPOSER_CREDITS_REFRESH_INTERVAL_MS = 3e5;
|
|
5139
5277
|
/**
|
|
5278
|
+
* Refresh period for the panel's per-account table, while it stays open.
|
|
5279
|
+
*
|
|
5280
|
+
* Deliberately NOT the readout's five minutes. The table carries two things,
|
|
5281
|
+
* and the faster-moving one decides the cadence:
|
|
5282
|
+
*
|
|
5283
|
+
* - the balance, which barely moves, and
|
|
5284
|
+
* - each account's `excludedBy` ("被限流" / "模型繁忙"), which is derived from
|
|
5285
|
+
* the pool's own cooldowns — and the SHORTEST of those is the model-scoped
|
|
5286
|
+
* two-minute throttle (`MODEL_BUSY_COOLDOWN_MS`).
|
|
5287
|
+
*
|
|
5288
|
+
* At five minutes the table could state an account is 模型繁忙 long after the
|
|
5289
|
+
* upstream let it serve again, and the pool card — which polls every 60 s
|
|
5290
|
+
* (`POLL_INTERVAL_MS` in `WorkBuddyCard.tsx`) — would say it is fine at the same
|
|
5291
|
+
* moment. Two surfaces disagreeing about one account is exactly the complaint
|
|
5292
|
+
* this constant answers, so the table now converges on the card's cadence
|
|
5293
|
+
* instead of its own.
|
|
5294
|
+
*
|
|
5295
|
+
* The cost is bounded and paid only while the panel is open: one `fetchCredits`
|
|
5296
|
+
* per account per minute, the same read the card already makes when it is open.
|
|
5297
|
+
*/
|
|
5298
|
+
const COMPOSER_ACCOUNT_REFRESH_INTERVAL_MS = 6e4;
|
|
5299
|
+
/**
|
|
5140
5300
|
* Provider routes whose credit balance this readout can show, mapped to the
|
|
5141
5301
|
* region to read it from.
|
|
5142
5302
|
*
|
|
@@ -5185,6 +5345,10 @@ window.__ModuleLoader__.load({
|
|
|
5185
5345
|
const [lastRefresh, setLastRefresh] = (0, react.useState)(void 0);
|
|
5186
5346
|
const [open, setOpen] = (0, react.useState)(false);
|
|
5187
5347
|
const inFlight = (0, react.useRef)(false);
|
|
5348
|
+
/** Account-table reads in flight; see `fetchAccountCredits`. */
|
|
5349
|
+
const accountsInFlight = (0, react.useRef)(0);
|
|
5350
|
+
/** Which account-table read is the newest; see `fetchAccountCredits`. */
|
|
5351
|
+
const accountsReadGeneration = (0, react.useRef)(0);
|
|
5188
5352
|
const mounted = (0, react.useRef)(true);
|
|
5189
5353
|
const rootRef = (0, react.useRef)(null);
|
|
5190
5354
|
const panelRef = (0, react.useRef)(null);
|
|
@@ -5233,9 +5397,27 @@ window.__ModuleLoader__.load({
|
|
|
5233
5397
|
* A failure keeps whatever we already have rather than clearing it: the table
|
|
5234
5398
|
* falls back to the usage document's account list, so a failed figure must not
|
|
5235
5399
|
* remove the switch.
|
|
5400
|
+
*
|
|
5401
|
+
* `force` distinguishes the TWO callers, and the distinction is the point:
|
|
5402
|
+
*
|
|
5403
|
+
* - the panel's timer passes nothing, so a tick that arrives while a read is
|
|
5404
|
+
* still in flight is SKIPPED. The panel is asking "has anything changed?",
|
|
5405
|
+
* and a read already on its way answers that.
|
|
5406
|
+
* - a deliberate action (the refresh button, or `switchTo` re-reading after a
|
|
5407
|
+
* write) passes `force: true`, because it must actually happen. Skipping it
|
|
5408
|
+
* would be the same defect this file is fixing: a control that reports "the
|
|
5409
|
+
* table is current" without having asked. It also claims the newest
|
|
5410
|
+
* generation, so its answer is the one that lands.
|
|
5411
|
+
*
|
|
5412
|
+
* Latest-wins rather than a queue: responses can resolve out of order, and a
|
|
5413
|
+
* slow tick started earlier must not overwrite the press that superseded it —
|
|
5414
|
+
* which would silently re-draw a mark the press had just cleared.
|
|
5236
5415
|
*/
|
|
5237
|
-
const fetchAccountCredits = (0, react.useCallback)(async (signal) => {
|
|
5416
|
+
const fetchAccountCredits = (0, react.useCallback)(async (signal, force) => {
|
|
5238
5417
|
if (activeRegion === void 0) return;
|
|
5418
|
+
if (!force && accountsInFlight.current > 0) return;
|
|
5419
|
+
const generation = ++accountsReadGeneration.current;
|
|
5420
|
+
accountsInFlight.current += 1;
|
|
5239
5421
|
try {
|
|
5240
5422
|
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_ACCOUNT_CREDITS_PATH, activeRegion), {
|
|
5241
5423
|
headers: { accept: "application/json" },
|
|
@@ -5245,8 +5427,14 @@ window.__ModuleLoader__.load({
|
|
|
5245
5427
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
5246
5428
|
const data = await response.json();
|
|
5247
5429
|
if (!mounted.current || signal?.aborted === true) return;
|
|
5248
|
-
if (
|
|
5249
|
-
|
|
5430
|
+
if (generation !== accountsReadGeneration.current) return;
|
|
5431
|
+
if (Array.isArray(data.accounts)) {
|
|
5432
|
+
setAccountCredits(data.accounts);
|
|
5433
|
+
setLastRefresh(Date.now());
|
|
5434
|
+
}
|
|
5435
|
+
} catch {} finally {
|
|
5436
|
+
accountsInFlight.current -= 1;
|
|
5437
|
+
}
|
|
5250
5438
|
}, [activeRegion]);
|
|
5251
5439
|
const hostHidden = hostCreditsShownOf(usage) === false;
|
|
5252
5440
|
(0, react.useEffect)(() => {
|
|
@@ -5270,11 +5458,37 @@ window.__ModuleLoader__.load({
|
|
|
5270
5458
|
hostHidden,
|
|
5271
5459
|
fetchUsage
|
|
5272
5460
|
]);
|
|
5461
|
+
/**
|
|
5462
|
+
* Per-account figures are read on OPEN, then kept fresh while the panel stays
|
|
5463
|
+
* open.
|
|
5464
|
+
*
|
|
5465
|
+
* The on-open read is what makes the table useful immediately. The TIMER is
|
|
5466
|
+
* the fix for the complaint that started this: with an open-only read, a panel
|
|
5467
|
+
* left open showed a frozen snapshot of every account's `excludedBy`, while
|
|
5468
|
+
* the pool card behind it re-ranked every 60 s. The two then disagreed about
|
|
5469
|
+
* the same account — the panel drawing no mark where the card said 模型繁忙 —
|
|
5470
|
+
* and neither surface was wrong on its own data, which is what made it so hard
|
|
5471
|
+
* to read.
|
|
5472
|
+
*
|
|
5473
|
+
* The timer starts only AFTER the first read settles, so a slow upstream
|
|
5474
|
+
* cannot stack overlapping reads (the same shape the readout's own effect
|
|
5475
|
+
* uses). Aborted on close: the panel pays one read per minute only while it is
|
|
5476
|
+
* actually on screen.
|
|
5477
|
+
*/
|
|
5273
5478
|
(0, react.useEffect)(() => {
|
|
5274
5479
|
if (!open || activeRegion === void 0) return void 0;
|
|
5480
|
+
let cancelled = false;
|
|
5481
|
+
let timer;
|
|
5275
5482
|
const controller = new AbortController();
|
|
5276
|
-
fetchAccountCredits(controller.signal)
|
|
5483
|
+
fetchAccountCredits(controller.signal).finally(() => {
|
|
5484
|
+
if (cancelled) return;
|
|
5485
|
+
timer = window.setInterval(() => {
|
|
5486
|
+
fetchAccountCredits(controller.signal);
|
|
5487
|
+
}, COMPOSER_ACCOUNT_REFRESH_INTERVAL_MS);
|
|
5488
|
+
});
|
|
5277
5489
|
return () => {
|
|
5490
|
+
cancelled = true;
|
|
5491
|
+
if (timer !== void 0) window.clearInterval(timer);
|
|
5278
5492
|
controller.abort();
|
|
5279
5493
|
};
|
|
5280
5494
|
}, [
|
|
@@ -5310,13 +5524,34 @@ window.__ModuleLoader__.load({
|
|
|
5310
5524
|
try {
|
|
5311
5525
|
await writeAccountSlot(settingsScope, activeRegion, accountId);
|
|
5312
5526
|
await fetchUsage();
|
|
5313
|
-
await fetchAccountCredits();
|
|
5527
|
+
await fetchAccountCredits(void 0, true);
|
|
5314
5528
|
} catch {
|
|
5315
5529
|
if (mounted.current) setFailed(true);
|
|
5316
5530
|
} finally {
|
|
5317
5531
|
if (mounted.current) setSwitchingId(void 0);
|
|
5318
5532
|
}
|
|
5319
5533
|
};
|
|
5534
|
+
/**
|
|
5535
|
+
* One manual refresh, of both things the panel shows.
|
|
5536
|
+
*
|
|
5537
|
+
* It used to call `fetchUsage` alone, so pressing 刷新 on the account table
|
|
5538
|
+
* refreshed the trigger's balance and left every ROW exactly as it was —
|
|
5539
|
+
* including its 被限流 / 模型繁忙 mark. The button sits inside the table's own
|
|
5540
|
+
* header, so the user reads it as "refresh this table"; a control that
|
|
5541
|
+
* refreshes something else is worse than no control, and it is one of the
|
|
5542
|
+
* reasons the panel could keep claiming a state the card had already moved
|
|
5543
|
+
* past.
|
|
5544
|
+
*
|
|
5545
|
+
* Both reads run concurrently rather than in sequence: neither depends on the
|
|
5546
|
+
* other, and awaiting the usage document first would delay the table by one
|
|
5547
|
+
* round trip for no benefit. `refreshUsage` resolves rather than throws on
|
|
5548
|
+
* failure (it records `failed` itself), so the table's read is never skipped
|
|
5549
|
+
* because the other half went wrong.
|
|
5550
|
+
*/
|
|
5551
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
5552
|
+
setFailed(false);
|
|
5553
|
+
await Promise.all([fetchUsage(), fetchAccountCredits(void 0, true)]);
|
|
5554
|
+
}, [fetchUsage, fetchAccountCredits]);
|
|
5320
5555
|
if (owned === void 0) return null;
|
|
5321
5556
|
/**
|
|
5322
5557
|
* The Host's own answer for this region, when the document carries one.
|
|
@@ -5397,7 +5632,7 @@ window.__ModuleLoader__.load({
|
|
|
5397
5632
|
className: "dsm-workbuddy-composer-panel-refresh",
|
|
5398
5633
|
disabled: busy || switchingId !== void 0,
|
|
5399
5634
|
onClick: () => {
|
|
5400
|
-
|
|
5635
|
+
refresh();
|
|
5401
5636
|
},
|
|
5402
5637
|
children: busy ? t("composer.refreshing") : t("composer.refresh")
|
|
5403
5638
|
})]
|
|
@@ -5583,7 +5818,14 @@ window.__ModuleLoader__.load({
|
|
|
5583
5818
|
//#endregion
|
|
5584
5819
|
//#region src/client/WorkBuddyQuotaNotice.tsx
|
|
5585
5820
|
/**
|
|
5586
|
-
* The supplement this plugin adds to
|
|
5821
|
+
* The supplement this plugin adds to a quota failure (issue #63).
|
|
5822
|
+
*
|
|
5823
|
+
* 参考:`dsh-connect-trae`(MIT)— 「以插件身份向宿主的通知链注册一个条目」的
|
|
5824
|
+
* 席位用法与自建 DOM(不依赖 ui-primitives)的渲染约定,见 docs/DESIGN.md §5.3。
|
|
5825
|
+
* 改动:本模块是原创实现,但其中两处决定**与本文件早期版本相反**,且都是被真机
|
|
5826
|
+
* 截图推翻的:(1) 曾认领 `shell.quota-notice` 链并在其中重绘宿主句子,后判定该
|
|
5827
|
+
* 链的语义是「接管整个界面」而非加装饰,遂改为**一律拒收**;(2) 曾是带按钮的
|
|
5828
|
+
* 卡片,后改为只读的行内括号。两处的原因各自写在下文对应位置,删改前请先读。
|
|
5587
5829
|
*
|
|
5588
5830
|
* WHY THIS EXISTS. When an upstream failure is classified `QUOTA`, the host
|
|
5589
5831
|
* renders ONE fixed sentence — 「当前请求的额度已用尽」 — and (measured on the
|
|
@@ -5604,19 +5846,29 @@ window.__ModuleLoader__.load({
|
|
|
5604
5846
|
* resets hours later. Staying out of the retry set is the behaviour we keep
|
|
5605
5847
|
* (the maintainer's call on issue #63); the sentence is what we supplement.
|
|
5606
5848
|
*
|
|
5607
|
-
* HOW. The host ships a CHAIN seat
|
|
5608
|
-
*
|
|
5609
|
-
*
|
|
5610
|
-
*
|
|
5611
|
-
*
|
|
5612
|
-
*
|
|
5613
|
-
* the
|
|
5614
|
-
*
|
|
5615
|
-
*
|
|
5616
|
-
*
|
|
5617
|
-
*
|
|
5618
|
-
*
|
|
5619
|
-
*
|
|
5849
|
+
* HOW (SUPERSEDED — see the DECLINE note below). The host ships a CHAIN seat
|
|
5850
|
+
* for exactly this: `shell.quota-notice`, a frame-wide surface whose own
|
|
5851
|
+
* comment says "a `shell.quota-notice` entry may replace the generic Toast for
|
|
5852
|
+
* codes it claims". An earlier build of this plugin claimed `QUOTA` there and
|
|
5853
|
+
* re-rendered the sentence plus the supplement.
|
|
5854
|
+
*
|
|
5855
|
+
* THE DECLINE (the maintainer's call, on a screenshot). Claiming a chain entry
|
|
5856
|
+
* does not decorate the host's surface, it TAKES IT OVER: the host's generic
|
|
5857
|
+
* warning Toast is then not rendered at all, and what replaced it was an inline
|
|
5858
|
+
* `<span>` — no fixed positioning, no centring, no background, no fade-out. It
|
|
5859
|
+
* landed at the top-left of `shell.overlay` and stayed. That is the box the
|
|
5860
|
+
* maintainer reported as "not being there before", and it was correct: the
|
|
5861
|
+
* plugin had painted it. The registration now declines every owner, so the
|
|
5862
|
+
* host's own Toast is what shows — a surface whose look this plugin does not
|
|
5863
|
+
* get to decide — and the supplement lives in the conversation instead, beside
|
|
5864
|
+
* the host's failure row, which is where the user is actually looking when a
|
|
5865
|
+
* turn fails.
|
|
5866
|
+
*
|
|
5867
|
+
* WHY THE HOST'S SENTENCE IS LEFT ALONE: it is the string the shell shows
|
|
5868
|
+
* everywhere else for this code, and a plugin that reworded it would contradict
|
|
5869
|
+
* the shell's own vocabulary. The supplement is additive, and it is written to
|
|
5870
|
+
* be TRUE FOR BOTH CASES the code covers — a model's own daily quota (not
|
|
5871
|
+
* credits) and a genuinely empty balance — because the plugin cannot always
|
|
5620
5872
|
* tell which one it is: the upstream states it in Chinese prose the plugin is
|
|
5621
5873
|
* not allowed to parse for policy.
|
|
5622
5874
|
*
|
|
@@ -5627,29 +5879,35 @@ window.__ModuleLoader__.load({
|
|
|
5627
5879
|
* notice is built from the same primitives every other component here uses.
|
|
5628
5880
|
*/
|
|
5629
5881
|
/**
|
|
5630
|
-
*
|
|
5882
|
+
* The chain selector: which owner (if any) this plugin takes over.
|
|
5631
5883
|
*
|
|
5632
|
-
* `
|
|
5633
|
-
*
|
|
5634
|
-
*
|
|
5635
|
-
*
|
|
5884
|
+
* ALWAYS `null`, and that is the fix — see the DECLINE note in the file header.
|
|
5885
|
+
* Exported so the decline is unit-testable without a host: the spec asserts
|
|
5886
|
+
* this returns null for every owner, which is what keeps the host's own Toast
|
|
5887
|
+
* on screen. A function rather than an inline lambda in the registration so the
|
|
5888
|
+
* rule has one home and one test, instead of being restated at the call site.
|
|
5636
5889
|
*/
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
function claimsQuotaNotice(code) {
|
|
5640
|
-
return typeof code === "string" && WORKBUDDY_QUOTA_NOTICE_CODES.includes(code);
|
|
5890
|
+
function selectQuotaNoticeClaim(_owner) {
|
|
5891
|
+
return null;
|
|
5641
5892
|
}
|
|
5642
|
-
/**
|
|
5893
|
+
/**
|
|
5894
|
+
* The supplement this plugin adds to a quota failure.
|
|
5895
|
+
*
|
|
5896
|
+
* It is what the `shell.quota-notice` chain WOULD render if the registration
|
|
5897
|
+
* claimed an owner — which it no longer does. Kept as the rendering seat for
|
|
5898
|
+
* the supplement so the copy, the locale keys and their guards survive the
|
|
5899
|
+
* decline: the next surface that wants this sentence (a failure row, a
|
|
5900
|
+
* transcript notice) mounts this component rather than re-inventing the markup.
|
|
5901
|
+
*
|
|
5902
|
+
* ONE line, no body, no button, no host sentence of its own — the takeover that
|
|
5903
|
+
* used to prepend the host's sentence is exactly what was removed.
|
|
5904
|
+
*/
|
|
5643
5905
|
function WorkBuddyQuotaNotice(props) {
|
|
5644
|
-
const {
|
|
5645
|
-
return (0, react.createElement)("
|
|
5906
|
+
const { t } = props;
|
|
5907
|
+
return (0, react.createElement)("span", {
|
|
5646
5908
|
className: "dsm-workbuddy-quota-notice",
|
|
5647
5909
|
role: "status"
|
|
5648
|
-
}, (0, react.createElement)("span", { className: "dsm-workbuddy-quota-notice-
|
|
5649
|
-
type: "button",
|
|
5650
|
-
className: "dsm-btn dsm-btn-outline dsm-workbuddy-quota-notice-dismiss",
|
|
5651
|
-
onClick: () => matched.dismiss?.()
|
|
5652
|
-
}, t("quotaNotice.dismiss")));
|
|
5910
|
+
}, (0, react.createElement)("span", { className: "dsm-workbuddy-quota-notice-body" }, t("quotaNotice.supplement")));
|
|
5653
5911
|
}
|
|
5654
5912
|
//#endregion
|
|
5655
5913
|
//#region src/client/locales.ts
|
|
@@ -5739,6 +5997,13 @@ window.__ModuleLoader__.load({
|
|
|
5739
5997
|
"row.modelReasoning": "Reasoning: {efforts}",
|
|
5740
5998
|
"row.modelUnknown": "Unknown",
|
|
5741
5999
|
"row.modelCapabilityPending": "Only capabilities advertised by WorkBuddy are shown.",
|
|
6000
|
+
"row.modelsTestEnabled": "Test enabled models",
|
|
6001
|
+
"row.modelsTestAll": "Test all models",
|
|
6002
|
+
"row.modelsTesting": "Testing {done}/{total}…",
|
|
6003
|
+
"row.modelsTestEnabledHint": "Tests every model you have ENABLED, one request each — each probe sends a real-volume request, so this spends real credit (about 0.01 per model at the default 25000-token size). Rows show their verdict as it lands. Stops early if the credential is rejected.",
|
|
6004
|
+
"row.modelsTestAllHint": "Tests EVERY model in this region's catalog, including the ones you have NOT enabled — so it costs noticeably more than \"test enabled models\". Same per-probe cost, same early stop on a rejected credential.",
|
|
6005
|
+
"row.modelsTestDone": "Done: {available} of {total} available",
|
|
6006
|
+
"row.modelsTestAuth": "Stopped: the credential was rejected, so every remaining model would answer the same way. Sign in again, then re-run.",
|
|
5742
6007
|
"row.probe": "Test",
|
|
5743
6008
|
"row.probing": "Testing…",
|
|
5744
6009
|
"row.probeHint": "Send a real-volume request (~25k input tokens) to this model and report whether it answers right now. The upstream rate limit fires on request SIZE — measured: about 20k tokens pass, about 30k are refused — so a small request can look fine while every long-conversation request is refused. This is why every test costs real credits (about 0.7) and runs one model at a time.",
|
|
@@ -5798,6 +6063,8 @@ window.__ModuleLoader__.load({
|
|
|
5798
6063
|
"row.poolTitle": "Account pool",
|
|
5799
6064
|
"row.poolNoCandidate": "No account in this pool can be used right now.",
|
|
5800
6065
|
"row.poolNoCandidateHint": "Members are rate-limited, out of credits, rejected, or unreachable. Rate limits, drained quotas and transient failures return to the pool on their own — at the time the upstream states, or after a 30-minute fallback. Only a rejected credential needs a new sign-in.",
|
|
6066
|
+
"row.poolModelWideBusy": "Every member is blocked on the same model — this is the model, not your accounts.",
|
|
6067
|
+
"row.poolModelWideBusyHint": "Switching accounts cannot help: the limit follows the model, so a different account hits the same wall. Wait about a minute, or switch to a less busy model in this session.",
|
|
5801
6068
|
"row.poolCheckinAll": "Check in all accounts",
|
|
5802
6069
|
"row.poolCheckingIn": "Checking in…",
|
|
5803
6070
|
"row.poolTestAll": "Test all accounts",
|
|
@@ -5847,9 +6114,6 @@ window.__ModuleLoader__.load({
|
|
|
5847
6114
|
"row.poolProbeSourceUnknown": "source unknown",
|
|
5848
6115
|
"row.poolProbeModel": "model {model}",
|
|
5849
6116
|
"row.poolProbeModelUnknown": "model not recorded",
|
|
5850
|
-
"row.poolRowModelHint": "Which model this account's test uses. Leave it on \"region target\" to follow the pool's model; pick one when this account must be tested on the model you actually chat with.",
|
|
5851
|
-
"row.poolRowModelAria": "Test model for {accountName}",
|
|
5852
|
-
"row.poolRowModelInherit": "region target",
|
|
5853
6117
|
"row.poolSettingsTitle": "Account pool settings",
|
|
5854
6118
|
"row.poolSettingsHint": "These are preferences: they take effect when you save (same as the model list). Test results are written automatically and are not affected by this button.",
|
|
5855
6119
|
"row.poolEnabled": "Enable the account pool",
|
|
@@ -5917,8 +6181,7 @@ window.__ModuleLoader__.load({
|
|
|
5917
6181
|
"row.creditPositionStats": "below the input box",
|
|
5918
6182
|
"row.creditPositionAria": "Credit readout position for {region}",
|
|
5919
6183
|
"quotaNotice.fallbackHead": "The quota for this request is exhausted",
|
|
5920
|
-
"quotaNotice.supplement": "
|
|
5921
|
-
"quotaNotice.dismiss": "Got it",
|
|
6184
|
+
"quotaNotice.supplement": " (some models also have a daily limit that is unrelated to your credits)",
|
|
5922
6185
|
"row.creditPositionHint": "Where the credit readout sits. \"composer toolbar\" is the default (the row with the model picker). Choose \"below the input box\" — the row carrying the round / step / token statistics — if another plugin occupies the composer toolbar and the readout does not appear.",
|
|
5923
6186
|
"row.showCreditsGlobalHint": "Show the international (WB AI) credit balance in the composer toolbar, but only while this session uses a WB AI model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
|
|
5924
6187
|
"composer.pointsCn": "WB CN",
|
|
@@ -6018,6 +6281,13 @@ window.__ModuleLoader__.load({
|
|
|
6018
6281
|
"row.modelReasoning": "推理强度:{efforts}",
|
|
6019
6282
|
"row.modelUnknown": "未知",
|
|
6020
6283
|
"row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
|
|
6284
|
+
"row.modelsTestEnabled": "一键测试启用模型",
|
|
6285
|
+
"row.modelsTestAll": "一键测试所有模型",
|
|
6286
|
+
"row.modelsTesting": "测试中 {done}/{total}…",
|
|
6287
|
+
"row.modelsTestEnabledHint": "逐个测试你**已启用**的模型,一个模型一次请求 —— 每次探测都发真实体积的请求,真花积分(默认 25000 token 体积下约 0.01/个)。每行的结果一出来就显示。凭据被拒时提前停止。",
|
|
6288
|
+
"row.modelsTestAllHint": "测试本区域目录里的**每一个**模型,含**未启用**的 —— 所以比「仅测试启用模型」明显更贵。单次探测成本相同,凭据被拒时同样提前停止。",
|
|
6289
|
+
"row.modelsTestDone": "完成:{total} 个里 {available} 个可用",
|
|
6290
|
+
"row.modelsTestAuth": "已停止:凭据被拒,剩下的模型都会得到同一个答案。请重新登录后再跑一次。",
|
|
6021
6291
|
"row.probe": "测试",
|
|
6022
6292
|
"row.probing": "测试中…",
|
|
6023
6293
|
"row.probeHint": "对该模型发一条真实体积的请求(约 25k 输入 token),报告它现在能不能用。上游的限流按请求体积触发——实测约 20k 能过、约 30k 被拒——所以小请求看着正常、长对话里却可能每次都被拒。这也是为什么每次测试都会消耗真实积分(约 0.7),且一次只测一个模型。",
|
|
@@ -6077,6 +6347,8 @@ window.__ModuleLoader__.load({
|
|
|
6077
6347
|
"row.poolTitle": "账号池",
|
|
6078
6348
|
"row.poolNoCandidate": "账号池里目前没有可用的账号。",
|
|
6079
6349
|
"row.poolNoCandidateHint": "成员要么在限流冷却中、要么积分耗尽、要么凭据被拒、要么上游无响应。限流、积分耗尽与瞬时故障都会自动回池——按上游给出的恢复时间,没给时间就按 30 分钟兜底,不需要你操作;只有「凭据被拒」需要重新登录。",
|
|
6350
|
+
"row.poolModelWideBusy": "所有成员都卡在同一个模型上 —— 这是模型的问题,不是你的账号。",
|
|
6351
|
+
"row.poolModelWideBusyHint": "换账号解决不了:限制跟随模型,换一个账号撞的是同一堵墙。等大约 1 分钟,或把这个会话切到负载较低的模型。",
|
|
6080
6352
|
"row.poolCheckinAll": "一键签到所有账号",
|
|
6081
6353
|
"row.poolCheckingIn": "签到中…",
|
|
6082
6354
|
"row.poolTestAll": "一键测试所有账号",
|
|
@@ -6126,9 +6398,6 @@ window.__ModuleLoader__.load({
|
|
|
6126
6398
|
"row.poolProbeSourceUnknown": "来源未知",
|
|
6127
6399
|
"row.poolProbeModel": "模型 {model}",
|
|
6128
6400
|
"row.poolProbeModelUnknown": "未记录模型",
|
|
6129
|
-
"row.poolRowModelHint": "这个账号测试用哪个模型。留「区域目标」就跟随账号池的目标模型;当某个号必须用你实际在聊的模型来测时,在这里单独指定。",
|
|
6130
|
-
"row.poolRowModelAria": "{accountName} 的测试模型",
|
|
6131
|
-
"row.poolRowModelInherit": "区域目标",
|
|
6132
6401
|
"row.poolSettingsTitle": "账号池设置",
|
|
6133
6402
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
6134
6403
|
"row.poolEnabled": "启用账号池",
|
|
@@ -6196,8 +6465,7 @@ window.__ModuleLoader__.load({
|
|
|
6196
6465
|
"row.creditPositionStats": "输入框下方",
|
|
6197
6466
|
"row.creditPositionAria": "{region} 的积分显示位置",
|
|
6198
6467
|
"quotaNotice.fallbackHead": "当前请求的额度已用尽",
|
|
6199
|
-
"quotaNotice.supplement": "
|
|
6200
|
-
"quotaNotice.dismiss": "知道了",
|
|
6468
|
+
"quotaNotice.supplement": "(部分模型另有每日额度,与积分无关)",
|
|
6201
6469
|
"row.creditPositionHint": "积分显示在哪里。「输入框工具栏」是默认位置(与模型选择同一行)。如果装了别的插件后读数不显示,改成「输入框下方」—— 那是「轮次 / 步数 / token」统计所在的那一行。",
|
|
6202
6470
|
"row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|
|
6203
6471
|
"composer.pointsCn": "WB CN",
|
|
@@ -6377,32 +6645,42 @@ window.__ModuleLoader__.load({
|
|
|
6377
6645
|
}
|
|
6378
6646
|
};
|
|
6379
6647
|
/**
|
|
6380
|
-
* The frame-wide quota notice,
|
|
6381
|
-
* (issue #63).
|
|
6648
|
+
* The frame-wide quota notice, DECLINED so the host keeps its own Toast
|
|
6649
|
+
* (issue #63 follow-up, the maintainer's call on a screenshot).
|
|
6650
|
+
*
|
|
6651
|
+
* WHAT WAS WRONG. The previous build claimed `QUOTA` and rendered the
|
|
6652
|
+
* supplement as an inline `<span>` in this seat. The chain's own contract
|
|
6653
|
+
* says a claiming entry "takes over the surface" — so claiming REPLACED the
|
|
6654
|
+
* host's generic warning Toast, and the replacement was not a Toast: an
|
|
6655
|
+
* inline span has no `position: fixed`, no centring, no background and no
|
|
6656
|
+
* fade, so it parked at the TOP-LEFT of `shell.overlay` (an `inset: 0`
|
|
6657
|
+
* layer) and stayed there until the next notice. The maintainer read it, on
|
|
6658
|
+
* screen, as a box that had not been there before — and it had not.
|
|
6382
6659
|
*
|
|
6383
|
-
*
|
|
6384
|
-
*
|
|
6385
|
-
*
|
|
6386
|
-
*
|
|
6387
|
-
*
|
|
6388
|
-
*
|
|
6389
|
-
*
|
|
6390
|
-
*
|
|
6391
|
-
*
|
|
6660
|
+
* WHY DECLINING IS THE FIX, not a worse one. The host's own sentence is
|
|
6661
|
+
* already correct and already appears in the turn's failure row inside the
|
|
6662
|
+
* conversation (`message.failure.quota`, via `failureMessage` in
|
|
6663
|
+
* `ui-chat`'s MessageItem) — the place the user is actually reading when a
|
|
6664
|
+
* turn fails. Taking over the frame-wide surface bought this plugin nothing
|
|
6665
|
+
* except a second, uglier copy of a sentence the host already draws where it
|
|
6666
|
+
* matters. Declining the claim returns the seat to the host: the Toast is
|
|
6667
|
+
* the host's own design, and this plugin stops imposing a look on a surface
|
|
6668
|
+
* it does not own.
|
|
6392
6669
|
*
|
|
6393
|
-
*
|
|
6394
|
-
*
|
|
6395
|
-
*
|
|
6396
|
-
*
|
|
6670
|
+
* The supplement itself is NOT lost — it rides in the conversation, which is
|
|
6671
|
+
* the only surface that is actually in the user's eyeline (see
|
|
6672
|
+
* `registerStatsCredits` and the failure row). The `select` below is
|
|
6673
|
+
* therefore an explicit, documented DECLINE of every code rather than a
|
|
6674
|
+
* deletion of the registration: the seat stays claimed in one place with its
|
|
6675
|
+
* rationale attached, so a future reader does not re-add the takeover
|
|
6676
|
+
* without re-reading why it was removed.
|
|
6397
6677
|
*/
|
|
6398
6678
|
const registerQuotaNotice = () => {
|
|
6399
6679
|
try {
|
|
6400
6680
|
return ctx.slots.inject("shell.quota-notice", () => ctx.slots.register({
|
|
6401
6681
|
name: "shell.quota-notice",
|
|
6402
6682
|
locale: namespace,
|
|
6403
|
-
select:
|
|
6404
|
-
return claimsQuotaNotice(typeof owner === "object" && owner !== null ? owner.code : void 0) ? owner : null;
|
|
6405
|
-
},
|
|
6683
|
+
select: selectQuotaNoticeClaim,
|
|
6406
6684
|
inject: () => ({ t })
|
|
6407
6685
|
}, WorkBuddyQuotaNotice));
|
|
6408
6686
|
} catch (error) {
|