dsh-connect-workbuddy 4.8.0 → 4.8.2
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 +32 -0
- package/README.en.md +5 -1
- package/README.md +5 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +1205 -300
- package/lib/{host-heartbeat--S6JVSci.js → host-heartbeat-B-4nBogJ.js} +11 -2
- package/lib/index.d.ts +33 -0
- package/lib/index.js +293 -50
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -690,7 +690,8 @@ window.__ModuleLoader__.load({
|
|
|
690
690
|
const slot = {
|
|
691
691
|
enabled: preferences.enabled,
|
|
692
692
|
targetModelId: preferences.targetModelId,
|
|
693
|
-
memberAccountIds: preferences.memberAccountIds
|
|
693
|
+
memberAccountIds: preferences.memberAccountIds,
|
|
694
|
+
orderAccountIds: preferences.orderAccountIds
|
|
694
695
|
};
|
|
695
696
|
if (preferences.probeInputTokens !== void 0) slot["probeInputTokens"] = preferences.probeInputTokens;
|
|
696
697
|
const next = nextRegionPool(scope.getSnapshot().value, region, slot)[region];
|
|
@@ -704,10 +705,73 @@ window.__ModuleLoader__.load({
|
|
|
704
705
|
if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
|
|
705
706
|
const wanted = new Set(preferences.memberAccountIds);
|
|
706
707
|
if (!stored.every((id) => typeof id === "string" && wanted.has(id))) return false;
|
|
708
|
+
const storedOrder = pool["orderAccountIds"];
|
|
709
|
+
if (!Array.isArray(storedOrder) || storedOrder.length !== preferences.orderAccountIds.length) return false;
|
|
710
|
+
if (!storedOrder.every((id, index) => id === preferences.orderAccountIds[index])) return false;
|
|
707
711
|
return true;
|
|
708
712
|
});
|
|
709
713
|
}
|
|
710
714
|
//#endregion
|
|
715
|
+
//#region src/client/manual-order.ts
|
|
716
|
+
/**
|
|
717
|
+
* Manual priority order, shared by every surface that lists member accounts.
|
|
718
|
+
*
|
|
719
|
+
* The defect this exists to prevent: the pool card sorts its rows by
|
|
720
|
+
* `orderAccountIds` while the composer credits panel listed the same accounts
|
|
721
|
+
* in whatever order the Host returned. Two views of one pool, disagreeing — the
|
|
722
|
+
* user sorts the pool, opens the credit panel, and sees a different order, so
|
|
723
|
+
* neither list looks authoritative. A user cannot tell which one is "the" order,
|
|
724
|
+
* and the manual sorting feature looks broken even though it saved correctly.
|
|
725
|
+
*
|
|
726
|
+
* So the order lives here, once, and both lists call it. The alternative —
|
|
727
|
+
* each component sorting however it likes — is exactly what drifted.
|
|
728
|
+
*/
|
|
729
|
+
/**
|
|
730
|
+
* The saved manual order out of a settings snapshot, or an empty array.
|
|
731
|
+
*
|
|
732
|
+
* Reads defensively: the snapshot is Host-supplied JSON, so `regions` may be
|
|
733
|
+
* absent (a profile that never used this feature), `pool` may be absent, and
|
|
734
|
+
* `orderAccountIds` may be anything at all. Every shape that is not a list of
|
|
735
|
+
* strings means "no manual order", which is also an older profile's spelling —
|
|
736
|
+
* never a crash.
|
|
737
|
+
*/
|
|
738
|
+
function readOrderAccountIds(value, region) {
|
|
739
|
+
if (region === void 0) return [];
|
|
740
|
+
const regions = value?.regions;
|
|
741
|
+
if (typeof regions !== "object" || regions === null) return [];
|
|
742
|
+
const slot = regions[region];
|
|
743
|
+
if (typeof slot !== "object" || slot === null) return [];
|
|
744
|
+
const order = slot.pool;
|
|
745
|
+
if (typeof order !== "object" || order === null) return [];
|
|
746
|
+
const ids = order.orderAccountIds;
|
|
747
|
+
if (!Array.isArray(ids)) return [];
|
|
748
|
+
return ids.filter((id) => typeof id === "string");
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Reorder `accounts` to follow `orderIds`, leaving unordered accounts in place
|
|
752
|
+
* AFTER the ordered ones, and preserving their original relative order.
|
|
753
|
+
*
|
|
754
|
+
* Returns a new array; never mutates the input, because the caller's array is
|
|
755
|
+
* usually a state value that must not change identity behind React's back.
|
|
756
|
+
*
|
|
757
|
+
* Ids in `orderIds` that no longer name an account are simply ignored: the pool
|
|
758
|
+
* can drop a member while an order still mentions it, and that must degrade to
|
|
759
|
+
* "that position is empty" rather than hide a real account.
|
|
760
|
+
*/
|
|
761
|
+
function applyManualOrder(accounts, orderIds) {
|
|
762
|
+
if (orderIds.length === 0) return [...accounts];
|
|
763
|
+
const rank = /* @__PURE__ */ new Map();
|
|
764
|
+
orderIds.forEach((id, index) => {
|
|
765
|
+
if (!rank.has(id)) rank.set(id, index);
|
|
766
|
+
});
|
|
767
|
+
const ordered = [];
|
|
768
|
+
const rest = [];
|
|
769
|
+
for (const account of accounts) if (rank.has(account.id)) ordered.push(account);
|
|
770
|
+
else rest.push(account);
|
|
771
|
+
ordered.sort((left, right) => (rank.get(left.id) ?? 0) - (rank.get(right.id) ?? 0));
|
|
772
|
+
return [...ordered, ...rest];
|
|
773
|
+
}
|
|
774
|
+
//#endregion
|
|
711
775
|
//#region src/client/exclusion-text.ts
|
|
712
776
|
/** The exclusion label for one account, or undefined when it is usable. */
|
|
713
777
|
function exclusionText(t, excludedBy) {
|
|
@@ -724,6 +788,449 @@ window.__ModuleLoader__.load({
|
|
|
724
788
|
}
|
|
725
789
|
}
|
|
726
790
|
//#endregion
|
|
791
|
+
//#region src/account-pool.ts
|
|
792
|
+
/**
|
|
793
|
+
* How long an account stays out of the pool when the upstream stated no time.
|
|
794
|
+
*
|
|
795
|
+
* A conservative FALLBACK, not a measurement: it applies whenever a failed
|
|
796
|
+
* outcome carries no `retryAtMs`, which is every rate limit that came without a
|
|
797
|
+
* reset sentence AND every transient failure (a transport error, a 5xx). In both
|
|
798
|
+
* cases "when can I use this again" has no honest answer.
|
|
799
|
+
*
|
|
800
|
+
* The two failure directions it avoids: keeping the account in the pool would
|
|
801
|
+
* re-bill it immediately and re-trip the same limit; dropping it FOREVER would
|
|
802
|
+
* discard a working account over an event that said nothing about it. A short,
|
|
803
|
+
* stated cooldown is the least-wrong of the two — and it is what lets the pool
|
|
804
|
+
* heal itself, since nothing else re-probes an excluded member.
|
|
805
|
+
*/
|
|
806
|
+
const POOL_UNKNOWN_COOLDOWN_MS = 18e5;
|
|
807
|
+
/**
|
|
808
|
+
* Whether one account can be billed at `nowMs`.
|
|
809
|
+
*
|
|
810
|
+
* Returns the exclusion reason, or undefined when the account is a candidate.
|
|
811
|
+
* The order matters and is NOT arbitrary:
|
|
812
|
+
*
|
|
813
|
+
* - A `credential-rejected` account is excluded regardless of any cooldown: its
|
|
814
|
+
* token is not accepted, so waiting cannot help and the user must re-auth.
|
|
815
|
+
* - A limited, unreachable or failed outcome is checked against its stated (or
|
|
816
|
+
* fallback) cooldown, so an account whose limit has already reset — or whose
|
|
817
|
+
* gateway blipped once — is back in the pool without needing another test
|
|
818
|
+
* first.
|
|
819
|
+
* - An `ok` measurement, and no measurement at all, are both candidates. The
|
|
820
|
+
* latter matters: a freshly discovered account has never been tested and must
|
|
821
|
+
* not be invisible.
|
|
822
|
+
*
|
|
823
|
+
* WHY TRANSIENT OUTCOMES GET A COOLDOWN. They are not statements about the
|
|
824
|
+
* ACCOUNT. A transport failure can be a DNS blip, and — before the shim learned
|
|
825
|
+
* to skip reporting an aborted request — it was routinely the user closing the
|
|
826
|
+
* panel mid-flight. A 5xx is the gateway's afternoon, not this account's health.
|
|
827
|
+
* Excluding them permanently meant one transient event idled a perfectly good
|
|
828
|
+
* account until the user happened to press "test" again, and with every member
|
|
829
|
+
* so idled the pool had no candidate at all: failover then had nowhere to go and
|
|
830
|
+
* the raw upstream error was reported as-is. The cooldown is what makes the pool
|
|
831
|
+
* RECOVER instead of staying broken.
|
|
832
|
+
*
|
|
833
|
+
* `not-found` and `credential-rejected` deliberately get none: one describes a
|
|
834
|
+
* model the region no longer offers (a catalog fact, not a transient one) and
|
|
835
|
+
* the other needs the user to sign in again. Neither is fixed by waiting, and
|
|
836
|
+
* pretending otherwise would re-bill a token the upstream has already refused.
|
|
837
|
+
*
|
|
838
|
+
* Each unusable outcome keeps its OWN value rather than being folded into
|
|
839
|
+
* `credential-rejected`. The fold made one row state two contradictory things —
|
|
840
|
+
* the name column said "被拒绝" (sign in again) while the probe column, reading
|
|
841
|
+
* the same measurement, said "连不上上游——这是网络问题,不是模型问题" — and it
|
|
842
|
+
* told a user whose upstream had merely blipped that their sign-in was bad.
|
|
843
|
+
* (That label has since been made cause-NEUTRAL: it used to assert "not the
|
|
844
|
+
* model" for a bucket that also holds gateway 5xx and request timeouts, where
|
|
845
|
+
* the claim is simply unfounded. The recorded `message` now carries the cause.)
|
|
846
|
+
* The distinctions are already computed upstream in `outcomeOfFailure`
|
|
847
|
+
* (`src/probe.ts:290-299`); this only stops discarding them.
|
|
848
|
+
*/
|
|
849
|
+
function exclusionOf(probe, nowMs) {
|
|
850
|
+
if (probe === void 0) return void 0;
|
|
851
|
+
switch (probe.outcome) {
|
|
852
|
+
case "ok": return;
|
|
853
|
+
case "credential-rejected": return "credential-rejected";
|
|
854
|
+
case "not-found": return "not-found";
|
|
855
|
+
case "unavailable": return retryDue(probe, nowMs) ? void 0 : "unavailable";
|
|
856
|
+
case "failed": return retryDue(probe, nowMs) ? void 0 : "failed";
|
|
857
|
+
case "rate-limited": return retryDue(probe, nowMs) ? void 0 : "rate-limited";
|
|
858
|
+
case "model-busy": return retryDue(probe, nowMs, 12e4) ? void 0 : "model-busy";
|
|
859
|
+
case "out-of-credit": return retryDue(probe, nowMs) ? void 0 : "out-of-credit";
|
|
860
|
+
case "policy-rejected": return;
|
|
861
|
+
default: return "unusable";
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Whether an account's cooldown has elapsed (or was never stated).
|
|
866
|
+
*
|
|
867
|
+
* The fallback is the ONLY thing a transient outcome relies on: `cooldownOf`
|
|
868
|
+
* attaches `retryAtMs` for a rate limit or a drained quota, never for a
|
|
869
|
+
* transport failure, so `atMs + POOL_UNKNOWN_COOLDOWN_MS` is what brings those
|
|
870
|
+
* back. That is deliberate — the alternative is a permanent exclusion decided by
|
|
871
|
+
* an event that said nothing about the account.
|
|
872
|
+
*
|
|
873
|
+
* `fallbackMs` lets one outcome carry a SHORTER clock than the generic one: the
|
|
874
|
+
* model-scoped throttle is upstream's own "about a minute", and applying the
|
|
875
|
+
* half-hour account fallback to it is issue #56. It is a parameter rather than a
|
|
876
|
+
* second function so every outcome keeps sharing one definition of "due".
|
|
877
|
+
*/
|
|
878
|
+
function retryDue(probe, nowMs, fallbackMs = POOL_UNKNOWN_COOLDOWN_MS) {
|
|
879
|
+
return nowMs >= (probe.retryAtMs ?? probe.atMs + fallbackMs);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The checked ids that still resolve to a local sign-in, in the caller's order.
|
|
883
|
+
*
|
|
884
|
+
* This is THE definition of "effective members", and it exists because the same
|
|
885
|
+
* predicate was written five separate times: the status document
|
|
886
|
+
* (`web-status.ts`), the batch route's empty-pool guard, the Host dependency the
|
|
887
|
+
* card reads, `poolMemberAccounts` and the browser helper. Five copies of one
|
|
888
|
+
* rule is this project's single most productive bug shape ("fixed one end,
|
|
889
|
+
* missed the other"), so the rule now has one home and every site delegates to
|
|
890
|
+
* it.
|
|
891
|
+
*
|
|
892
|
+
* ORDER IS THE CALLER'S, and comes from `orderedIds`: the answer preserves the
|
|
893
|
+
* order of THAT argument. Callers pass whichever list's order they want to show
|
|
894
|
+
* — the saved list for the user's own order, the store's roster for the account
|
|
895
|
+
* table's. Neither is silently reshuffled by the filter. (The first parameter was
|
|
896
|
+
* once named `saved`, which read as "this must be the saved list" even though
|
|
897
|
+
* `web-status.ts` legitimately passes the roster for store order; the name was
|
|
898
|
+
* the trap, so it now describes the ROLE instead of the source.)
|
|
899
|
+
*
|
|
900
|
+
* Pure, so it is unit-testable without a store, and it never mutates its input.
|
|
901
|
+
*/
|
|
902
|
+
function effectiveMembersOf(orderedIds, listedIds) {
|
|
903
|
+
return orderedIds.filter((id) => listedIds.has(id));
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* A latest-wins guard for one region's asynchronous work.
|
|
907
|
+
*
|
|
908
|
+
* Exists because `applyRotation` awaits (it fetches credits per member), so two
|
|
909
|
+
* overlapping calls can interleave: a slow call that read "rotation is ON" can
|
|
910
|
+
* resume AFTER a newer call cleared the override for "rotation is OFF" and write
|
|
911
|
+
* the stale account back — leaving the card showing the pool switched off while
|
|
912
|
+
* a rotated account is still billed.
|
|
913
|
+
*
|
|
914
|
+
* `begin()` claims a new generation and returns a probe; the caller must check
|
|
915
|
+
* it after EVERY await and bail when it reports stale. Extracted as a pure
|
|
916
|
+
* factory rather than inlined so the rule is unit-testable: an inlined token is
|
|
917
|
+
* only observable by reproducing a real interleaving, which is slow and easy to
|
|
918
|
+
* get subtly wrong (a harness that never actually overlaps passes either way).
|
|
919
|
+
*/
|
|
920
|
+
function createLatestWins() {
|
|
921
|
+
const generations = {};
|
|
922
|
+
return { begin(key) {
|
|
923
|
+
const generation = (generations[key] ?? 0) + 1;
|
|
924
|
+
generations[key] = generation;
|
|
925
|
+
return () => generations[key] !== generation;
|
|
926
|
+
} };
|
|
927
|
+
}
|
|
928
|
+
//#endregion
|
|
929
|
+
//#region src/client/remaining.ts
|
|
930
|
+
const MINUTE_MS$1 = 6e4;
|
|
931
|
+
const HOUR_MS$1 = 60 * MINUTE_MS$1;
|
|
932
|
+
const DAY_MS$1 = 24 * HOUR_MS$1;
|
|
933
|
+
/**
|
|
934
|
+
* How far out hours stop being the useful unit.
|
|
935
|
+
*
|
|
936
|
+
* Below this, hours; at or above it, days. The cut is at TWO days rather than
|
|
937
|
+
* one so that 30 hours reads as "约 30 小时后" instead of rounding to "约 1 天后",
|
|
938
|
+
* which would understate it by six hours.
|
|
939
|
+
*/
|
|
940
|
+
const DAYS_AT_MS = 2 * DAY_MS$1;
|
|
941
|
+
/**
|
|
942
|
+
* A short "约 N 分钟后 / 约 N 小时后 / 约 N 天后", or `''` once the moment passed.
|
|
943
|
+
*
|
|
944
|
+
* The empty string is the contract callers depend on: an elapsed cooldown means
|
|
945
|
+
* the account is already back, so appending "in 0 minutes" would assert the
|
|
946
|
+
* opposite of the state the card is showing. Callers append this only when it is
|
|
947
|
+
* non-empty.
|
|
948
|
+
*
|
|
949
|
+
* Rounding is deliberately coarse — "约 1 小时后" for 69 minutes is what a reader
|
|
950
|
+
* wants, and a precision that changes every minute would be noise rather than
|
|
951
|
+
* information. The absolute instant sits right beside it for anyone who needs it.
|
|
952
|
+
*/
|
|
953
|
+
function remainingText(t, untilMs, nowMs) {
|
|
954
|
+
const left = untilMs - nowMs;
|
|
955
|
+
if (left <= 0) return "";
|
|
956
|
+
if (left < MINUTE_MS$1) return t("row.remainingImminent");
|
|
957
|
+
if (left < HOUR_MS$1) return t("row.remainingMinutes", { count: Math.round(left / MINUTE_MS$1) });
|
|
958
|
+
if (left < DAYS_AT_MS) return t("row.remainingHours", { count: Math.round(left / HOUR_MS$1) });
|
|
959
|
+
return t("row.remainingDays", { count: Math.round(left / DAY_MS$1) });
|
|
960
|
+
}
|
|
961
|
+
//#endregion
|
|
962
|
+
//#region src/client/account-state.ts
|
|
963
|
+
/** Whether a probe outcome means the model answered. */
|
|
964
|
+
function outcomeOk(outcome) {
|
|
965
|
+
return outcome === "ok";
|
|
966
|
+
}
|
|
967
|
+
/** A short, localizable label for one probe outcome. */
|
|
968
|
+
function outcomeText(t, outcome) {
|
|
969
|
+
switch (outcome) {
|
|
970
|
+
case "ok": return t("row.probeOk");
|
|
971
|
+
case "rate-limited": return t("row.poolExcludedRateLimited");
|
|
972
|
+
case "model-busy": return t("row.poolExcludedModelBusy");
|
|
973
|
+
case "out-of-credit": return t("row.poolExcludedOutOfCredit");
|
|
974
|
+
case "credential-rejected": return t("row.probeCredentialRejected");
|
|
975
|
+
case "not-found": return t("row.probeNotFound");
|
|
976
|
+
case "unavailable": return t("row.probeUnavailable");
|
|
977
|
+
default: return t("row.probeFailed");
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* `MM/DD, HH:mm` on a 24-hour clock.
|
|
982
|
+
*
|
|
983
|
+
* `hourCycle: 'h23'` is REQUIRED, not decorative: without it `Intl` follows the
|
|
984
|
+
* browser locale's preference, and en-US renders `09/30, 01:23 PM`. A wall-clock
|
|
985
|
+
* glued to "AM/PM" reads as 12-hour no matter what the surrounding UI assumes,
|
|
986
|
+
* and it is the one thing a Chinese user cannot scan past — the card otherwise
|
|
987
|
+
* speaks 24-hour everywhere (see `formatClock`).
|
|
988
|
+
*
|
|
989
|
+
* `h23` rather than `hour12: false`, because the latter maps to `h24` in some
|
|
990
|
+
* engines and would print midnight as `24:00`. `h23` pins 00–23 exactly, which
|
|
991
|
+
* is also the cycle DSH's own timestamps use.
|
|
992
|
+
*/
|
|
993
|
+
function formatShortClock(value) {
|
|
994
|
+
return new Intl.DateTimeFormat(void 0, {
|
|
995
|
+
month: "2-digit",
|
|
996
|
+
day: "2-digit",
|
|
997
|
+
hour: "2-digit",
|
|
998
|
+
minute: "2-digit",
|
|
999
|
+
hourCycle: "h23"
|
|
1000
|
+
}).format(new Date(value));
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* The "why can't this serve, and until when" sentence for one account.
|
|
1004
|
+
*
|
|
1005
|
+
* Returns `undefined` when there is nothing to say — an account with no
|
|
1006
|
+
* measurement and no exclusion is simply unknown, and inventing "可用" for it
|
|
1007
|
+
* would be a claim nobody made.
|
|
1008
|
+
*
|
|
1009
|
+
* The retry wording has three states, matching the card's three:
|
|
1010
|
+
* - a stated instant → "被限流 · 10/19 01:32 之后可再用"
|
|
1011
|
+
* - no instant but a timed → "被限流 · 未提供恢复时间"
|
|
1012
|
+
* exclusion
|
|
1013
|
+
* - any other outcome → the bare outcome ("连不上上游")
|
|
1014
|
+
*
|
|
1015
|
+
* That third case is deliberate: "未提供恢复时间" beside a credential rejection
|
|
1016
|
+
* would imply waiting helps, when the only fix is signing in again.
|
|
1017
|
+
*/
|
|
1018
|
+
function availabilityLine(t, account, nowMs = Date.now()) {
|
|
1019
|
+
const probe = account.probe;
|
|
1020
|
+
if (probe !== void 0) {
|
|
1021
|
+
if (probe.retryAtMs !== void 0) {
|
|
1022
|
+
const left = remainingText(t, probe.retryAtMs, nowMs);
|
|
1023
|
+
const at = t("row.poolRetryAt", { at: formatShortClock(probe.retryAtMs) });
|
|
1024
|
+
return `${outcomeText(t, probe.outcome)} · ${at}${left === "" ? "" : ` · ${left}`}`;
|
|
1025
|
+
}
|
|
1026
|
+
if (probe.outcome === "rate-limited" || probe.outcome === "model-busy" || probe.outcome === "out-of-credit") return `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}`;
|
|
1027
|
+
return outcomeText(t, probe.outcome);
|
|
1028
|
+
}
|
|
1029
|
+
if (account.excludedBy !== void 0) return exclusionLabel(t, account.excludedBy);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* The SHORT form of the same fact: why the account cannot serve, and how long
|
|
1033
|
+
* until it can — without the absolute instant.
|
|
1034
|
+
*
|
|
1035
|
+
* This exists because the composer's balance panel prints one line per account
|
|
1036
|
+
* and the absolute date made every limited row read like a log entry
|
|
1037
|
+
* ("被限流 · 10/19, 01:32 之后可再用 · 约 6 小时后") beside a number the row is
|
|
1038
|
+
* actually about. The panel keeps the reason and the wait; the pool card keeps
|
|
1039
|
+
* the full sentence from {@link availabilityLine}, which is the one place a
|
|
1040
|
+
* reader can afford the exact instant.
|
|
1041
|
+
*
|
|
1042
|
+
* It is a sibling rather than a parameter so the two surfaces cannot half-adopt
|
|
1043
|
+
* a flag: the card has no way to reach this function, and the panel has no way
|
|
1044
|
+
* to lose the wait.
|
|
1045
|
+
*/
|
|
1046
|
+
function availabilityMark(t, account, nowMs = Date.now()) {
|
|
1047
|
+
const probe = account.probe;
|
|
1048
|
+
if (probe !== void 0) {
|
|
1049
|
+
if (probe.retryAtMs !== void 0) {
|
|
1050
|
+
const left = remainingText(t, probe.retryAtMs, nowMs);
|
|
1051
|
+
return left === "" ? outcomeText(t, probe.outcome) : `${outcomeText(t, probe.outcome)} · ${left}`;
|
|
1052
|
+
}
|
|
1053
|
+
if (probe.outcome === "rate-limited" || probe.outcome === "model-busy" || probe.outcome === "out-of-credit") return `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}`;
|
|
1054
|
+
return outcomeText(t, probe.outcome);
|
|
1055
|
+
}
|
|
1056
|
+
if (account.excludedBy !== void 0) return exclusionLabel(t, account.excludedBy);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* The exclusion's own label, for the case where no probe backs it.
|
|
1060
|
+
*
|
|
1061
|
+
* Kept private: callers that have a probe must go through `availabilityLine` or
|
|
1062
|
+
* `availabilityMark`, so neither surface can invent its own vocabulary for the
|
|
1063
|
+
* same measurement.
|
|
1064
|
+
*/
|
|
1065
|
+
function exclusionLabel(t, excludedBy) {
|
|
1066
|
+
switch (excludedBy) {
|
|
1067
|
+
case "rate-limited": return t("row.poolExcludedRateLimited");
|
|
1068
|
+
case "model-busy": return t("row.poolExcludedModelBusy");
|
|
1069
|
+
case "out-of-credit": return t("row.poolExcludedOutOfCredit");
|
|
1070
|
+
case "credential-rejected": return t("row.poolExcludedRejected");
|
|
1071
|
+
case "not-found": return t("row.probeNotFound");
|
|
1072
|
+
case "unavailable": return t("row.probeUnavailable");
|
|
1073
|
+
case "failed": return t("row.probeFailed");
|
|
1074
|
+
case "unusable": return t("row.poolExcludedUnusable");
|
|
1075
|
+
default: return t("row.poolExcludedUnusable");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* The dot's state as a class suffix, so CSS can colour it without inline styles.
|
|
1080
|
+
*
|
|
1081
|
+
* `error` is separate from `warn` because the advice differs: one means "wait",
|
|
1082
|
+
* the other means "do something".
|
|
1083
|
+
*/
|
|
1084
|
+
function stateTone(account) {
|
|
1085
|
+
if (account.excludedBy !== void 0) return account.excludedBy === "rate-limited" || account.excludedBy === "model-busy" || account.excludedBy === "unavailable" ? "warn" : "error";
|
|
1086
|
+
if (account.probe === void 0) return "unknown";
|
|
1087
|
+
return outcomeOk(account.probe.outcome) ? "ok" : "warn";
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* The tone as a CSS colour variable.
|
|
1091
|
+
*
|
|
1092
|
+
* Derived from `stateTone` rather than repeating its branches: an inlined copy
|
|
1093
|
+
* would be a second definition of the rule, free to drift the first time one is
|
|
1094
|
+
* edited. The pool table draws a dot from a computed colour, so it needs a value
|
|
1095
|
+
* rather than a class name.
|
|
1096
|
+
*/
|
|
1097
|
+
function stateColor(account) {
|
|
1098
|
+
switch (stateTone(account)) {
|
|
1099
|
+
case "ok": return "var(--dsw-alias-state-success-primary, #22a06b)";
|
|
1100
|
+
case "warn": return "var(--dsw-alias-state-warn-primary, #f59e0b)";
|
|
1101
|
+
case "error": return "var(--dsw-alias-state-error-primary, #ef4444)";
|
|
1102
|
+
default: return "var(--dsw-alias-label-dimmed, #9aa0a6)";
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Read one line of the test batch's stream as a test row.
|
|
1107
|
+
*
|
|
1108
|
+
* The stream is `{ action, modelId }`, then one `{ row }` per account, then
|
|
1109
|
+
* `{ done: true }` — or `{ reason, error }` when the batch itself broke. Only the
|
|
1110
|
+
* row shape is recognized here; the rest is the caller's business.
|
|
1111
|
+
*
|
|
1112
|
+
* Every field is validated rather than trusted. This parses a wire the panel did
|
|
1113
|
+
* not write, and a half-shaped row reaching the component's state would render
|
|
1114
|
+
* as a measurement nobody took — worse than ignoring the line, because the
|
|
1115
|
+
* subsequent re-read is what would have quietly corrected it.
|
|
1116
|
+
*/
|
|
1117
|
+
function testRowOf(line) {
|
|
1118
|
+
const raw = line["row"];
|
|
1119
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1120
|
+
const row = raw;
|
|
1121
|
+
const accountId = row["accountId"];
|
|
1122
|
+
const name = row["accountName"];
|
|
1123
|
+
const rawResult = row["result"];
|
|
1124
|
+
if (typeof accountId !== "string" || accountId === "") return void 0;
|
|
1125
|
+
if (typeof rawResult !== "object" || rawResult === null) return void 0;
|
|
1126
|
+
const result = probeResultOf(rawResult);
|
|
1127
|
+
if (result === void 0) return void 0;
|
|
1128
|
+
return {
|
|
1129
|
+
accountId,
|
|
1130
|
+
accountName: typeof name === "string" && name !== "" ? name : accountId,
|
|
1131
|
+
result
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
/** The probe result inside a test row, when it is complete enough to render. */
|
|
1135
|
+
function probeResultOf(raw) {
|
|
1136
|
+
const outcome = raw["outcome"];
|
|
1137
|
+
const modelId = raw["modelId"];
|
|
1138
|
+
if (typeof outcome !== "string" || !isProbeOutcome(outcome)) return void 0;
|
|
1139
|
+
if (typeof modelId !== "string") return void 0;
|
|
1140
|
+
const elapsedMs = raw["elapsedMs"];
|
|
1141
|
+
const status = raw["status"];
|
|
1142
|
+
const message = raw["message"];
|
|
1143
|
+
const retryAtMs = raw["retryAtMs"];
|
|
1144
|
+
const retrySource = raw["retrySource"];
|
|
1145
|
+
return {
|
|
1146
|
+
modelId,
|
|
1147
|
+
outcome,
|
|
1148
|
+
...typeof elapsedMs === "number" ? { elapsedMs } : {},
|
|
1149
|
+
...typeof status === "number" ? { status } : {},
|
|
1150
|
+
...typeof message === "string" ? { message } : {},
|
|
1151
|
+
...typeof retryAtMs === "number" ? { retryAtMs } : {},
|
|
1152
|
+
...isRetrySource(retrySource) ? { retrySource } : {}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
/** The outcome vocabulary, as a value — the type alone cannot be tested against a wire. */
|
|
1156
|
+
const PROBE_OUTCOMES = [
|
|
1157
|
+
"ok",
|
|
1158
|
+
"rate-limited",
|
|
1159
|
+
"model-busy",
|
|
1160
|
+
"out-of-credit",
|
|
1161
|
+
"credential-rejected",
|
|
1162
|
+
"policy-rejected",
|
|
1163
|
+
"unavailable",
|
|
1164
|
+
"not-found",
|
|
1165
|
+
"failed"
|
|
1166
|
+
];
|
|
1167
|
+
function isProbeOutcome(value) {
|
|
1168
|
+
return PROBE_OUTCOMES.includes(value);
|
|
1169
|
+
}
|
|
1170
|
+
function isRetrySource(value) {
|
|
1171
|
+
return value === "retry-after" || value === "upstream-message" || value === "quota-refresh" || value === "upstream-tips";
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Fold ONE streamed test row into the usage document the panel is rendering.
|
|
1175
|
+
*
|
|
1176
|
+
* Why fold at all, when the batch is followed by a re-read: the batch is SERIAL
|
|
1177
|
+
* and a stuck member can take seconds, so a panel that changes nothing until the
|
|
1178
|
+
* LAST account answers is exactly the dead-looking button the streamed batch was
|
|
1179
|
+
* built to avoid. Folding each row as it lands makes the row change the moment
|
|
1180
|
+
* its measurement exists; the re-read afterwards then replaces the fold with the
|
|
1181
|
+
* same facts, this time from the Host's own store.
|
|
1182
|
+
*
|
|
1183
|
+
* The exclusion is RE-DERIVED here rather than left as it was: it is derived
|
|
1184
|
+
* state (`exclusionOf`), so copying the previous one forward would keep a
|
|
1185
|
+
* just-measured-good account red until the re-read — and would keep a
|
|
1186
|
+
* just-measured-limited one green, which is the more expensive error.
|
|
1187
|
+
*/
|
|
1188
|
+
function withTestRow(usage, row, nowMs) {
|
|
1189
|
+
if (!("pool" in usage) || usage.pool === void 0) return usage;
|
|
1190
|
+
const pool = usage.pool;
|
|
1191
|
+
const index = indexOfTestedAccount(pool.accounts, row);
|
|
1192
|
+
if (index === void 0) return usage;
|
|
1193
|
+
const measured = pool.accounts[index];
|
|
1194
|
+
if (measured === void 0) return usage;
|
|
1195
|
+
const { excludedBy: _previous, ...account } = measured;
|
|
1196
|
+
const probe = {
|
|
1197
|
+
outcome: row.result.outcome,
|
|
1198
|
+
atMs: nowMs,
|
|
1199
|
+
source: "test-batch",
|
|
1200
|
+
modelId: row.result.modelId,
|
|
1201
|
+
...row.result.retryAtMs === void 0 ? {} : { retryAtMs: row.result.retryAtMs },
|
|
1202
|
+
...row.result.message === void 0 ? {} : { message: row.result.message }
|
|
1203
|
+
};
|
|
1204
|
+
const excludedBy = exclusionOf(probe, nowMs);
|
|
1205
|
+
const folded = {
|
|
1206
|
+
...account,
|
|
1207
|
+
probe,
|
|
1208
|
+
...excludedBy === void 0 ? {} : { excludedBy }
|
|
1209
|
+
};
|
|
1210
|
+
const accounts = pool.accounts.map((candidate, at) => at === index ? folded : candidate);
|
|
1211
|
+
return {
|
|
1212
|
+
...usage,
|
|
1213
|
+
pool: {
|
|
1214
|
+
...pool,
|
|
1215
|
+
accounts
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Which listed account a test row measured, or `undefined` when it measured none
|
|
1221
|
+
* of them.
|
|
1222
|
+
*
|
|
1223
|
+
* Matched by ID first. The NAME is only a fallback for a row whose id this
|
|
1224
|
+
* document no longer lists, and only when exactly one listed account carries it:
|
|
1225
|
+
* two accounts sharing a name must never have one measurement written onto both.
|
|
1226
|
+
*/
|
|
1227
|
+
function indexOfTestedAccount(accounts, row) {
|
|
1228
|
+
const byId = accounts.findIndex((account) => account.accountId === row.accountId);
|
|
1229
|
+
if (byId >= 0) return byId;
|
|
1230
|
+
const matches = accounts.flatMap((account, at) => account.accountName === row.accountName ? [at] : []);
|
|
1231
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
1232
|
+
}
|
|
1233
|
+
//#endregion
|
|
727
1234
|
//#region src/client/credit-format.ts
|
|
728
1235
|
/**
|
|
729
1236
|
* How a credit balance is written on screen.
|
|
@@ -936,9 +1443,9 @@ window.__ModuleLoader__.load({
|
|
|
936
1443
|
}
|
|
937
1444
|
//#endregion
|
|
938
1445
|
//#region src/client/probe-age.ts
|
|
939
|
-
const MINUTE_MS
|
|
940
|
-
const HOUR_MS
|
|
941
|
-
const DAY_MS
|
|
1446
|
+
const MINUTE_MS = 6e4;
|
|
1447
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
1448
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
942
1449
|
/**
|
|
943
1450
|
* How long ago a measurement was taken, coarsely: "刚刚", "N 分钟前",
|
|
944
1451
|
* "N 小时前", "N 天前".
|
|
@@ -954,10 +1461,10 @@ window.__ModuleLoader__.load({
|
|
|
954
1461
|
*/
|
|
955
1462
|
function probeAgeText(t, atMs, nowMs) {
|
|
956
1463
|
const age = nowMs - atMs;
|
|
957
|
-
if (age < MINUTE_MS
|
|
958
|
-
if (age < HOUR_MS
|
|
959
|
-
if (age < 2 * DAY_MS
|
|
960
|
-
return t("row.poolProbeDaysAgo", { count: Math.round(age / DAY_MS
|
|
1464
|
+
if (age < MINUTE_MS) return t("row.poolProbeJustNow");
|
|
1465
|
+
if (age < HOUR_MS) return t("row.poolProbeMinutesAgo", { count: Math.round(age / MINUTE_MS) });
|
|
1466
|
+
if (age < 2 * DAY_MS) return t("row.poolProbeHoursAgo", { count: Math.round(age / HOUR_MS) });
|
|
1467
|
+
return t("row.poolProbeDaysAgo", { count: Math.round(age / DAY_MS) });
|
|
961
1468
|
}
|
|
962
1469
|
//#endregion
|
|
963
1470
|
//#region src/probe.ts
|
|
@@ -993,88 +1500,6 @@ window.__ModuleLoader__.load({
|
|
|
993
1500
|
new RegExp(`将在?\\s*${RESET_STAMP}[\\s\\S]*?重置`, "u");
|
|
994
1501
|
new RegExp(`(?:reset|resets|refresh|refreshes)\\s+(?:at|on)\\s+${RESET_STAMP}`, "iu");
|
|
995
1502
|
//#endregion
|
|
996
|
-
//#region src/client/remaining.ts
|
|
997
|
-
const MINUTE_MS = 6e4;
|
|
998
|
-
const HOUR_MS = 60 * MINUTE_MS;
|
|
999
|
-
const DAY_MS = 24 * HOUR_MS;
|
|
1000
|
-
/**
|
|
1001
|
-
* How far out hours stop being the useful unit.
|
|
1002
|
-
*
|
|
1003
|
-
* Below this, hours; at or above it, days. The cut is at TWO days rather than
|
|
1004
|
-
* one so that 30 hours reads as "约 30 小时后" instead of rounding to "约 1 天后",
|
|
1005
|
-
* which would understate it by six hours.
|
|
1006
|
-
*/
|
|
1007
|
-
const DAYS_AT_MS = 2 * DAY_MS;
|
|
1008
|
-
/**
|
|
1009
|
-
* A short "约 N 分钟后 / 约 N 小时后 / 约 N 天后", or `''` once the moment passed.
|
|
1010
|
-
*
|
|
1011
|
-
* The empty string is the contract callers depend on: an elapsed cooldown means
|
|
1012
|
-
* the account is already back, so appending "in 0 minutes" would assert the
|
|
1013
|
-
* opposite of the state the card is showing. Callers append this only when it is
|
|
1014
|
-
* non-empty.
|
|
1015
|
-
*
|
|
1016
|
-
* Rounding is deliberately coarse — "约 1 小时后" for 69 minutes is what a reader
|
|
1017
|
-
* wants, and a precision that changes every minute would be noise rather than
|
|
1018
|
-
* information. The absolute instant sits right beside it for anyone who needs it.
|
|
1019
|
-
*/
|
|
1020
|
-
function remainingText(t, untilMs, nowMs) {
|
|
1021
|
-
const left = untilMs - nowMs;
|
|
1022
|
-
if (left <= 0) return "";
|
|
1023
|
-
if (left < MINUTE_MS) return t("row.remainingImminent");
|
|
1024
|
-
if (left < HOUR_MS) return t("row.remainingMinutes", { count: Math.round(left / MINUTE_MS) });
|
|
1025
|
-
if (left < DAYS_AT_MS) return t("row.remainingHours", { count: Math.round(left / HOUR_MS) });
|
|
1026
|
-
return t("row.remainingDays", { count: Math.round(left / DAY_MS) });
|
|
1027
|
-
}
|
|
1028
|
-
//#endregion
|
|
1029
|
-
//#region src/account-pool.ts
|
|
1030
|
-
/**
|
|
1031
|
-
* The checked ids that still resolve to a local sign-in, in the caller's order.
|
|
1032
|
-
*
|
|
1033
|
-
* This is THE definition of "effective members", and it exists because the same
|
|
1034
|
-
* predicate was written five separate times: the status document
|
|
1035
|
-
* (`web-status.ts`), the batch route's empty-pool guard, the Host dependency the
|
|
1036
|
-
* card reads, `poolMemberAccounts` and the browser helper. Five copies of one
|
|
1037
|
-
* rule is this project's single most productive bug shape ("fixed one end,
|
|
1038
|
-
* missed the other"), so the rule now has one home and every site delegates to
|
|
1039
|
-
* it.
|
|
1040
|
-
*
|
|
1041
|
-
* ORDER IS THE CALLER'S, and comes from `orderedIds`: the answer preserves the
|
|
1042
|
-
* order of THAT argument. Callers pass whichever list's order they want to show
|
|
1043
|
-
* — the saved list for the user's own order, the store's roster for the account
|
|
1044
|
-
* table's. Neither is silently reshuffled by the filter. (The first parameter was
|
|
1045
|
-
* once named `saved`, which read as "this must be the saved list" even though
|
|
1046
|
-
* `web-status.ts` legitimately passes the roster for store order; the name was
|
|
1047
|
-
* the trap, so it now describes the ROLE instead of the source.)
|
|
1048
|
-
*
|
|
1049
|
-
* Pure, so it is unit-testable without a store, and it never mutates its input.
|
|
1050
|
-
*/
|
|
1051
|
-
function effectiveMembersOf(orderedIds, listedIds) {
|
|
1052
|
-
return orderedIds.filter((id) => listedIds.has(id));
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* A latest-wins guard for one region's asynchronous work.
|
|
1056
|
-
*
|
|
1057
|
-
* Exists because `applyRotation` awaits (it fetches credits per member), so two
|
|
1058
|
-
* overlapping calls can interleave: a slow call that read "rotation is ON" can
|
|
1059
|
-
* resume AFTER a newer call cleared the override for "rotation is OFF" and write
|
|
1060
|
-
* the stale account back — leaving the card showing the pool switched off while
|
|
1061
|
-
* a rotated account is still billed.
|
|
1062
|
-
*
|
|
1063
|
-
* `begin()` claims a new generation and returns a probe; the caller must check
|
|
1064
|
-
* it after EVERY await and bail when it reports stale. Extracted as a pure
|
|
1065
|
-
* factory rather than inlined so the rule is unit-testable: an inlined token is
|
|
1066
|
-
* only observable by reproducing a real interleaving, which is slow and easy to
|
|
1067
|
-
* get subtly wrong (a harness that never actually overlaps passes either way).
|
|
1068
|
-
*/
|
|
1069
|
-
function createLatestWins() {
|
|
1070
|
-
const generations = {};
|
|
1071
|
-
return { begin(key) {
|
|
1072
|
-
const generation = (generations[key] ?? 0) + 1;
|
|
1073
|
-
generations[key] = generation;
|
|
1074
|
-
return () => generations[key] !== generation;
|
|
1075
|
-
} };
|
|
1076
|
-
}
|
|
1077
|
-
//#endregion
|
|
1078
1503
|
//#region src/client/pool-state.ts
|
|
1079
1504
|
/**
|
|
1080
1505
|
* Pure decision helpers for the account pool's card section.
|
|
@@ -1339,47 +1764,19 @@ window.__ModuleLoader__.load({
|
|
|
1339
1764
|
const wanted = new Set(right);
|
|
1340
1765
|
return left.every((id) => wanted.has(id));
|
|
1341
1766
|
}
|
|
1342
|
-
/** Whether a probe outcome means the model answered. */
|
|
1343
|
-
function outcomeOk(outcome) {
|
|
1344
|
-
return outcome === "ok";
|
|
1345
|
-
}
|
|
1346
|
-
/** A short, localizable label for one probe outcome. */
|
|
1347
|
-
function outcomeText(t, outcome) {
|
|
1348
|
-
switch (outcome) {
|
|
1349
|
-
case "ok": return t("row.probeOk");
|
|
1350
|
-
case "rate-limited": return t("row.poolExcludedRateLimited");
|
|
1351
|
-
case "model-busy": return t("row.poolExcludedModelBusy");
|
|
1352
|
-
case "out-of-credit": return t("row.poolExcludedOutOfCredit");
|
|
1353
|
-
case "credential-rejected": return t("row.probeCredentialRejected");
|
|
1354
|
-
case "not-found": return t("row.probeNotFound");
|
|
1355
|
-
case "unavailable": return t("row.probeUnavailable");
|
|
1356
|
-
default: return t("row.probeFailed");
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
function formatNumber$1(value) {
|
|
1360
|
-
return formatCredits(value);
|
|
1361
|
-
}
|
|
1362
1767
|
/**
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
1365
|
-
* `hourCycle: 'h23'` is REQUIRED, not decorative: without it `Intl` follows the
|
|
1366
|
-
* browser locale's preference, and en-US renders `09/30, 01:23 PM`. A wall-clock
|
|
1367
|
-
* glued to "AM/PM" reads as 12-hour no matter what the surrounding UI assumes,
|
|
1368
|
-
* and it is the one thing a Chinese user cannot scan past — the card otherwise
|
|
1369
|
-
* speaks 24-hour everywhere (see `formatClock`).
|
|
1768
|
+
* Whether two id lists are equal IN ORDER.
|
|
1370
1769
|
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1770
|
+
* Unlike `sameIds`, position matters: manual priority is order-sensitive, so a
|
|
1771
|
+
* swapped pair is a real change and must light up "unsaved changes". A set
|
|
1772
|
+
* comparison here would treat reordering as a no-op and drop the user's sort.
|
|
1374
1773
|
*/
|
|
1375
|
-
function
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
hourCycle: "h23"
|
|
1382
|
-
}).format(new Date(value));
|
|
1774
|
+
function sameOrder(left, right) {
|
|
1775
|
+
if (left.length !== right.length) return false;
|
|
1776
|
+
return left.every((id, index) => id === right[index]);
|
|
1777
|
+
}
|
|
1778
|
+
function formatNumber$1(value) {
|
|
1779
|
+
return formatCredits(value);
|
|
1383
1780
|
}
|
|
1384
1781
|
function formatClock$1(value) {
|
|
1385
1782
|
return new Intl.DateTimeFormat(void 0, {
|
|
@@ -1389,12 +1786,6 @@ window.__ModuleLoader__.load({
|
|
|
1389
1786
|
hourCycle: "h23"
|
|
1390
1787
|
}).format(new Date(value));
|
|
1391
1788
|
}
|
|
1392
|
-
/** A dot colour for one account's state. */
|
|
1393
|
-
function stateColor(account) {
|
|
1394
|
-
if (account.excludedBy !== void 0) return account.excludedBy === "rate-limited" || account.excludedBy === "model-busy" || account.excludedBy === "unavailable" ? "var(--dsw-alias-state-warn-primary, #f59e0b)" : "var(--dsw-alias-state-error-primary, #ef4444)";
|
|
1395
|
-
if (account.probe === void 0) return "var(--dsw-alias-label-dimmed, #9aa0a6)";
|
|
1396
|
-
return outcomeOk(account.probe.outcome) ? "var(--dsw-alias-state-success-primary, #22a06b)" : "var(--dsw-alias-state-warn-primary, #f59e0b)";
|
|
1397
|
-
}
|
|
1398
1789
|
/**
|
|
1399
1790
|
* The pool section.
|
|
1400
1791
|
*
|
|
@@ -1403,7 +1794,7 @@ window.__ModuleLoader__.load({
|
|
|
1403
1794
|
* section implying the feature exists.
|
|
1404
1795
|
*/
|
|
1405
1796
|
function AccountPool(props) {
|
|
1406
|
-
const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRescan, rescanning, onSelectAccount, selectingAccount, accountTools
|
|
1797
|
+
const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRescan, rescanning, onSelectAccount, selectingAccount, accountTools } = props;
|
|
1407
1798
|
const [draft, setDraft] = (0, react.useState)(null);
|
|
1408
1799
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
1409
1800
|
const [saveError, setSaveError] = (0, react.useState)(void 0);
|
|
@@ -1414,6 +1805,7 @@ window.__ModuleLoader__.load({
|
|
|
1414
1805
|
setLog(logStore.get(region) ?? []);
|
|
1415
1806
|
}, [region]);
|
|
1416
1807
|
const [draftRegion, setDraftRegion] = (0, react.useState)(void 0);
|
|
1808
|
+
const [orderRulesOpen, setOrderRulesOpen] = (0, react.useState)(false);
|
|
1417
1809
|
const mounted = (0, react.useRef)(true);
|
|
1418
1810
|
(0, react.useEffect)(() => {
|
|
1419
1811
|
mounted.current = true;
|
|
@@ -1425,7 +1817,8 @@ window.__ModuleLoader__.load({
|
|
|
1425
1817
|
enabled: pool?.enabled ?? false,
|
|
1426
1818
|
targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
|
|
1427
1819
|
probeInputTokens: pool?.probeInputTokens ?? 0,
|
|
1428
|
-
memberAccountIds: pool?.memberAccountIds ?? []
|
|
1820
|
+
memberAccountIds: pool?.memberAccountIds ?? [],
|
|
1821
|
+
orderAccountIds: pool?.orderAccountIds ?? []
|
|
1429
1822
|
};
|
|
1430
1823
|
const active = draftRegion === region && draft !== null ? draft : saved;
|
|
1431
1824
|
/**
|
|
@@ -1446,7 +1839,7 @@ window.__ModuleLoader__.load({
|
|
|
1446
1839
|
* the pending edit.
|
|
1447
1840
|
*/
|
|
1448
1841
|
const poolRunning = saved.enabled;
|
|
1449
|
-
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
|
|
1842
|
+
const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || draft.probeInputTokens !== saved.probeInputTokens || !sameIds(draft.memberAccountIds, saved.memberAccountIds) || !sameOrder(draft.orderAccountIds, saved.orderAccountIds));
|
|
1450
1843
|
/**
|
|
1451
1844
|
* Membership that actually resolves to a local sign-in — what a batch runs on.
|
|
1452
1845
|
*
|
|
@@ -1460,6 +1853,13 @@ window.__ModuleLoader__.load({
|
|
|
1460
1853
|
* the field.
|
|
1461
1854
|
*/
|
|
1462
1855
|
const listedIds = new Set((pool?.accounts ?? []).map((account) => account.accountId));
|
|
1856
|
+
const orderIds = (() => {
|
|
1857
|
+
const members = active.memberAccountIds.filter((id) => listedIds.has(id));
|
|
1858
|
+
const explicit = active.orderAccountIds.filter((id) => members.includes(id));
|
|
1859
|
+
const rest = members.filter((id) => !explicit.includes(id));
|
|
1860
|
+
return [...explicit, ...rest];
|
|
1861
|
+
})();
|
|
1862
|
+
const orderIsManual = active.orderAccountIds.some((id) => active.memberAccountIds.includes(id));
|
|
1463
1863
|
const effectiveMembers = effectiveMemberIds({
|
|
1464
1864
|
savedEffective: pool?.effectiveMemberAccountIds ?? [],
|
|
1465
1865
|
draftMembers: active.memberAccountIds,
|
|
@@ -1607,6 +2007,36 @@ window.__ModuleLoader__.load({
|
|
|
1607
2007
|
};
|
|
1608
2008
|
});
|
|
1609
2009
|
}, [editDraft]);
|
|
2010
|
+
/**
|
|
2011
|
+
* Move one member one step up (`-1`) or down (`+1`) in the manual priority.
|
|
2012
|
+
*
|
|
2013
|
+
* The FIRST click is "the user starts sorting manually": it persists the WHOLE
|
|
2014
|
+
* currently-visible order into the draft, not just the one swapped pair. A
|
|
2015
|
+
* click that wrote only the moved id would leave the other rows unsorted (and
|
|
2016
|
+
* `orderIsManual` false), so the draft and the tables they power stay in sync.
|
|
2017
|
+
* `delta` is restricted to ±1 — a single swap per click — which is what makes
|
|
2018
|
+
* the first/last rows' extra buttons (rather than fields) correct.
|
|
2019
|
+
*/
|
|
2020
|
+
const moveMember = (0, react.useCallback)((accountId, delta) => {
|
|
2021
|
+
editDraft((current) => {
|
|
2022
|
+
const listed = new Set((pool?.accounts ?? []).map((account) => account.accountId));
|
|
2023
|
+
const members = current.memberAccountIds.filter((id) => listed.has(id));
|
|
2024
|
+
const explicit = current.orderAccountIds.filter((id) => members.includes(id));
|
|
2025
|
+
const rest = members.filter((id) => !explicit.includes(id));
|
|
2026
|
+
const visible = [...explicit, ...rest];
|
|
2027
|
+
const index = visible.indexOf(accountId);
|
|
2028
|
+
if (index < 0) return current;
|
|
2029
|
+
const to = index + delta;
|
|
2030
|
+
if (to < 0 || to >= visible.length) return current;
|
|
2031
|
+
const next = [...visible];
|
|
2032
|
+
next.splice(index, 1);
|
|
2033
|
+
next.splice(to, 0, accountId);
|
|
2034
|
+
return {
|
|
2035
|
+
...current,
|
|
2036
|
+
orderAccountIds: next
|
|
2037
|
+
};
|
|
2038
|
+
});
|
|
2039
|
+
}, [editDraft, pool]);
|
|
1610
2040
|
/** Check every listed account, or none of them. */
|
|
1611
2041
|
const selectAll = (0, react.useCallback)((next) => {
|
|
1612
2042
|
editDraft((current) => ({
|
|
@@ -1863,7 +2293,7 @@ window.__ModuleLoader__.load({
|
|
|
1863
2293
|
visible: true,
|
|
1864
2294
|
message: t("row.poolSaving"),
|
|
1865
2295
|
hint: t("row.veilHintList")
|
|
1866
|
-
}) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolTitle")),
|
|
2296
|
+
}) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolTitle")), currentName === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-badge" }, t("row.poolCurrentHeader", { account: currentName }))), !poolRunning && onSelectAccount !== void 0 && pool.accounts.length > 0 ? (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolManualAccountLabel")), (0, react.createElement)("span", null, t("row.poolManualAccountHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
|
|
1867
2297
|
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-account-select",
|
|
1868
2298
|
value: currentAccount?.accountId ?? "",
|
|
1869
2299
|
disabled: selectingAccount === true,
|
|
@@ -1915,23 +2345,50 @@ window.__ModuleLoader__.load({
|
|
|
1915
2345
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
1916
2346
|
disabled: effectiveMembers.length === 0 || !canEditPool,
|
|
1917
2347
|
onClick: () => selectAll(false)
|
|
1918
|
-
}, t("row.poolSelectNone")))), (0, react.createElement)("
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2348
|
+
}, t("row.poolSelectNone")))), (0, react.createElement)("details", {
|
|
2349
|
+
className: "dsm-workbuddy-pool-order-rules",
|
|
2350
|
+
open: orderRulesOpen,
|
|
2351
|
+
onToggle: (event) => setOrderRulesOpen(event.currentTarget.open)
|
|
2352
|
+
}, (0, react.createElement)("summary", { className: "dsm-workbuddy-pool-order-rules-summary" }, t("row.poolOrderRulesTitle")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-order-rules-body" }, (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolOrderRulesIntro")), (0, react.createElement)("ol", { className: "dsm-workbuddy-pool-order-rules-list" }, ...ORDER_RULE_KEYS.map((key) => (0, react.createElement)("li", { key }, t(key)))), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-order-rules-why" }, t("row.poolOrderWhy")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolOrderBadgeNote")))), (0, react.createElement)("div", { className: `dsm-workbuddy-pool-table${pool.checkinSupported ? " dsm-workbuddy-pool-table-checkin" : ""}` }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-row dsm-workbuddy-pool-row-head" }, (0, react.createElement)("span", null, t("row.poolColumnAccount")), (0, react.createElement)("span", null, t("row.poolColumnCredits")), (0, react.createElement)("span", null, t("row.poolColumnProbe")), pool.checkinSupported ? (0, react.createElement)("span", null, t("row.poolColumnCheckin")) : null, (0, react.createElement)("span", null)), ...(() => {
|
|
2353
|
+
const ordered = sortAccountsByOrder(pool.accounts, orderIds);
|
|
2354
|
+
let memberSeen = 0;
|
|
2355
|
+
let memberTotal = 0;
|
|
2356
|
+
for (const account of ordered) if (active.memberAccountIds.includes(account.accountId)) memberTotal += 1;
|
|
2357
|
+
return ordered.map((account) => {
|
|
2358
|
+
const isMember = active.memberAccountIds.includes(account.accountId);
|
|
2359
|
+
const left = isMember ? memberSeen : -1;
|
|
2360
|
+
const right = isMember ? memberTotal - 1 - memberSeen : -1;
|
|
2361
|
+
if (isMember) memberSeen += 1;
|
|
2362
|
+
return renderAccountRow({
|
|
2363
|
+
t,
|
|
2364
|
+
account,
|
|
2365
|
+
checked: isMember,
|
|
2366
|
+
canEdit: canEditPool,
|
|
2367
|
+
checkinSupported: pool.checkinSupported,
|
|
2368
|
+
onToggle: (next) => toggleMember(account.accountId, next),
|
|
2369
|
+
busy: rowBusyAccountId !== void 0,
|
|
2370
|
+
testDisabled: pool.targetModelSource === "none" || pool.targetModelSource === "stale",
|
|
2371
|
+
targetHint: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
|
|
2372
|
+
onTest: () => {
|
|
2373
|
+
runRowTest(account.accountId);
|
|
2374
|
+
},
|
|
2375
|
+
onRemove: () => {
|
|
2376
|
+
runRowRemove(account.accountId, account.accountName);
|
|
2377
|
+
},
|
|
2378
|
+
orderIndex: isMember && orderIsManual ? left + 1 : void 0,
|
|
2379
|
+
orderIsManual,
|
|
2380
|
+
moveEnabled: pool.enabled === true && isMember && canEditPool,
|
|
2381
|
+
onMoveUp: () => {
|
|
2382
|
+
moveMember(account.accountId, -1);
|
|
2383
|
+
},
|
|
2384
|
+
onMoveDown: () => {
|
|
2385
|
+
moveMember(account.accountId, 1);
|
|
2386
|
+
},
|
|
2387
|
+
moveUpDisabled: left <= 0,
|
|
2388
|
+
moveDownDisabled: right <= 0
|
|
2389
|
+
});
|
|
2390
|
+
});
|
|
2391
|
+
})()), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
|
|
1935
2392
|
className: "dsm-workbuddy-pool-warn",
|
|
1936
2393
|
role: "status"
|
|
1937
2394
|
}, `${t("row.poolNoneSelected")} ${t("row.poolNoneSelectedHint")}`) : null, ghostMembers$1.length === 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-ghosts" }, (0, react.createElement)("p", {
|
|
@@ -1997,12 +2454,11 @@ window.__ModuleLoader__.load({
|
|
|
1997
2454
|
}
|
|
1998
2455
|
/** One account's row. */
|
|
1999
2456
|
function renderAccountRow(input) {
|
|
2000
|
-
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove } = input;
|
|
2457
|
+
const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove, orderIndex, orderIsManual, moveEnabled, onMoveUp, onMoveDown, moveUpDisabled, moveDownDisabled } = input;
|
|
2001
2458
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
2002
2459
|
const excluded = exclusionText(t, account.excludedBy);
|
|
2003
2460
|
const probe = account.probe;
|
|
2004
|
-
const outcomeLine =
|
|
2005
|
-
const remaining = probe?.retryAtMs === void 0 ? "" : remainingText(t, probe.retryAtMs, Date.now());
|
|
2461
|
+
const outcomeLine = availabilityLine(t, account) ?? t("row.poolNeverTested");
|
|
2006
2462
|
const reason = probe === void 0 ? void 0 : inlineProbeReason(probe.outcome, probe.message);
|
|
2007
2463
|
const age = probe === void 0 ? void 0 : probeAgeText(t, probe.atMs, Date.now());
|
|
2008
2464
|
const probeLabel = probe === void 0 ? void 0 : probe.source === void 0 ? {
|
|
@@ -2015,11 +2471,7 @@ window.__ModuleLoader__.load({
|
|
|
2015
2471
|
text: t("row.poolProbeSourceTest"),
|
|
2016
2472
|
className: "dsm-workbuddy-pool-prov-tag"
|
|
2017
2473
|
};
|
|
2018
|
-
const probeLine = [
|
|
2019
|
-
outcomeLine,
|
|
2020
|
-
remaining,
|
|
2021
|
-
reason
|
|
2022
|
-
].filter((part) => part !== void 0 && part !== "").join(" · ");
|
|
2474
|
+
const probeLine = [outcomeLine, reason].filter((part) => part !== void 0 && part !== "").join(" · ");
|
|
2023
2475
|
return (0, react.createElement)("div", {
|
|
2024
2476
|
className: `dsm-workbuddy-pool-row${account.current ? " dsm-workbuddy-pool-row-current" : ""}`,
|
|
2025
2477
|
key: account.accountId
|
|
@@ -2032,7 +2484,27 @@ window.__ModuleLoader__.load({
|
|
|
2032
2484
|
})), (0, react.createElement)("span", {
|
|
2033
2485
|
className: "dsm-workbuddy-usage-dot",
|
|
2034
2486
|
style: { background: stateColor(account) }
|
|
2035
|
-
}), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-account-name" }, (0, react.createElement)("b", null, name),
|
|
2487
|
+
}), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-account-name" }, (0, react.createElement)("b", null, name), checked ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-order" }, orderIsManual ? (0, react.createElement)("span", {
|
|
2488
|
+
className: "dsm-workbuddy-pool-rank",
|
|
2489
|
+
title: t("row.poolRankManualTitle")
|
|
2490
|
+
}, t("row.poolRankLabel", { rank: orderIndex ?? 0 })) : (0, react.createElement)("span", {
|
|
2491
|
+
className: "dsm-workbuddy-pool-rank dsm-workbuddy-pool-rank-auto",
|
|
2492
|
+
title: t("row.poolRankAutoTitle")
|
|
2493
|
+
}, t("row.poolRankAuto")), moveEnabled ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-move" }, (0, react.createElement)("button", {
|
|
2494
|
+
type: "button",
|
|
2495
|
+
className: "dsm-workbuddy-pool-move-btn",
|
|
2496
|
+
title: t("row.poolMoveUpTitle"),
|
|
2497
|
+
"aria-label": t("row.poolMoveUpAria", { accountName: name }),
|
|
2498
|
+
disabled: moveUpDisabled,
|
|
2499
|
+
onClick: onMoveUp
|
|
2500
|
+
}, "↑"), (0, react.createElement)("button", {
|
|
2501
|
+
type: "button",
|
|
2502
|
+
className: "dsm-workbuddy-pool-move-btn",
|
|
2503
|
+
title: t("row.poolMoveDownTitle"),
|
|
2504
|
+
"aria-label": t("row.poolMoveDownAria", { accountName: name }),
|
|
2505
|
+
disabled: moveDownDisabled,
|
|
2506
|
+
onClick: onMoveDown
|
|
2507
|
+
}, "↓")) : null) : null, account.current ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-tag" }, t("row.poolCurrentBadge")) : null, (0, react.createElement)("span", { className: checked ? "dsm-workbuddy-pool-member" : "dsm-workbuddy-pool-not-member" }, checked ? t("row.poolMember") : t("row.poolNotMember")), excluded === void 0 ? probe === void 0 ? null : (0, react.createElement)("span", null, t("row.poolTestedAt", { at: formatShortClock(probe.atMs) })) : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-excluded" }, excluded))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-credits" }, account.credits === void 0 ? (0, react.createElement)("span", null, "—") : (0, react.createElement)("b", null, formatNumber$1(account.credits)), account.expiringSoon !== void 0 && account.expiringSoon > 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon" }, t("row.poolCreditSoon", { count: formatNumber$1(account.expiringSoon) })) : account.nearestExpiryMs === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon-plain" }, t("row.poolCreditNearest", { at: formatShortClock(account.nearestExpiryMs) }))), (0, react.createElement)("span", {
|
|
2036
2508
|
className: "dsm-workbuddy-pool-probe",
|
|
2037
2509
|
...probe?.message === void 0 ? {} : { title: probe.message }
|
|
2038
2510
|
}, (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", {
|
|
@@ -2134,6 +2606,61 @@ window.__ModuleLoader__.load({
|
|
|
2134
2606
|
key: `${entry.atMs}-${index}`
|
|
2135
2607
|
}, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-log-time" }, formatClock$1(entry.atMs)), (0, react.createElement)("span", { className: `dsm-workbuddy-pool-log-text dsm-workbuddy-pool-log-${entry.tone}` }, entry.text)))));
|
|
2136
2608
|
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Sort accounts by the manual priority order, without losing stability.
|
|
2611
|
+
*
|
|
2612
|
+
* Helpers/rows are rendered in this order so a click on ↑/↓ reorders the row
|
|
2613
|
+
* immediately (client-side), before any save + host round-trip. Ids in `orderIds`
|
|
2614
|
+
* that are not actually members are skipped; members not listed fall back to the
|
|
2615
|
+
* END in their original relative order.
|
|
2616
|
+
*
|
|
2617
|
+
* The sort is STABLE: two ids with the same priority rank keep their original
|
|
2618
|
+
* array order. A `rank` lookup returns `orderIds.length` as the sentinel for
|
|
2619
|
+
* "not ordered", which is larger than any real rank, so unlisted members sort
|
|
2620
|
+
* after every listed one. An EMPTY `orderIds` short-circuits: the input order is
|
|
2621
|
+
* already the answer, and we avoid building the rank map / comparator for the
|
|
2622
|
+
* common (never-sorted) case.
|
|
2623
|
+
*
|
|
2624
|
+
* TWO subtleties that a naive rank comparator gets wrong, both of which would
|
|
2625
|
+
* make the table visibly wrong:
|
|
2626
|
+
*
|
|
2627
|
+
* - DUPLICATE ids keep the FIRST occurrence's rank (`if (!rank.has(id))`).
|
|
2628
|
+
* Overwriting instead would let a stray duplicate silently push the account
|
|
2629
|
+
* it names to the duplicate's later position, so the row would drift away
|
|
2630
|
+
* from the badge that claims to describe it.
|
|
2631
|
+
* - The sort key is the PAIR (rank, original index), not rank alone. Ranks are
|
|
2632
|
+
* unique per id, but the `Infinity` fallback is shared by every unlisted
|
|
2633
|
+
* account, and `Array.prototype.sort` is NOT guaranteed stable across engines
|
|
2634
|
+
* for arbitrary inputs; relying on it would let the unlisted tail reshuffle
|
|
2635
|
+
* between renders. Breaking the tie on the original index makes the result a
|
|
2636
|
+
* pure function of the input, which is what stops the table from jittering
|
|
2637
|
+
* when the same snapshot arrives twice.
|
|
2638
|
+
*/
|
|
2639
|
+
/**
|
|
2640
|
+
* The ordering rules, in EXACTLY the comparator's priority order.
|
|
2641
|
+
*
|
|
2642
|
+
* Kept as one table rather than inline literals so the order is a single thing
|
|
2643
|
+
* a reader can compare against `src/account-pool.ts` — the rule text and the
|
|
2644
|
+
* code must never drift, and a flat list makes that diff trivial. Typed as
|
|
2645
|
+
* `WorkBuddySettingsKey` so a renamed or removed locale key is a compile error
|
|
2646
|
+
* instead of a raw key rendered to the user.
|
|
2647
|
+
*/
|
|
2648
|
+
const ORDER_RULE_KEYS = [
|
|
2649
|
+
"row.poolOrderRuleUsable",
|
|
2650
|
+
"row.poolOrderRuleExpiring",
|
|
2651
|
+
"row.poolOrderRuleManual",
|
|
2652
|
+
"row.poolOrderRuleNearestExpiry",
|
|
2653
|
+
"row.poolOrderRuleExpiringAmount",
|
|
2654
|
+
"row.poolOrderRuleTotal",
|
|
2655
|
+
"row.poolOrderRuleFreshness",
|
|
2656
|
+
"row.poolOrderRuleId"
|
|
2657
|
+
];
|
|
2658
|
+
function sortAccountsByOrder(accounts, orderIds) {
|
|
2659
|
+
return applyManualOrder(accounts.map((account) => ({
|
|
2660
|
+
id: account.accountId,
|
|
2661
|
+
account
|
|
2662
|
+
})), orderIds).map((entry) => entry.account);
|
|
2663
|
+
}
|
|
2137
2664
|
//#endregion
|
|
2138
2665
|
//#region src/client/OAuthSignIn.tsx
|
|
2139
2666
|
/**
|
|
@@ -3047,33 +3574,105 @@ window.__ModuleLoader__.load({
|
|
|
3047
3574
|
.dsm-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
3048
3575
|
.dsm-btn-primary:hover:not(:disabled){opacity:.9}
|
|
3049
3576
|
.dsm-workbuddy-usage{display:flex;flex-direction:column;gap:16px;margin:0;padding:16px 0 4px}
|
|
3050
|
-
|
|
3051
|
-
|
|
3577
|
+
/* The provider bar: one CARD per region, side by side.
|
|
3578
|
+
*
|
|
3579
|
+
* COPIED FROM dsh-connect-trae, rule for rule, because an approximation of it
|
|
3580
|
+
* read as a different component. An earlier revision marked the selected card
|
|
3581
|
+
* with 1px border PLUS an inset 1.5px hairline in the contrast colour, and
|
|
3582
|
+
* that is 2.5 CSS px of near-black on a 2x display — five device pixels of
|
|
3583
|
+
* frame against the reference's one. What the reference does instead:
|
|
3584
|
+
*
|
|
3585
|
+
* - selected = the accent COLOUR on the same 1px border, plus a slightly
|
|
3586
|
+
* darker fill. The LIGHTNESS carries the state, not the thickness.
|
|
3587
|
+
* - the switch is separated from the label by a hairline RULE inside the
|
|
3588
|
+
* card (border-left in border-l1), not by dead space. That rule is what
|
|
3589
|
+
* makes the switch read as a second control in the same box rather than
|
|
3590
|
+
* as a floating square at the card's right edge.
|
|
3591
|
+
* - the cell owns its padding (8px 12px) and the tab button has none, so
|
|
3592
|
+
* the label's text sits at a predictable inset no matter how the button's
|
|
3593
|
+
* focus ring draws.
|
|
3594
|
+
* - 10px corner radius, matching the reference's card shape.
|
|
3595
|
+
*
|
|
3596
|
+
* The row stays what it was semantically: role="tablist" with one button per
|
|
3597
|
+
* region, and the checkbox inside each card is a SEPARATE control (switch this
|
|
3598
|
+
* provider on/off) — never merged into the tab action. Both regions can be on
|
|
3599
|
+
* at once, so this must not become a radio group just because the cards look
|
|
3600
|
+
* like one.
|
|
3601
|
+
*
|
|
3602
|
+
* The checkbox is the PLATFORM'S OWN, styled only with accent-color — the
|
|
3603
|
+
* same three declarations the reference card has, no more. An earlier revision
|
|
3604
|
+
* drew it here (appearance:none, 15px square, 3px radius, a tick made of two
|
|
3605
|
+
* rotated borders) on the theory that pinning the geometry would match the
|
|
3606
|
+
* reference across platforms. It did not: the reference does not draw it, so
|
|
3607
|
+
* the drawn box was a second, invented control sitting in the same row as the
|
|
3608
|
+
* real one. Measured against the screenshot, its box is 26 device px on a 2x
|
|
3609
|
+
* display — 13 CSS px, which is the engine's own default size, not a number
|
|
3610
|
+
* anyone wrote down.
|
|
3611
|
+
*
|
|
3612
|
+
* accent-color hands size, radius and tick glyph to the platform. That is the
|
|
3613
|
+
* point: the box then follows the reader's OS and theme, which is what makes
|
|
3614
|
+
* it read as a checkbox rather than as a painted square. */
|
|
3615
|
+
.dsm-workbuddy-tabs{display:flex;gap:8px;align-items:stretch}
|
|
3616
|
+
.dsm-workbuddy-tab-cell{display:flex;align-items:center;gap:4px;flex:1;min-width:0;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;background:var(--dsw-alias-bg-layer-3,#2a2c33);transition:border-color .15s,background .15s}
|
|
3617
|
+
.dsm-workbuddy-tab-cell:hover{border-color:var(--dsw-alias-label-dimmed,#777)}
|
|
3618
|
+
.dsm-workbuddy-tab-cell:has(.dsm-workbuddy-tab-active){border-color:var(--dsw-alias-brand-primary,#5686fe);background:var(--dsw-alias-bg-layer-2,#232529)}
|
|
3619
|
+
.dsm-workbuddy-tab{appearance:none;font:inherit;cursor:pointer;flex:1;min-width:0;border:0;border-radius:7px;padding:0;color:var(--dsw-alias-label-tertiary,#999);font-size:13px;font-weight:600;line-height:20px;text-align:left;background:transparent;transition:color .15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3052
3620
|
.dsm-workbuddy-tab:hover:not(:disabled):not(.dsm-workbuddy-tab-active){color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
3053
3621
|
.dsm-workbuddy-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
|
|
3054
|
-
.dsm-workbuddy-tab-active{color:var(--dsw-alias-label-primary,#e6e6e6)
|
|
3055
|
-
.dsm-workbuddy-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:
|
|
3056
|
-
.
|
|
3057
|
-
|
|
3622
|
+
.dsm-workbuddy-tab-active{color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
3623
|
+
.dsm-workbuddy-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:7px;vertical-align:baseline}
|
|
3624
|
+
/* The hairline rule between the label and the switch. The auto left margin is
|
|
3625
|
+
what pushes the switch to the card's right edge; without it the rule would
|
|
3626
|
+
sit tight against the text. */
|
|
3627
|
+
.dsm-workbuddy-tab-switch{display:inline-flex;align-items:center;flex:none;margin-left:auto;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));cursor:pointer}
|
|
3058
3628
|
.dsm-workbuddy-tab-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
3059
|
-
.dsm-workbuddy-tab-switch input:disabled{
|
|
3060
|
-
/*
|
|
3061
|
-
the
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
.
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3629
|
+
.dsm-workbuddy-tab-switch input:disabled{cursor:default}
|
|
3630
|
+
/* PREFERENCE LINES: a checkbox on a line that commits on the click, packed from
|
|
3631
|
+
the LEFT — the shape the reference card uses for this exact block.
|
|
3632
|
+
*
|
|
3633
|
+
* They belong to the TAB block above and carry no heading of their own, because
|
|
3634
|
+
* both switches describe THIS tab. Earlier revisions gave them a headed section,
|
|
3635
|
+
* which meant a second 「积分」 heading one line under the tab hint; the reference
|
|
3636
|
+
* card never had one either.
|
|
3637
|
+
*
|
|
3638
|
+
* The picker used to sit at the row's far edge, pushed there by
|
|
3639
|
+
* justify-content:space-between. That split one sentence into two objects with
|
|
3640
|
+
* half a card of empty space between them, and left the reader to guess which
|
|
3641
|
+
* label the control belonged to. Here nothing in a row stretches: the words ask
|
|
3642
|
+
* the question and the control answers right beside them.
|
|
3643
|
+
*
|
|
3644
|
+
* align-items:flex-start, not center: the hint wraps to two lines at narrow
|
|
3645
|
+
* widths, and centring would float the checkbox against the middle of it.
|
|
3646
|
+
*
|
|
3647
|
+
* No horizontal inset: the reference card pads this block by the same 2px its
|
|
3648
|
+
* tab hint carries, so box and caption share one left edge. THIS card's tab hint
|
|
3649
|
+
* (the dsm-workbuddy-models-summary class) has no side padding, so copying the
|
|
3650
|
+
* reference's 2px would leave the two edges 2px apart — a difference too small
|
|
3651
|
+
* to name and just big enough to look unaligned. */
|
|
3652
|
+
.dsm-workbuddy-prefs{display:flex;flex-direction:column;gap:14px;padding:2px 0 0}
|
|
3653
|
+
.dsm-workbuddy-pref{display:flex;align-items:flex-start;gap:10px;font-size:13px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0);cursor:pointer}
|
|
3654
|
+
/* The platform's own checkbox, themed only with accent-color. A hand-drawn box
|
|
3655
|
+
was tried on this card and reverted: brand-primary resolves to the host's
|
|
3656
|
+
CONTRAST colour, so a ticked box painted itself as a heavy black pill — the
|
|
3657
|
+
loudest object in a section made of plain 13px text. */
|
|
3658
|
+
.dsm-workbuddy-pref input{flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
3659
|
+
.dsm-workbuddy-pref-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
3660
|
+
.dsm-workbuddy-pref input:disabled{cursor:default}
|
|
3661
|
+
.dsm-workbuddy-pref:has(input:disabled){opacity:.6;cursor:default}
|
|
3662
|
+
/* The NAME and what it does are TWO lines, not one wrapped blob: first what the
|
|
3663
|
+
switch does, then what it means. Running them together is what made this
|
|
3664
|
+
block read as a wall of text. */
|
|
3665
|
+
.dsm-workbuddy-pref-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
3666
|
+
.dsm-workbuddy-pref-label{font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
3667
|
+
/* The picker's own line: indented by the checkbox's width plus the row gap
|
|
3668
|
+
(15px + 10px), so its label starts on the same left edge as the checkbox
|
|
3669
|
+
labels above it rather than under their boxes. */
|
|
3670
|
+
.dsm-workbuddy-pref-indent{padding-left:25px}
|
|
3671
|
+
/* Name then control, packed left — 显示位置 [输入框工具栏] — so the row reads as
|
|
3672
|
+
one sentence instead of a text column facing a control column. */
|
|
3673
|
+
.dsm-workbuddy-pref-lead{display:flex;align-items:center;justify-content:flex-start;gap:10px;min-width:0}
|
|
3674
|
+
.dsm-workbuddy-pref-lead .dsm-workbuddy-pref-label{flex:none}
|
|
3675
|
+
.dsm-workbuddy-pref-hint{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:17px}
|
|
3077
3676
|
/* The supplement to the host's fixed quota sentence (issue #63). The plugin no
|
|
3078
3677
|
longer claims the host's shell.quota-notice chain seat, so this no longer
|
|
3079
3678
|
paints into shell.overlay — that seat is the reason the previous build's
|
|
@@ -3084,11 +3683,34 @@ window.__ModuleLoader__.load({
|
|
|
3084
3683
|
The body is one step quieter than the text it follows. */
|
|
3085
3684
|
.dsm-workbuddy-quota-notice{display:inline}
|
|
3086
3685
|
.dsm-workbuddy-quota-notice-body{color:var(--dsw-alias-label-tertiary,#8b8f98);font-size:12px}
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3686
|
+
/* The seat picker is the PLATFORM's select, sized by its own two options rather
|
|
3687
|
+
* than stretched: the control answers one short question and sits beside the
|
|
3688
|
+
* words that ask it.
|
|
3689
|
+
*
|
|
3690
|
+
* This was drawn by hand for one revision — appearance:none, a layer-2 panel and
|
|
3691
|
+
* a chevron built from two rotated gradients — copying a reference screenshot's
|
|
3692
|
+
* white field. That does not survive the trip: the screenshot was a LIGHT
|
|
3693
|
+
* settings page, this card is dark, and the drawn field ended up the only
|
|
3694
|
+
* element here with its own fill, border and arrow, so it read as a form control
|
|
3695
|
+
* pasted into a paragraph. The reference card's own dark build uses the native
|
|
3696
|
+
* control with a hairline border, which is what this rule is.
|
|
3697
|
+
*
|
|
3698
|
+
* Sized by content, not by a min-width: it used to carry min-width:132px so it
|
|
3699
|
+
* could share a heading row with the account badge, and that floor is now the
|
|
3700
|
+
* reason a two-word option would still leave a gap after the word it answers. */
|
|
3701
|
+
.dsm-workbuddy-credit-position{flex:none;max-width:none;margin-top:0;padding:3px 8px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:7px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:12px;line-height:16px;cursor:pointer}
|
|
3702
|
+
.dsm-workbuddy-credit-position:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,#777)}
|
|
3703
|
+
.dsm-workbuddy-credit-position:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
|
|
3704
|
+
.dsm-workbuddy-credit-position:disabled{opacity:.5;cursor:default}
|
|
3705
|
+
/* A card whose provider is switched OFF dims its LABEL AND DOT. The checkbox
|
|
3706
|
+
* beside them must stay at full strength, because that checkbox is the only
|
|
3707
|
+
* way to switch the region back on — dimming it would grey out the control
|
|
3708
|
+
* that fixes the state it is reporting. Its own disabled rule still dims it
|
|
3709
|
+
* when writes are unavailable, which is a different condition and reads as a
|
|
3710
|
+
* different thing.
|
|
3711
|
+
*
|
|
3712
|
+
* That is also what the reference card does: it marks the off state on the
|
|
3713
|
+
* tab button and leaves the switch live. */
|
|
3092
3714
|
.dsm-workbuddy-tab-off{opacity:.55}
|
|
3093
3715
|
/* Re-detection keeps a copy in the signed-out branch (that branch is where
|
|
3094
3716
|
"I just signed in over there" applies), so it needs its own placement: the
|
|
@@ -3187,14 +3809,19 @@ window.__ModuleLoader__.load({
|
|
|
3187
3809
|
|
|
3188
3810
|
/* ---- Account pool ---- */
|
|
3189
3811
|
.dsm-workbuddy-pool{display:flex;flex-direction:column;gap:12px;margin:0;padding:16px 0 4px;border-top:1px solid var(--dsw-alias-border-l2,#36373b)}
|
|
3190
|
-
/*
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3812
|
+
/* The block's heading: title on the left, serving-account badge on the right.
|
|
3813
|
+
*
|
|
3814
|
+
* It used to hold a THIRD child — the credit-readout switch, slotted in between
|
|
3815
|
+
* the title and the badge by the card. A control that configures something else
|
|
3816
|
+
* spent several revisions sitting inside this block's title, and it forced the
|
|
3817
|
+
* row onto a shared baseline with a picker whose width shoved the badge to the
|
|
3818
|
+
* far edge. The switch now has its own section above; what is left is the one
|
|
3819
|
+
* thing that is genuinely this block's title row.
|
|
3820
|
+
*
|
|
3821
|
+
* The badge is pushed right with margin-left:auto rather than the row using
|
|
3822
|
+
* space-between, so it stays on the right edge even if a future change adds a
|
|
3823
|
+
* child or removes one. */
|
|
3196
3824
|
.dsm-workbuddy-pool-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
3197
|
-
.dsm-workbuddy-pool-head-extra{display:inline-flex;align-items:center}
|
|
3198
3825
|
.dsm-workbuddy-pool-current-badge{margin-left:auto;padding:2px 10px;border-radius:999px;background:rgba(86,134,254,.16);color:var(--dsw-alias-brand-primary,#5686fe);font-size:12px;font-weight:600;line-height:18px;white-space:nowrap}
|
|
3199
3826
|
/* Sits under the header block, before the batch buttons: account discovery is
|
|
3200
3827
|
the first step of using the pool, so it reads above the actions it enables. */
|
|
@@ -3243,7 +3870,48 @@ window.__ModuleLoader__.load({
|
|
|
3243
3870
|
/* Declared AFTER the blanket span rule so it wins: the badge must not inherit
|
|
3244
3871
|
the muted 11px tertiary treatment the membership line uses. */
|
|
3245
3872
|
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-current-tag{align-self:flex-start;margin-top:2px;padding:1px 6px;border-radius:999px;background:rgba(86,134,254,.16);color:var(--dsw-alias-brand-primary,#5686fe);font-size:11px;font-weight:600;line-height:16px;white-space:nowrap}
|
|
3873
|
+
/* Manual-priority chrome (order badge + up/down arrows). The rank badge is a SPAN,
|
|
3874
|
+
not a button: it only DESCRIBES the current order and must not read as a
|
|
3875
|
+
control (nor join the row's button list). The solid vs dashed distinction is
|
|
3876
|
+
load-bearing — a numeric rank must NOT appear when the credit policy still
|
|
3877
|
+
decides the order, so the -auto modifier is the dashed/grey state. */
|
|
3878
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-order{display:flex;align-items:center;gap:6px;margin-top:2px;flex-wrap:wrap}
|
|
3879
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-rank{display:inline-flex;align-items:center;border-radius:999px;border:1px solid rgba(86,134,254,.32);background:rgba(86,134,254,.12);color:var(--dsw-alias-brand-primary,#5686fe);font-size:11px;font-weight:600;line-height:16px;padding:1px 6px;white-space:nowrap;cursor:default;font-variant-numeric:tabular-nums}
|
|
3880
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-rank-auto{border:1px dashed var(--dsw-alias-border-l1,#2c2d31);background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3881
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-move{display:inline-flex;align-items:center;gap:2px}
|
|
3882
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-move-btn{width:18px;height:18px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;border:1px solid var(--dsw-alias-border-l1,#2c2d31);background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:1;cursor:pointer}
|
|
3883
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-move-btn:disabled{opacity:.4;cursor:default}
|
|
3884
|
+
/* Hover/focus must change BOTH halves of the contrast pair.
|
|
3885
|
+
The rule this replaces recoloured only the glyph — to
|
|
3886
|
+
--dsw-alias-label-primary (#e6e6e6) — and left the background as it was. In
|
|
3887
|
+
the LIGHT theme that token is near-WHITE, so on hover the arrow painted
|
|
3888
|
+
white-on-light and vanished under the cursor (reported as "the up arrow
|
|
3889
|
+
disappears when I hover"). Recolouring text without also repainting its
|
|
3890
|
+
backdrop is unsafe whenever the two come from independent tokens, so this
|
|
3891
|
+
sets an explicit tinted background and keeps the glyph on the brand colour,
|
|
3892
|
+
which is legible in both themes. The outline is a third, theme-independent
|
|
3893
|
+
affordance so the target is findable even if a token is remapped. */
|
|
3894
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-move-btn:not(:disabled):hover,
|
|
3895
|
+
.dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-move-btn:not(:disabled):focus-visible{background:rgba(86,134,254,.16);border-color:rgba(86,134,254,.55);color:var(--dsw-alias-brand-primary,#5686fe);outline:2px solid rgba(86,134,254,.45);outline-offset:1px}
|
|
3246
3896
|
.dsm-workbuddy-pool-excluded{color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
3897
|
+
/* The ordering-rule disclosure, placed directly above the member table.
|
|
3898
|
+
A native <details>: the marker is left to the browser so the control keeps
|
|
3899
|
+
its built-in keyboard and screen-reader behaviour. The summary is a full-width
|
|
3900
|
+
row because a 12px grey label on its own is a small click target, and this is
|
|
3901
|
+
the one place a confused user is meant to click. */
|
|
3902
|
+
.dsm-workbuddy-pool-order-rules{border:1px solid var(--dsw-alias-border-l1,#2c2d31);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#232529);margin:0 0 10px;overflow:hidden}
|
|
3903
|
+
.dsm-workbuddy-pool-order-rules-summary{padding:8px 12px;cursor:pointer;user-select:none;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;font-weight:600;line-height:18px}
|
|
3904
|
+
.dsm-workbuddy-pool-order-rules-summary:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
3905
|
+
.dsm-workbuddy-pool-order-rules-summary:focus-visible{outline:2px solid rgba(86,134,254,.55);outline-offset:-2px}
|
|
3906
|
+
.dsm-workbuddy-pool-order-rules-body{display:flex;flex-direction:column;gap:8px;padding:0 12px 12px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31);padding-top:10px}
|
|
3907
|
+
/* The rules already carry their own "1." … "8." inside the sentence, so the <ol>
|
|
3908
|
+
is semantic only. Suppressing the marker keeps a single number on screen
|
|
3909
|
+
instead of "1. 1. …". */
|
|
3910
|
+
.dsm-workbuddy-pool-order-rules-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3911
|
+
.dsm-workbuddy-pool-order-rules-list li{padding-left:2px}
|
|
3912
|
+
/* The "why expiring credits win" note is the sentence users push back on, so
|
|
3913
|
+
it is tinted rather than buried in the same grey as the eight rules. */
|
|
3914
|
+
.dsm-workbuddy-pool-order-rules-why{margin:0;padding:8px 10px;border-radius:8px;border-left:3px solid var(--dsw-alias-state-warn-primary,#f59e0b);background:rgba(245,158,11,.10);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
3247
3915
|
.dsm-workbuddy-pool-credits{display:flex;flex-direction:column;gap:1px;min-width:0;font-variant-numeric:tabular-nums}
|
|
3248
3916
|
.dsm-workbuddy-pool-credits b{color:var(--dsw-alias-label-primary,#e6e6e6);font-weight:600}
|
|
3249
3917
|
.dsm-workbuddy-pool-soon{font-size:11px;line-height:16px;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
@@ -3523,11 +4191,24 @@ window.__ModuleLoader__.load({
|
|
|
3523
4191
|
instead of the name. */
|
|
3524
4192
|
.dsm-workbuddy-composer-panel-who{display:flex;flex-direction:column;min-width:0;gap:1px}
|
|
3525
4193
|
.dsm-workbuddy-composer-panel-name{overflow:hidden;text-overflow:ellipsis}
|
|
3526
|
-
/*
|
|
3527
|
-
|
|
4194
|
+
/* The second line under the name: TWO facts, two tones.
|
|
4195
|
+
.dsm-workbuddy-composer-panel-mark-current -- "当前选用", green: it says which
|
|
4196
|
+
account is serving this tab, which is not a problem and must not be painted
|
|
4197
|
+
like one. The owner asked for exactly this ("框出来的,当前选用用绿色的字").
|
|
4198
|
+
.dsm-workbuddy-composer-panel-mark-reason -- why it cannot serve ("被限流"),
|
|
4199
|
+
in the warn colour the pool table uses for the same fact, so one
|
|
4200
|
+
measurement reads the same on both surfaces.
|
|
4201
|
+
The two are siblings in a flex line and the separator between them is drawn
|
|
4202
|
+
here, not in the text: rendered text stays exactly "当前选用 · 被限流 · 约 6
|
|
4203
|
+
小时后" for anything that reads the DOM as a string. */
|
|
3528
4204
|
.dsm-workbuddy-composer-panel-mark{
|
|
4205
|
+
display:flex;flex-wrap:wrap;align-items:baseline;
|
|
3529
4206
|
font-size:11px;line-height:15px;
|
|
3530
|
-
|
|
4207
|
+
}
|
|
4208
|
+
.dsm-workbuddy-composer-panel-mark-current{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
4209
|
+
.dsm-workbuddy-composer-panel-mark-reason{color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
4210
|
+
.dsm-workbuddy-composer-panel-mark-current+.dsm-workbuddy-composer-panel-mark-reason::before{
|
|
4211
|
+
content:" · ";color:var(--dsw-alias-label-dimmed,rgba(127,127,127,.6));
|
|
3531
4212
|
}
|
|
3532
4213
|
/* Hover belongs to the switch, not the row: the number cell is not clickable,
|
|
3533
4214
|
so lighting it up would promise an action it cannot perform. */
|
|
@@ -3542,9 +4223,21 @@ window.__ModuleLoader__.load({
|
|
|
3542
4223
|
flex:none;width:7px;height:7px;border-radius:50%;
|
|
3543
4224
|
background:var(--dsw-alias-label-dimmed,rgba(127,127,127,.5));
|
|
3544
4225
|
}
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
4226
|
+
/* Availability, matching the pool table's dot EXACTLY (see
|
|
4227
|
+
account-state.ts, stateTone, which is the one place the rule lives).
|
|
4228
|
+
Three states, and colour answers only the first question: can this account
|
|
4229
|
+
serve the next message? The second question -- which account is serving this
|
|
4230
|
+
tab -- is answered in words on the row ("current"), so the colour is free to
|
|
4231
|
+
stay honest about usability. Green for usable (selected or not), red for a
|
|
4232
|
+
refusal of any kind (a stated rate limit included: amber used to sit here and
|
|
4233
|
+
read as "mostly fine"), and the neutral grey above for an account nobody has
|
|
4234
|
+
measured yet, which is neither.
|
|
4235
|
+
NOTE: no backticks in this comment. The whole stylesheet is a template
|
|
4236
|
+
literal, and a stray backtick silently closes it (the file then fails to
|
|
4237
|
+
parse with a misleading "Expected a semicolon"). */
|
|
4238
|
+
.dsm-workbuddy-composer-panel-dot-ok{background:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
4239
|
+
.dsm-workbuddy-composer-panel-dot-error{background:var(--dsw-alias-state-error-primary,#ef4444)}
|
|
4240
|
+
.dsm-workbuddy-composer-panel-dot-unknown{background:var(--dsw-alias-label-dimmed,rgba(127,127,127,.5))}
|
|
3548
4241
|
.dsm-workbuddy-composer-panel-foot{
|
|
3549
4242
|
display:flex;justify-content:space-between;gap:8px;
|
|
3550
4243
|
margin-top:9px;padding-top:7px;
|
|
@@ -4614,39 +5307,6 @@ window.__ModuleLoader__.load({
|
|
|
4614
5307
|
}))
|
|
4615
5308
|
})] });
|
|
4616
5309
|
/**
|
|
4617
|
-
* Sidebar credit line switch — ONE control, for the ACTIVE tab's region only.
|
|
4618
|
-
*
|
|
4619
|
-
* The two regions are independent providers with independent accounts, so
|
|
4620
|
-
* each tab owns its own switch and they are never shown as a pair: this one
|
|
4621
|
-
* writes `regions.<activeRegion>.showCreditsInMainUi`, which decides only
|
|
4622
|
-
* whether THAT region's line appears in the sidebar. That matches how every
|
|
4623
|
-
* other control in this card is scoped to the selected tab (account, pool,
|
|
4624
|
-
* model list), and it means a CN-only user is never shown a switch for an
|
|
4625
|
-
* account they do not have. An earlier revision rendered both boxes side by
|
|
4626
|
-
* side under the tab bar regardless of the active tab; that read as one
|
|
4627
|
-
* shared setting and was replaced.
|
|
4628
|
-
*
|
|
4629
|
-
* RENDERED BESIDE THE POOL HEADING, not on its own row. It used to occupy a
|
|
4630
|
-
* full line of vertical space for a single checkbox; the pool heading already
|
|
4631
|
-
* had a title-left / badge-right row, so the control fits there without the
|
|
4632
|
-
* block growing. It is handed to `AccountPool` as a ready-made node purely so
|
|
4633
|
-
* the two can share a flex row — the write path, the disabled rules and the
|
|
4634
|
-
* serialization against the card's other region writers all stay here, where
|
|
4635
|
-
* the settings scope and the save state live.
|
|
4636
|
-
*/
|
|
4637
|
-
const creditsSwitch = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4638
|
-
className: "dsm-workbuddy-credits-switch",
|
|
4639
|
-
title: activeRegion === "cn" ? t("row.showCreditsCnHint") : t("row.showCreditsGlobalHint"),
|
|
4640
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4641
|
-
type: "checkbox",
|
|
4642
|
-
checked: activeRegionOn ? creditsShownOn(activeRegion) : false,
|
|
4643
|
-
disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
|
|
4644
|
-
onChange: (event) => {
|
|
4645
|
-
toggleCredits(activeRegion, event.target.checked);
|
|
4646
|
-
}
|
|
4647
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activeRegion === "cn" ? t("row.showCreditsCn") : t("row.showCreditsGlobal") })]
|
|
4648
|
-
});
|
|
4649
|
-
/**
|
|
4650
5310
|
* WHERE the readout sits — a two-way choice, active tab's region only
|
|
4651
5311
|
* (issue #62).
|
|
4652
5312
|
*
|
|
@@ -4654,31 +5314,76 @@ window.__ModuleLoader__.load({
|
|
|
4654
5314
|
* question together ("show it, and where"), and because a user only reaches
|
|
4655
5315
|
* for this when the default seat is unavailable — typically after installing
|
|
4656
5316
|
* another plugin that occupies the composer row. Rendered as a `<select>`
|
|
4657
|
-
* rather than a pair of radios so it stays one compact row
|
|
4658
|
-
*
|
|
5317
|
+
* rather than a pair of radios so it stays one compact row.
|
|
5318
|
+
*
|
|
5319
|
+
* LABEL LEFT, CONTROL RIGHT — the reverse of the first draft, which put the
|
|
5320
|
+
* select first. Under an always-visible caption the label is what the eye
|
|
5321
|
+
* scans for ("显示位置"), so it leads and the value follows; leading with the
|
|
5322
|
+
* control made it read as a second checkbox whose box happened to be a
|
|
5323
|
+
* dropdown. It also matches the reference settings screen, where the option
|
|
5324
|
+
* name sits left of its picker.
|
|
4659
5325
|
*
|
|
4660
5326
|
* Disabled with the region, exactly like the switch: a region that is off has
|
|
4661
5327
|
* no readout to place.
|
|
4662
5328
|
*/
|
|
4663
|
-
const creditPositionPicker = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
4664
|
-
className: "dsm-workbuddy-credit-position
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
5329
|
+
const creditPositionPicker = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
5330
|
+
className: "dsm-workbuddy-credit-position",
|
|
5331
|
+
value: activeRegionOn ? creditPositionOn(activeRegion) : "composer",
|
|
5332
|
+
disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
|
|
5333
|
+
"aria-label": t("row.creditPositionAria", { region: activeRegion === "cn" ? t("row.tabCn") : t("row.tabGlobal") }),
|
|
5334
|
+
onChange: (event) => {
|
|
5335
|
+
changeCreditPosition(activeRegion, event.currentTarget.value === "stats" ? "stats" : "composer");
|
|
5336
|
+
},
|
|
5337
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
5338
|
+
value: "composer",
|
|
5339
|
+
children: t("row.creditPositionComposer")
|
|
5340
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
5341
|
+
value: "stats",
|
|
5342
|
+
children: t("row.creditPositionStats")
|
|
5343
|
+
})]
|
|
5344
|
+
});
|
|
5345
|
+
const creditPositionCaption = t("row.creditPositionHintInline");
|
|
5346
|
+
const creditPositionDisabledCaption = t("row.creditPositionDisabledHint");
|
|
5347
|
+
const creditPreferences = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5348
|
+
className: "dsm-workbuddy-prefs",
|
|
5349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
5350
|
+
className: "dsm-workbuddy-pref",
|
|
5351
|
+
title: activeRegion === "cn" ? t("row.showCreditsCnHint") : t("row.showCreditsGlobalHint"),
|
|
5352
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5353
|
+
type: "checkbox",
|
|
5354
|
+
className: "dsm-workbuddy-pref-check",
|
|
5355
|
+
checked: activeRegionOn ? creditsShownOn(activeRegion) : false,
|
|
5356
|
+
disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
|
|
5357
|
+
onChange: (event) => {
|
|
5358
|
+
toggleCredits(activeRegion, event.target.checked);
|
|
5359
|
+
}
|
|
5360
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5361
|
+
className: "dsm-workbuddy-pref-copy",
|
|
5362
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5363
|
+
className: "dsm-workbuddy-pref-label",
|
|
5364
|
+
children: activeRegion === "cn" ? t("row.showCreditsCn") : t("row.showCreditsGlobal")
|
|
5365
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5366
|
+
className: "dsm-workbuddy-pref-hint",
|
|
5367
|
+
children: t("row.showCreditsHintInline")
|
|
5368
|
+
})]
|
|
4680
5369
|
})]
|
|
4681
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
5370
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
5371
|
+
className: "dsm-workbuddy-pref dsm-workbuddy-pref-indent",
|
|
5372
|
+
title: t("row.creditPositionHint"),
|
|
5373
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5374
|
+
className: "dsm-workbuddy-pref-copy",
|
|
5375
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5376
|
+
className: "dsm-workbuddy-pref-lead",
|
|
5377
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5378
|
+
className: "dsm-workbuddy-pref-label",
|
|
5379
|
+
children: t("row.creditPositionLabel")
|
|
5380
|
+
}), creditPositionPicker]
|
|
5381
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5382
|
+
className: "dsm-workbuddy-pref-hint",
|
|
5383
|
+
children: activeRegionOn ? creditPositionCaption : creditPositionDisabledCaption
|
|
5384
|
+
})]
|
|
5385
|
+
})
|
|
5386
|
+
})]
|
|
4682
5387
|
});
|
|
4683
5388
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4684
5389
|
className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
|
|
@@ -4768,6 +5473,7 @@ window.__ModuleLoader__.load({
|
|
|
4768
5473
|
className: "dsm-workbuddy-tab-off-notice",
|
|
4769
5474
|
children: t("row.tabOffNotice")
|
|
4770
5475
|
}) : null,
|
|
5476
|
+
creditPreferences,
|
|
4771
5477
|
status.status === "error" || status.status === "signed-in" && status.creditsError !== void 0 ? status.status === "signed-in" && status.credentialRejected === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4772
5478
|
className: "dsm-workbuddy-usage-hint",
|
|
4773
5479
|
children: t("row.reloginHint")
|
|
@@ -4824,16 +5530,11 @@ window.__ModuleLoader__.load({
|
|
|
4824
5530
|
},
|
|
4825
5531
|
selectingAccount: switchingAccount,
|
|
4826
5532
|
accountTools,
|
|
4827
|
-
...status.pool === void 0 ? {} : { headerExtra: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [creditsSwitch, creditPositionPicker] }) },
|
|
4828
5533
|
siblingBusy: saving || togglingRegion !== void 0,
|
|
4829
5534
|
onBusyChange: setPoolBusy,
|
|
4830
5535
|
onSaved: async () => await refreshUsage(activeRegion) !== void 0,
|
|
4831
5536
|
onRefresh: () => refreshUsage(activeRegion)
|
|
4832
5537
|
}),
|
|
4833
|
-
status.pool === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4834
|
-
className: "dsm-workbuddy-credits-switch-row",
|
|
4835
|
-
children: [creditsSwitch, creditPositionPicker]
|
|
4836
|
-
}) : null,
|
|
4837
5538
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
4838
5539
|
className: "dsm-workbuddy-models",
|
|
4839
5540
|
"aria-label": t("row.modelsTitle"),
|
|
@@ -5346,6 +6047,57 @@ window.__ModuleLoader__.load({
|
|
|
5346
6047
|
minute: "2-digit"
|
|
5347
6048
|
});
|
|
5348
6049
|
}
|
|
6050
|
+
/**
|
|
6051
|
+
* The dot's classes, one per tone.
|
|
6052
|
+
*
|
|
6053
|
+
* Written out in full instead of `\`…-dot-${tone}\`` on purpose: the panel's
|
|
6054
|
+
* CSS-contract test harvests class names from this file with
|
|
6055
|
+
* /dsm-workbuddy-composer-[a-z-]+/ and a template literal would contribute a
|
|
6056
|
+
* dangling "…-dot-" prefix that no rule defines.
|
|
6057
|
+
*/
|
|
6058
|
+
const DOT_CLASS = {
|
|
6059
|
+
ok: "dsm-workbuddy-composer-panel-dot dsm-workbuddy-composer-panel-dot-ok",
|
|
6060
|
+
error: "dsm-workbuddy-composer-panel-dot dsm-workbuddy-composer-panel-dot-error",
|
|
6061
|
+
unknown: "dsm-workbuddy-composer-panel-dot dsm-workbuddy-composer-panel-dot-unknown"
|
|
6062
|
+
};
|
|
6063
|
+
/**
|
|
6064
|
+
* The dot class for one row: green when the account can serve, red when it cannot.
|
|
6065
|
+
*
|
|
6066
|
+
* The rule the owner settled on, after three rounds of this: colour answers
|
|
6067
|
+
* "can it be used", and the row's second line answers "is it the one I am
|
|
6068
|
+
* using". Those two questions used to share one channel — an earlier revision
|
|
6069
|
+
* painted green only on the selected account, which made every usable row grey
|
|
6070
|
+
* and the list read as one live row among dead ones ("这样无法知道当前用的是什么"):
|
|
6071
|
+
* true about selection, false about usability, and the panel is opened mainly to
|
|
6072
|
+
* ask the second question.
|
|
6073
|
+
*
|
|
6074
|
+
* So green is now the ordinary state of a usable account, selected or not, and
|
|
6075
|
+
* "当前选用" is printed under the name of the selected one. Red carries the
|
|
6076
|
+
* refusal — including a stated rate limit, which used to be amber; amber read as
|
|
6077
|
+
* "mostly fine", and an account that cannot answer this message is not fine.
|
|
6078
|
+
*
|
|
6079
|
+
* Grey is reserved for an account nobody has measured yet. Painting it green
|
|
6080
|
+
* would assert a health nobody checked, and red would assert a failure nobody
|
|
6081
|
+
* saw; the absence of a reading is its own state.
|
|
6082
|
+
*/
|
|
6083
|
+
function dotClass(account) {
|
|
6084
|
+
const tone = stateTone(account);
|
|
6085
|
+
if (tone === "ok") return DOT_CLASS.ok;
|
|
6086
|
+
if (tone === "warn" || tone === "error") return DOT_CLASS.error;
|
|
6087
|
+
return DOT_CLASS.unknown;
|
|
6088
|
+
}
|
|
6089
|
+
/**
|
|
6090
|
+
* The row's second line, from the facts that belong in it.
|
|
6091
|
+
*
|
|
6092
|
+
* Two independent ones: whether this is the account serving the tab, and — when
|
|
6093
|
+
* it cannot serve — why, with the wait but never the absolute instant. Joined
|
|
6094
|
+
* with the same separator the rest of this panel uses, and dropped entirely when
|
|
6095
|
+
* there is nothing to say, so a plain usable row stays one line tall.
|
|
6096
|
+
*/
|
|
6097
|
+
function detailLine(...parts) {
|
|
6098
|
+
const kept = parts.filter((part) => part !== void 0 && part !== "");
|
|
6099
|
+
return kept.length === 0 ? void 0 : kept.join(" · ");
|
|
6100
|
+
}
|
|
5349
6101
|
function ComposerCredits(props) {
|
|
5350
6102
|
const { t, provider, region, settingsScope } = props;
|
|
5351
6103
|
if (t === void 0) throw new Error("Composer credits readout requires its translation function");
|
|
@@ -5354,11 +6106,24 @@ window.__ModuleLoader__.load({
|
|
|
5354
6106
|
const [usage, setUsage] = (0, react.useState)(void 0);
|
|
5355
6107
|
const [accountCredits, setAccountCredits] = (0, react.useState)(void 0);
|
|
5356
6108
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
6109
|
+
/**
|
|
6110
|
+
* A test batch is running.
|
|
6111
|
+
*
|
|
6112
|
+
* Separate from `busy`, which tracks a credits READ: the two take wildly
|
|
6113
|
+
* different times (one round trip against one probe per account, serial with
|
|
6114
|
+
* a gap), and sharing a flag would either freeze the readout for a minute or
|
|
6115
|
+
* let a press be swallowed as "already refreshing".
|
|
6116
|
+
*/
|
|
6117
|
+
const [testing, setTesting] = (0, react.useState)(false);
|
|
6118
|
+
/** The batch's failure, in the user's language; `failed` is for reads. */
|
|
6119
|
+
const [testError, setTestError] = (0, react.useState)(void 0);
|
|
5357
6120
|
const [switchingId, setSwitchingId] = (0, react.useState)(void 0);
|
|
5358
6121
|
const [failed, setFailed] = (0, react.useState)(false);
|
|
5359
6122
|
const [lastRefresh, setLastRefresh] = (0, react.useState)(void 0);
|
|
5360
6123
|
const [open, setOpen] = (0, react.useState)(false);
|
|
5361
6124
|
const inFlight = (0, react.useRef)(false);
|
|
6125
|
+
/** A batch is running; guards a second press that lands before `setTesting`. */
|
|
6126
|
+
const testingRef = (0, react.useRef)(false);
|
|
5362
6127
|
/** Account-table reads in flight; see `fetchAccountCredits`. */
|
|
5363
6128
|
const accountsInFlight = (0, react.useRef)(0);
|
|
5364
6129
|
/** Which account-table read is the newest; see `fetchAccountCredits`. */
|
|
@@ -5546,26 +6311,85 @@ window.__ModuleLoader__.load({
|
|
|
5546
6311
|
}
|
|
5547
6312
|
};
|
|
5548
6313
|
/**
|
|
5549
|
-
*
|
|
6314
|
+
* The header button: test EVERY account in this region, then re-read what the
|
|
6315
|
+
* batch changed.
|
|
6316
|
+
*
|
|
6317
|
+
* It used to be a plain refresh. The owner asked for the other thing this
|
|
6318
|
+
* panel's readers actually want — the same "一键测试所有账号" the pool card
|
|
6319
|
+
* runs (`src/account-pool-run.ts testAllAccounts`, one account at a time with
|
|
6320
|
+
* a gap, because hitting the whole pool at once is how the upstream's volume
|
|
6321
|
+
* limit gets tripped).
|
|
6322
|
+
*
|
|
6323
|
+
* Two reasons the batch, not a read, is the right control HERE. A mark in this
|
|
6324
|
+
* table is a MEASUREMENT (`被限流 · 约 6 小时后` is a probe result), and
|
|
6325
|
+
* re-reading the usage document cannot improve a stale measurement — only
|
|
6326
|
+
* running it again can. And the rows are the pool's members, so the question
|
|
6327
|
+
* "which of these can serve?" is answered by testing them, not by asking the
|
|
6328
|
+
* credits endpoint again.
|
|
5550
6329
|
*
|
|
5551
|
-
*
|
|
5552
|
-
*
|
|
5553
|
-
*
|
|
5554
|
-
*
|
|
5555
|
-
* refreshes something else is worse than no control, and it is one of the
|
|
5556
|
-
* reasons the panel could keep claiming a state the card had already moved
|
|
5557
|
-
* past.
|
|
6330
|
+
* The re-read afterwards is not a courtesy: the probes live in the USAGE
|
|
6331
|
+
* document (`usage.pool.accounts[].probe`), so the dots and marks only move
|
|
6332
|
+
* once that document is fetched again. Without it the button would leave the
|
|
6333
|
+
* table showing the state it just measured away.
|
|
5558
6334
|
*
|
|
5559
|
-
*
|
|
5560
|
-
*
|
|
5561
|
-
*
|
|
5562
|
-
*
|
|
5563
|
-
* because the other half went wrong.
|
|
6335
|
+
* The batch answers NDJSON (one line per account, streamed) on a current Host
|
|
6336
|
+
* and one document on an older one, so both shapes are read; the stream is
|
|
6337
|
+
* drained even after unmount, because dropping the reader leaves the Host
|
|
6338
|
+
* writing into a socket nobody reads.
|
|
5564
6339
|
*/
|
|
5565
|
-
const
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
6340
|
+
const testEveryAccount = (0, react.useCallback)(async () => {
|
|
6341
|
+
if (activeRegion === void 0 || testingRef.current) return;
|
|
6342
|
+
testingRef.current = true;
|
|
6343
|
+
if (mounted.current) {
|
|
6344
|
+
setTesting(true);
|
|
6345
|
+
setTestError(void 0);
|
|
6346
|
+
setFailed(false);
|
|
6347
|
+
}
|
|
6348
|
+
try {
|
|
6349
|
+
const response = await fetch(withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, activeRegion, "test"), {
|
|
6350
|
+
method: "POST",
|
|
6351
|
+
headers: { accept: "application/json" },
|
|
6352
|
+
credentials: "same-origin"
|
|
6353
|
+
});
|
|
6354
|
+
let failure;
|
|
6355
|
+
if ((response.headers.get("content-type") ?? "").includes("ndjson")) {
|
|
6356
|
+
if (!response.ok) failure = poolFailureText(t, { status: response.status });
|
|
6357
|
+
else await readNdjson(response.body, (line) => {
|
|
6358
|
+
const row = testRowOf(line);
|
|
6359
|
+
if (row !== void 0) {
|
|
6360
|
+
if (mounted.current) setUsage((previous) => previous === void 0 ? previous : withTestRow(previous, row, Date.now()));
|
|
6361
|
+
return;
|
|
6362
|
+
}
|
|
6363
|
+
const reason = line["reason"];
|
|
6364
|
+
if (typeof reason !== "string") return;
|
|
6365
|
+
failure = poolFailureText(t, {
|
|
6366
|
+
status: 200,
|
|
6367
|
+
reason,
|
|
6368
|
+
error: typeof line["error"] === "string" ? line["error"] : void 0
|
|
6369
|
+
});
|
|
6370
|
+
});
|
|
6371
|
+
} else {
|
|
6372
|
+
const body = await response.json().catch(() => void 0);
|
|
6373
|
+
if (!response.ok) failure = poolFailureText(t, {
|
|
6374
|
+
status: response.status,
|
|
6375
|
+
reason: body?.reason,
|
|
6376
|
+
error: body?.error
|
|
6377
|
+
});
|
|
6378
|
+
}
|
|
6379
|
+
if (mounted.current && failure !== void 0) setTestError(failure);
|
|
6380
|
+
} catch {
|
|
6381
|
+
if (mounted.current) setTestError(t("composer.testFailed"));
|
|
6382
|
+
} finally {
|
|
6383
|
+
testingRef.current = false;
|
|
6384
|
+
if (mounted.current) setTesting(false);
|
|
6385
|
+
await Promise.all([fetchUsage(), fetchAccountCredits(void 0, true)]);
|
|
6386
|
+
}
|
|
6387
|
+
}, [
|
|
6388
|
+
activeRegion,
|
|
6389
|
+
fetchUsage,
|
|
6390
|
+
fetchAccountCredits,
|
|
6391
|
+
t
|
|
6392
|
+
]);
|
|
5569
6393
|
if (owned === void 0) return null;
|
|
5570
6394
|
/**
|
|
5571
6395
|
* The Host's own answer for this region, when the document carries one.
|
|
@@ -5609,6 +6433,9 @@ window.__ModuleLoader__.load({
|
|
|
5609
6433
|
};
|
|
5610
6434
|
});
|
|
5611
6435
|
const canSwitch = settingsScope !== void 0 && settingsScope.getSnapshot().writable === true;
|
|
6436
|
+
const poolOrder = usage?.status === "signed-in" ? usage.pool?.orderAccountIds : void 0;
|
|
6437
|
+
const orderedRows = applyManualOrder(rows, poolOrder !== void 0 && poolOrder.length > 0 ? poolOrder : settingsScope === void 0 ? [] : readOrderAccountIds(settingsScope.getSnapshot().value, activeRegion));
|
|
6438
|
+
const poolById = new Map((usage?.status === "signed-in" ? usage.pool?.accounts ?? [] : []).map((account) => [account.accountId, account]));
|
|
5612
6439
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5613
6440
|
ref: rootRef,
|
|
5614
6441
|
className: "dsm-workbuddy-composer-credits",
|
|
@@ -5644,11 +6471,12 @@ window.__ModuleLoader__.load({
|
|
|
5644
6471
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5645
6472
|
type: "button",
|
|
5646
6473
|
className: "dsm-workbuddy-composer-panel-refresh",
|
|
5647
|
-
disabled: busy || switchingId !== void 0,
|
|
6474
|
+
disabled: busy || testing || switchingId !== void 0,
|
|
6475
|
+
title: t("composer.testAllHint"),
|
|
5648
6476
|
onClick: () => {
|
|
5649
|
-
|
|
6477
|
+
testEveryAccount();
|
|
5650
6478
|
},
|
|
5651
|
-
children:
|
|
6479
|
+
children: testing ? t("composer.testing") : t("composer.testAll")
|
|
5652
6480
|
})]
|
|
5653
6481
|
}),
|
|
5654
6482
|
signedOut ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -5666,13 +6494,25 @@ window.__ModuleLoader__.load({
|
|
|
5666
6494
|
scope: "col",
|
|
5667
6495
|
className: "dsm-workbuddy-composer-panel-num",
|
|
5668
6496
|
children: t("composer.credits")
|
|
5669
|
-
})] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children:
|
|
6497
|
+
})] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: orderedRows.map((row) => {
|
|
5670
6498
|
const current = row.id === selectedId;
|
|
5671
|
-
const
|
|
6499
|
+
const poolRow = poolById.get(row.id);
|
|
6500
|
+
const live = poolRow === void 0 ? current : poolRow.current;
|
|
6501
|
+
const availability = poolRow ?? {
|
|
6502
|
+
excludedBy: row.excludedBy,
|
|
6503
|
+
probe: void 0
|
|
6504
|
+
};
|
|
6505
|
+
const usable = stateTone(availability) === "ok";
|
|
6506
|
+
const currentMark = live ? t("composer.current") : void 0;
|
|
6507
|
+
const reasonMark = usable ? void 0 : availabilityMark(t, availability);
|
|
6508
|
+
const spoken = detailLine(live ? t("composer.current") : void 0, usable ? void 0 : availabilityLine(t, availability));
|
|
6509
|
+
const whoLabel = spoken === void 0 ? row.accountName : `${row.accountName} — ${spoken}`;
|
|
5672
6510
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
5673
6511
|
className: current ? "dsm-workbuddy-composer-panel-row-current" : void 0,
|
|
6512
|
+
title: spoken,
|
|
5674
6513
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
5675
6514
|
scope: "row",
|
|
6515
|
+
"aria-label": whoLabel,
|
|
5676
6516
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5677
6517
|
type: "button",
|
|
5678
6518
|
className: "dsm-workbuddy-composer-panel-switch",
|
|
@@ -5682,16 +6522,22 @@ window.__ModuleLoader__.load({
|
|
|
5682
6522
|
switchTo(row.id);
|
|
5683
6523
|
},
|
|
5684
6524
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5685
|
-
className:
|
|
6525
|
+
className: dotClass(availability),
|
|
5686
6526
|
"aria-hidden": "true"
|
|
5687
6527
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5688
6528
|
className: "dsm-workbuddy-composer-panel-who",
|
|
5689
6529
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5690
6530
|
className: "dsm-workbuddy-composer-panel-name",
|
|
5691
6531
|
children: row.accountName
|
|
5692
|
-
}),
|
|
6532
|
+
}), currentMark === void 0 && reasonMark === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5693
6533
|
className: "dsm-workbuddy-composer-panel-mark",
|
|
5694
|
-
children:
|
|
6534
|
+
children: [currentMark === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6535
|
+
className: "dsm-workbuddy-composer-panel-mark-current",
|
|
6536
|
+
children: currentMark
|
|
6537
|
+
}), reasonMark === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6538
|
+
className: "dsm-workbuddy-composer-panel-mark-reason",
|
|
6539
|
+
children: reasonMark
|
|
6540
|
+
})]
|
|
5695
6541
|
})]
|
|
5696
6542
|
})]
|
|
5697
6543
|
})
|
|
@@ -5714,7 +6560,12 @@ window.__ModuleLoader__.load({
|
|
|
5714
6560
|
className: "dsm-workbuddy-composer-panel-error",
|
|
5715
6561
|
role: "status",
|
|
5716
6562
|
children: t("composer.refreshFailed")
|
|
5717
|
-
}) : null
|
|
6563
|
+
}) : null,
|
|
6564
|
+
testError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6565
|
+
className: "dsm-workbuddy-composer-panel-error",
|
|
6566
|
+
role: "status",
|
|
6567
|
+
children: testError
|
|
6568
|
+
})
|
|
5718
6569
|
]
|
|
5719
6570
|
}), document.body)]
|
|
5720
6571
|
});
|
|
@@ -6169,6 +7020,19 @@ window.__ModuleLoader__.load({
|
|
|
6169
7020
|
"row.poolSelectAll": "Select all",
|
|
6170
7021
|
"row.poolSelectNone": "Clear all",
|
|
6171
7022
|
"row.poolSelectedCount": "{count} of {total} selected",
|
|
7023
|
+
"row.poolOrderRulesTitle": "How this pool chooses an account",
|
|
7024
|
+
"row.poolOrderRulesIntro": "The pool always picks the first account that can actually be billed right now. The rules below are checked in order — the first one that separates two accounts decides; later rules only break ties.",
|
|
7025
|
+
"row.poolOrderRuleUsable": "1. Can it be billed right now? Accounts that are rate-limited, out of credit, credential-rejected, or failing are moved to the back until their cooldown ends. This outranks everything, including your own order below.",
|
|
7026
|
+
"row.poolOrderRuleExpiring": "2. Does it have credits expiring within 3 days? Yes goes first. This also outranks your manual order — see the note below.",
|
|
7027
|
+
"row.poolOrderRuleManual": "3. Your manual order. The position you set with the ↑ / ↓ arrows. Accounts you never ordered are placed after every ordered one.",
|
|
7028
|
+
"row.poolOrderRuleNearestExpiry": "4. Which expiry comes first? Between accounts that both have credits expiring, the one losing them sooner goes first.",
|
|
7029
|
+
"row.poolOrderRuleExpiringAmount": "5. How many are expiring? Between those same accounts, the one with more at stake goes first. With nothing expiring this is equivalent to comparing totals.",
|
|
7030
|
+
"row.poolOrderRuleTotal": "6. Total credits. The account holding more goes first, so a nearly-drained account is kept in reserve.",
|
|
7031
|
+
"row.poolOrderRuleFreshness": "7. Freshness of the sign-in. The more recently issued credential wins a draw.",
|
|
7032
|
+
"row.poolOrderRuleId": "8. Account id. A final tie-break, so the same pool always produces the same order.",
|
|
7033
|
+
"row.poolOrderWhy": "Why expiring credits outrank your manual order: expiring credits are money about to disappear, while a manual order may be stale or set by accident. If the manual order won, one mis-drag could let credits lapse unused. So an account with credits expiring within 3 days always goes ahead of one without, even if you dragged it to the bottom.",
|
|
7034
|
+
"row.poolOrderBadgeNote": "The badge shows the position you SAVED, not the live ranking. An account can read \"No. 2\" while being benched for a rate limit — the number is your saved preference, and the pool skips accounts it cannot bill right now.",
|
|
7035
|
+
"row.poolOrderRulesToggle": "Show the full ordering rule",
|
|
6172
7036
|
"row.poolNoneSelected": "No account is checked into this pool.",
|
|
6173
7037
|
"row.poolNoneSelectedHint": "Check at least one account above and save. Until then both buttons stay disabled, because a pool with no members has nothing to run on.",
|
|
6174
7038
|
"row.poolGhostMembers": "{count} pool member(s) are no longer signed in on this machine, so they are not counted and not used: {names}. Remove any you no longer need below.",
|
|
@@ -6181,6 +7045,14 @@ window.__ModuleLoader__.load({
|
|
|
6181
7045
|
"row.poolCurrentHeader": "In use: {account}",
|
|
6182
7046
|
"row.poolNotMember": "Not in pool",
|
|
6183
7047
|
"row.poolUnsavedMembers": "Selection changed — save to apply",
|
|
7048
|
+
"row.poolRankLabel": "No. {rank}",
|
|
7049
|
+
"row.poolRankManualTitle": "You placed this account at position {rank} in the pool manually.",
|
|
7050
|
+
"row.poolRankAuto": "Auto",
|
|
7051
|
+
"row.poolRankAutoTitle": "Not manually sorted yet: the pool spends expiring credits first, then picks accounts by remaining balance. Use ↑/↓ to set your own order.",
|
|
7052
|
+
"row.poolMoveUpTitle": "Move account up in this pool's order",
|
|
7053
|
+
"row.poolMoveDownTitle": "Move account down in this pool's order",
|
|
7054
|
+
"row.poolMoveUpAria": "Move {accountName} up in this pool's order",
|
|
7055
|
+
"row.poolMoveDownAria": "Move {accountName} down in this pool's order",
|
|
6184
7056
|
"row.poolRowTest": "Test",
|
|
6185
7057
|
"row.poolRowRemove": "Remove",
|
|
6186
7058
|
"row.poolRowRemoveHint": "Remove this account from the list. The WorkBuddy desktop app is not signed out — it keeps its own sign-in. Removing only hides the account here; 「Detect accounts again」 brings it back.",
|
|
@@ -6188,13 +7060,16 @@ window.__ModuleLoader__.load({
|
|
|
6188
7060
|
"row.poolRowRemoved": "{accountName}: hidden from the list. 「Detect accounts again」 re-adds it; the desktop app stays signed in.",
|
|
6189
7061
|
"row.poolRowRemoving": "Removing the account…",
|
|
6190
7062
|
"row.poolRowRemoveDesktopOnly": "{accountName}: hidden from the list — this account comes from the desktop app's own sign-in, which is left untouched. 「Detect accounts again」 re-adds it.",
|
|
6191
|
-
"row.showCreditsCn": "
|
|
6192
|
-
"row.showCreditsGlobal": "
|
|
7063
|
+
"row.showCreditsCn": "Show credits in the composer toolbar",
|
|
7064
|
+
"row.showCreditsGlobal": "Show credits in the composer toolbar",
|
|
6193
7065
|
"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 manual refresh (refreshes by itself every 5 minutes).",
|
|
6194
|
-
"row.creditPositionLabel": "
|
|
7066
|
+
"row.creditPositionLabel": "Position",
|
|
6195
7067
|
"row.creditPositionComposer": "composer toolbar",
|
|
6196
7068
|
"row.creditPositionStats": "below the input box",
|
|
6197
7069
|
"row.creditPositionAria": "Credit readout position for {region}",
|
|
7070
|
+
"row.creditPositionHintInline": "Where the readout sits. \"composer toolbar\" is the default — the row with the model picker.",
|
|
7071
|
+
"row.creditPositionDisabledHint": "Unavailable while the switch above is off: tick \"Show credits in the composer toolbar\" first.",
|
|
7072
|
+
"row.showCreditsHintInline": "The balance for the account currently serving this tab. Click for per-account balances; it also refreshes every 5 minutes.",
|
|
6198
7073
|
"quotaNotice.fallbackHead": "The quota for this request is exhausted",
|
|
6199
7074
|
"quotaNotice.supplement": " (some models also have a daily limit that is unrelated to your credits)",
|
|
6200
7075
|
"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.",
|
|
@@ -6206,10 +7081,13 @@ window.__ModuleLoader__.load({
|
|
|
6206
7081
|
"composer.account": "Account",
|
|
6207
7082
|
"composer.credits": "Credits",
|
|
6208
7083
|
"composer.switchTo": "Switch to {account}",
|
|
7084
|
+
"composer.current": "In use",
|
|
6209
7085
|
"composer.noAccounts": "No account in this region",
|
|
6210
7086
|
"composer.signedOut": "Not signed in",
|
|
6211
|
-
"composer.
|
|
6212
|
-
"composer.
|
|
7087
|
+
"composer.testAll": "Test all",
|
|
7088
|
+
"composer.testing": "Testing…",
|
|
7089
|
+
"composer.testAllHint": "Test every account in this region, one at a time",
|
|
7090
|
+
"composer.testFailed": "Could not test the accounts",
|
|
6213
7091
|
"composer.refreshFailed": "Could not refresh credits",
|
|
6214
7092
|
"composer.lastRefresh": "Last refreshed",
|
|
6215
7093
|
"composer.panelTitle": "WorkBuddy credits",
|
|
@@ -6454,6 +7332,19 @@ window.__ModuleLoader__.load({
|
|
|
6454
7332
|
"row.poolSelectAll": "全选",
|
|
6455
7333
|
"row.poolSelectNone": "全不选",
|
|
6456
7334
|
"row.poolSelectedCount": "已选 {count} / {total}",
|
|
7335
|
+
"row.poolOrderRulesTitle": "本池如何挑选账号",
|
|
7336
|
+
"row.poolOrderRulesIntro": "池子总是先挑「现在确实能扣费的」账号。下面这些规则按顺序逐条比较,第一条分出胜负就停止,后面的只在平局时才看。",
|
|
7337
|
+
"row.poolOrderRuleUsable": "1. 现在能扣费吗?被限流、积分耗尽、凭证被拒或调用失败的账号会被挪到后面,直到冷却结束。这一条优先级最高,压过下面的手动顺序。",
|
|
7338
|
+
"row.poolOrderRuleExpiring": "2. 有 3 天内会过期的积分吗?有就排前面。这一条同样压过手动顺序,原因见下方说明。",
|
|
7339
|
+
"row.poolOrderRuleManual": "3. 你排的手动顺序。就是用 ↑ / ↓ 设定的位置。没有手动排过的账号,会排在所有已排序账号之后。",
|
|
7340
|
+
"row.poolOrderRuleNearestExpiry": "4. 谁先过期?两个都有临期积分时,先到期的排前面。",
|
|
7341
|
+
"row.poolOrderRuleExpiringAmount": "5. 过期多少?同样是两个都有临期积分时,临期数额大的排前面。没有临期积分时,这一步等同于比较总积分。",
|
|
7342
|
+
"row.poolOrderRuleTotal": "6. 总积分。积分多的排前面,接近耗尽的号先留着备用。",
|
|
7343
|
+
"row.poolOrderRuleFreshness": "7. 登录凭证新旧。凭证签发更晚的赢下平局。",
|
|
7344
|
+
"row.poolOrderRuleId": "8. 账号 id。最终兜底,保证同样的池子每次排出来的顺序都一样。",
|
|
7345
|
+
"row.poolOrderWhy": "为什么临期积分压过手动顺序:临期积分是马上要过期的钱,而手动顺序可能已经过时、也可能是手滑拖错了。如果让手动顺序赢,一次误拖就足以让积分白白过期。所以有 3 天内到期积分的账号,永远排在没有的账号前面 —— 哪怕你把它拖到了最后一位。",
|
|
7346
|
+
"row.poolOrderBadgeNote": "徽标显示的是你「保存」的位置,不是此刻的实际排序。一个账号可能写着「第 2 位」却因为被限流而暂时不被选中 —— 那个数字代表你的偏好,池子会跳过当下无法扣费的账号。",
|
|
7347
|
+
"row.poolOrderRulesToggle": "查看完整排序规则",
|
|
6457
7348
|
"row.poolNoneSelected": "这个池里还没有勾选任何账号。",
|
|
6458
7349
|
"row.poolNoneSelectedHint": "请在上方勾选至少一个账号并保存。在此之前两个按钮都不可用 —— 空池没有可执行的对象。",
|
|
6459
7350
|
"row.poolGhostMembers": "有 {count} 个已选入本池的账号在本机已不再登录,因此不计入、也不会被使用:{names}。不需要的可在下方逐个移除。",
|
|
@@ -6466,6 +7357,14 @@ window.__ModuleLoader__.load({
|
|
|
6466
7357
|
"row.poolCurrentHeader": "当前使用:{account}",
|
|
6467
7358
|
"row.poolNotMember": "未入池",
|
|
6468
7359
|
"row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
|
|
7360
|
+
"row.poolRankLabel": "第 {rank} 位",
|
|
7361
|
+
"row.poolRankManualTitle": "你在本池中手动把该账号排在第 {rank} 位。",
|
|
7362
|
+
"row.poolRankAuto": "自动排序",
|
|
7363
|
+
"row.poolRankAutoTitle": "还没有手动排序:池会先花临期积分,再按余额多寡选号。用 ↑/↓ 自己指定顺序。",
|
|
7364
|
+
"row.poolMoveUpTitle": "在本池顺序中上移该账号",
|
|
7365
|
+
"row.poolMoveDownTitle": "在本池顺序中下移该账号",
|
|
7366
|
+
"row.poolMoveUpAria": "在本池顺序中上移 {accountName}",
|
|
7367
|
+
"row.poolMoveDownAria": "在本池顺序中下移 {accountName}",
|
|
6469
7368
|
"row.poolRowTest": "测试",
|
|
6470
7369
|
"row.poolRowRemove": "删除",
|
|
6471
7370
|
"row.poolRowRemoveHint": "把这个账号从列表中移除。桌面 App 不会被退出登录——它保留自己的登录状态。移除只是暂时隐藏,点「重新检测账号」即可找回。",
|
|
@@ -6473,13 +7372,16 @@ window.__ModuleLoader__.load({
|
|
|
6473
7372
|
"row.poolRowRemoved": "{accountName}:已从列表中隐藏。点「重新检测账号」会重新加入,桌面 App 的登录不受影响。",
|
|
6474
7373
|
"row.poolRowRemoving": "正在删除账号…",
|
|
6475
7374
|
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
6476
|
-
"row.showCreditsCn": "
|
|
6477
|
-
"row.showCreditsGlobal": "
|
|
7375
|
+
"row.showCreditsCn": "在输入框工具栏显示积分读数",
|
|
7376
|
+
"row.showCreditsGlobal": "在输入框工具栏显示积分读数",
|
|
6478
7377
|
"row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
|
|
6479
|
-
"row.creditPositionLabel": "
|
|
7378
|
+
"row.creditPositionLabel": "显示位置",
|
|
6480
7379
|
"row.creditPositionComposer": "输入框工具栏",
|
|
6481
7380
|
"row.creditPositionStats": "输入框下方",
|
|
6482
7381
|
"row.creditPositionAria": "{region} 的积分显示位置",
|
|
7382
|
+
"row.creditPositionHintInline": "读数显示在哪里。「输入框工具栏」是默认位置,与模型选择同一行。",
|
|
7383
|
+
"row.creditPositionDisabledHint": "主开关关闭时不可调整:先勾选上方「在输入框工具栏显示积分读数」。",
|
|
7384
|
+
"row.showCreditsHintInline": "当前为本 tab 服务的那个账号的余额。点一下能看各账号明细,另外每 5 分钟自动刷新一次。",
|
|
6483
7385
|
"quotaNotice.fallbackHead": "当前请求的额度已用尽",
|
|
6484
7386
|
"quotaNotice.supplement": "(部分模型另有每日额度,与积分无关)",
|
|
6485
7387
|
"row.creditPositionHint": "积分显示在哪里。「输入框工具栏」是默认位置(与模型选择同一行)。如果装了别的插件后读数不显示,改成「输入框下方」—— 那是「轮次 / 步数 / token」统计所在的那一行。",
|
|
@@ -6491,10 +7393,13 @@ window.__ModuleLoader__.load({
|
|
|
6491
7393
|
"composer.account": "账号",
|
|
6492
7394
|
"composer.credits": "积分",
|
|
6493
7395
|
"composer.switchTo": "切换到 {account}",
|
|
7396
|
+
"composer.current": "当前选用",
|
|
6494
7397
|
"composer.noAccounts": "该区域暂无账号",
|
|
6495
7398
|
"composer.signedOut": "未登录",
|
|
6496
|
-
"composer.
|
|
6497
|
-
"composer.
|
|
7399
|
+
"composer.testAll": "一键测试",
|
|
7400
|
+
"composer.testing": "正在测试…",
|
|
7401
|
+
"composer.testAllHint": "逐个测试本区所有账号(一次一个,避免触发上游限流)",
|
|
7402
|
+
"composer.testFailed": "测试失败",
|
|
6498
7403
|
"composer.refreshFailed": "积分刷新失败",
|
|
6499
7404
|
"composer.lastRefresh": "最近刷新",
|
|
6500
7405
|
"composer.panelTitle": "WorkBuddy 积分",
|