dsh-connect-workbuddy 4.13.2 → 4.14.1
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 +75 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +381 -54
- package/lib/{host-heartbeat-BJAL9pEe.js → host-heartbeat-D1WnqQY2.js} +409 -61
- package/lib/index.d.ts +234 -21
- package/lib/index.js +6 -66
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -8,19 +8,6 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
9
|
let react_dom = require("react-dom");
|
|
10
10
|
//#region src/status-paths.ts
|
|
11
|
-
/**
|
|
12
|
-
* Node-free constants and types shared by the Host and browser halves.
|
|
13
|
-
*
|
|
14
|
-
* 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
|
|
15
|
-
* — 「3 条同源只读路由(usage / models:refresh / accounts:refresh)+ 一份
|
|
16
|
-
* 与浏览器共享的 node-free 类型定义」的 host↔client 桥梁形态来自该项目
|
|
17
|
-
* (其 `status-paths.ts` 亦如此,并注明沿用
|
|
18
|
-
* corrinehu/dsh-workbuddy-connect 的 status-route 模式)。
|
|
19
|
-
* 改动:路由路径改用本插件 id;类型字段按 WorkBuddy 上游实际给出的能力
|
|
20
|
-
* (积分倍率、多模态、推理档位)调整,不保留 trae 的 1M 变体字段。
|
|
21
|
-
*
|
|
22
|
-
* @module dsh-connect-workbuddy/status-paths
|
|
23
|
-
*/
|
|
24
11
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
25
12
|
const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
26
13
|
/**
|
|
@@ -1514,6 +1501,227 @@ window.__ModuleLoader__.load({
|
|
|
1514
1501
|
return t("row.poolProbeDaysAgo", { count: Math.round(age / DAY_MS) });
|
|
1515
1502
|
}
|
|
1516
1503
|
//#endregion
|
|
1504
|
+
//#region src/native-modality.ts
|
|
1505
|
+
/**
|
|
1506
|
+
* Reviewed vendor classifications, keyed by EXACT model id (never a prefix).
|
|
1507
|
+
* Ids absent from this record are `unknown` — see {@link nativeModalityOf}.
|
|
1508
|
+
*/
|
|
1509
|
+
const NATIVE_MODALITY_BY_MODEL_ID = {
|
|
1510
|
+
"deepseek-v4-pro": "text",
|
|
1511
|
+
"glm-5.1": "text",
|
|
1512
|
+
"glm-5.2": "text",
|
|
1513
|
+
"glm-5.3": "text",
|
|
1514
|
+
"hy3": "text",
|
|
1515
|
+
"hy4-preview": "text",
|
|
1516
|
+
"deepseek-v4.1-flash": "multimodal",
|
|
1517
|
+
"glm-5.3-flash": "multimodal",
|
|
1518
|
+
"glm-5v-turbo": "multimodal",
|
|
1519
|
+
"kimi-k2.6": "multimodal",
|
|
1520
|
+
"kimi-k2.7": "multimodal",
|
|
1521
|
+
"kimi-k3-1": "multimodal",
|
|
1522
|
+
"minimax-m3": "multimodal"
|
|
1523
|
+
};
|
|
1524
|
+
/**
|
|
1525
|
+
* Ids the upstream lists in the model roster that are ROUTERS, not models.
|
|
1526
|
+
*
|
|
1527
|
+
* `auto` (CN) is the one observed today: the gateway resolves it to whichever
|
|
1528
|
+
* model it considers the best fit (measured: CN `auto` → `hy4-preview`), so its
|
|
1529
|
+
* price, context window and behaviour are decided at request time by something
|
|
1530
|
+
* else. Offering it as a row advertises a model whose terms the plugin cannot
|
|
1531
|
+
* state, and it duplicates a real row already in the same roster.
|
|
1532
|
+
*
|
|
1533
|
+
* Kept as an explicit table rather than a heuristic: a router is an upstream
|
|
1534
|
+
* concept with no declared flag, and guessing from the name would eventually
|
|
1535
|
+
* hide a real model. Add an id here only after the same kind of evidence.
|
|
1536
|
+
*/
|
|
1537
|
+
const ROUTER_MODEL_IDS = /* @__PURE__ */ new Set(["auto"]);
|
|
1538
|
+
/** Whether this id is an upstream router rather than a model. */
|
|
1539
|
+
function isRouterModelId(modelId) {
|
|
1540
|
+
return ROUTER_MODEL_IDS.has(modelId);
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Native modality of one model id. Exact match only: a new model never
|
|
1544
|
+
* inherits a classification from its family, its display name, or the
|
|
1545
|
+
* platform's image flag.
|
|
1546
|
+
*/
|
|
1547
|
+
function nativeModalityOf(modelId) {
|
|
1548
|
+
if (isRouterModelId(modelId)) return "router";
|
|
1549
|
+
return NATIVE_MODALITY_BY_MODEL_ID[modelId] ?? "unknown";
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Whether a model refresh should PRE-CHECK this model's image box.
|
|
1553
|
+
*
|
|
1554
|
+
* Only a documented `multimodal` is pre-checked. `text` is documented as
|
|
1555
|
+
* unable to read images, and `unknown` is deliberately left unchecked rather
|
|
1556
|
+
* than guessed — the whole reason this table exists is that the platform flag
|
|
1557
|
+
* guessed wrong. A platform `supportsImages: false` still vetoes, so a
|
|
1558
|
+
* documented-multimodal id can never be pre-checked against the platform's
|
|
1559
|
+
* explicit "no".
|
|
1560
|
+
*
|
|
1561
|
+
* The user can always tick a box by hand; this decides a DEFAULT only, and the
|
|
1562
|
+
* effective runtime flag remains the saved `imageModelIds`.
|
|
1563
|
+
*/
|
|
1564
|
+
function imageDefaultFor(info) {
|
|
1565
|
+
if (info.supportsImages === false) return false;
|
|
1566
|
+
return NATIVE_MODALITY_BY_MODEL_ID[info.id] === "multimodal";
|
|
1567
|
+
}
|
|
1568
|
+
//#endregion
|
|
1569
|
+
//#region src/promo.ts
|
|
1570
|
+
/**
|
|
1571
|
+
* `'HH:mm'` → minutes since midnight, or undefined for anything else.
|
|
1572
|
+
*
|
|
1573
|
+
* `24:00` is accepted and means exactly 1440 — the END of a day. The upstream
|
|
1574
|
+
* writes "all day" as `0:00`–`23:59` today, which leaves a one-minute hole
|
|
1575
|
+
* between 23:59 and midnight where a standing campaign reads as not running.
|
|
1576
|
+
* Accepting `24:00` lets such a window be expressed (and lets a future upstream
|
|
1577
|
+
* fix it) without that hole; as a START it is rejected, because a day that
|
|
1578
|
+
* begins at 24:00 is the next day and that is a different statement.
|
|
1579
|
+
*
|
|
1580
|
+
* Any other malformed value is left to the caller to drop — a wrong window
|
|
1581
|
+
* would hide a live promotion, and guessing is not this parser's job.
|
|
1582
|
+
*/
|
|
1583
|
+
function minutesOfClock(value) {
|
|
1584
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
|
1585
|
+
if (match === null) return void 0;
|
|
1586
|
+
const hours = Number(match[1]);
|
|
1587
|
+
const minutes = Number(match[2]);
|
|
1588
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return void 0;
|
|
1589
|
+
if (minutes > 59) return void 0;
|
|
1590
|
+
if (hours > 24) return void 0;
|
|
1591
|
+
if (hours === 24) return minutes === 0 ? 1440 : void 0;
|
|
1592
|
+
return hours * 60 + minutes;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Minutes since local midnight in `timeZone` at `nowMs`.
|
|
1596
|
+
*
|
|
1597
|
+
* `Intl` does the zone maths, because the upstream's windows are written in
|
|
1598
|
+
* the zone IT names (Asia/Shanghai for the CN feed) and the user's own clock
|
|
1599
|
+
* is not necessarily in that zone. An unusable zone falls back to UTC rather
|
|
1600
|
+
* than throwing: a badge that cannot be placed is a missing badge, not a
|
|
1601
|
+
* broken card.
|
|
1602
|
+
*/
|
|
1603
|
+
function zonedMinutes(nowMs, timeZone) {
|
|
1604
|
+
const zone = timeZone ?? "UTC";
|
|
1605
|
+
try {
|
|
1606
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
1607
|
+
timeZone: zone,
|
|
1608
|
+
hour: "2-digit",
|
|
1609
|
+
minute: "2-digit",
|
|
1610
|
+
hourCycle: "h23"
|
|
1611
|
+
}).formatToParts(new Date(nowMs));
|
|
1612
|
+
const hour = Number(parts.find((part) => part.type === "hour")?.value ?? "0");
|
|
1613
|
+
const minute = Number(parts.find((part) => part.type === "minute")?.value ?? "0");
|
|
1614
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return 0;
|
|
1615
|
+
return hour * 60 + minute;
|
|
1616
|
+
} catch {
|
|
1617
|
+
return 0;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/** Whether `nowMinutes` falls inside one of the daily windows. */
|
|
1621
|
+
function withinDailyWindows(windows, nowMinutes) {
|
|
1622
|
+
return windows.some((window) => {
|
|
1623
|
+
const start = minutesOfClock(window.start);
|
|
1624
|
+
const end = minutesOfClock(window.end);
|
|
1625
|
+
if (start === void 0 || end === void 0) return false;
|
|
1626
|
+
if (start === end) return true;
|
|
1627
|
+
if (end > start) return nowMinutes >= start && nowMinutes < end;
|
|
1628
|
+
return nowMinutes >= start || nowMinutes < end;
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Whether the promotion's ABSOLUTE window holds now.
|
|
1633
|
+
*
|
|
1634
|
+
* The absolute window (`validFrom` / `validUntil`) is the promotion's own
|
|
1635
|
+
* lifetime. It gates the badge on its own: a promotion whose lifetime has
|
|
1636
|
+
* ended must not keep advertising itself, whatever its daily hours say.
|
|
1637
|
+
*/
|
|
1638
|
+
function promoWithinAbsoluteWindow(promo, nowMs) {
|
|
1639
|
+
const schedule = promo.schedule;
|
|
1640
|
+
if (schedule === void 0) return true;
|
|
1641
|
+
if (schedule.validFrom !== void 0 && nowMs < schedule.validFrom) return false;
|
|
1642
|
+
if (schedule.validUntil !== void 0 && nowMs >= schedule.validUntil) return false;
|
|
1643
|
+
return true;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Whether the promotion's DAILY window holds now.
|
|
1647
|
+
*
|
|
1648
|
+
* The daily window is the discount's own hours — it is what makes a
|
|
1649
|
+
* promotion a "night discount" — and a promotion without one is running
|
|
1650
|
+
* whenever its absolute window holds.
|
|
1651
|
+
*
|
|
1652
|
+
* A schedule whose `daily` was published but could not be evaluated
|
|
1653
|
+
* ({@link WorkBuddyPromoSchedule.dailyUnusable}) is NOT running: the upstream
|
|
1654
|
+
* did say the campaign has hours, we just cannot read them, and assuming
|
|
1655
|
+
* "always" is the one guess that overstates the offer.
|
|
1656
|
+
*/
|
|
1657
|
+
function promoWithinDailyWindow(promo, nowMs) {
|
|
1658
|
+
const schedule = promo.schedule;
|
|
1659
|
+
if (schedule === void 0) return true;
|
|
1660
|
+
if (schedule.dailyUnusable === true) return false;
|
|
1661
|
+
if (schedule.daily === void 0 || schedule.daily.length === 0) return true;
|
|
1662
|
+
return withinDailyWindows(schedule.daily, zonedMinutes(nowMs, schedule.timezone));
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Whether the promotion is running right now: its absolute window AND its
|
|
1666
|
+
* daily hours both hold.
|
|
1667
|
+
*/
|
|
1668
|
+
function promoActiveAt(promo, nowMs) {
|
|
1669
|
+
return promoWithinAbsoluteWindow(promo, nowMs) && promoWithinDailyWindow(promo, nowMs);
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Whether the promotion carries ANY temporal statement of its own.
|
|
1673
|
+
*
|
|
1674
|
+
* A tier badge ("Pro Only") has no `schedule` at all: it describes the model
|
|
1675
|
+
* unconditionally, so `promoActiveAt` is trivially true for it. That is
|
|
1676
|
+
* correct on its own, but it made such a badge eligible for step 1, where it
|
|
1677
|
+
* could shadow a lower-priority campaign whose window really is open.
|
|
1678
|
+
* "Running" should mean "this entry says it runs now", and an entry with no
|
|
1679
|
+
* schedule never does — it only ever "stands". Entries WITH a schedule (a
|
|
1680
|
+
* lifetime, hours, or both) are the ones that can be running.
|
|
1681
|
+
*/
|
|
1682
|
+
function hasSchedule(promo) {
|
|
1683
|
+
return promo.schedule !== void 0;
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* The badge to draw for a model right now, or undefined when there is none.
|
|
1687
|
+
*
|
|
1688
|
+
* The order of the two answers is the whole point:
|
|
1689
|
+
*
|
|
1690
|
+
* 1. A promotion RUNNING NOW wins — highest `priority` first. "Running" means
|
|
1691
|
+
* its absolute window AND its daily hours both hold.
|
|
1692
|
+
* 2. Otherwise a STANDING badge (one the upstream did not mark `activeOnly`)
|
|
1693
|
+
* describes the model for as long as the promotion itself lasts. It is
|
|
1694
|
+
* offered only inside the promotion's absolute window, so an expired
|
|
1695
|
+
* promotion stops advertising itself even though its badge was standing.
|
|
1696
|
+
*
|
|
1697
|
+
* TWO RULES KEEP STEP 2 FROM STATING SOMETHING THE UPSTREAM DID NOT:
|
|
1698
|
+
*
|
|
1699
|
+
* a. An entry that publishes daily hours may stand outside them (that is what
|
|
1700
|
+
* "the hours govern the discount, not the badge" means), but an entry whose
|
|
1701
|
+
* hours could not be parsed cannot stand at all — see `dailyUnusable`.
|
|
1702
|
+
* b. A standing entry with NO schedule at all must not outrank a promotion
|
|
1703
|
+
* that is genuinely running. `promoActiveAt` returns true for a
|
|
1704
|
+
* schedule-less tier, which is correct on its own, but it made a
|
|
1705
|
+
* high-priority tier shadow a lower-priority campaign whose window really
|
|
1706
|
+
* is open. Step 1 is therefore restricted to entries that carry a
|
|
1707
|
+
* `schedule`; the schedule-less badges compete only in step 2, where they
|
|
1708
|
+
* belong.
|
|
1709
|
+
*
|
|
1710
|
+
* The upstream's own `priority` orders entries. Ties are broken toward the
|
|
1711
|
+
* promotion: a price badge ("Free now") is more actionable than an access-tier
|
|
1712
|
+
* badge ("Pro Only"), and the upstream numbers them equally.
|
|
1713
|
+
*
|
|
1714
|
+
* Only ONE badge is drawn per model: a model row is one line wide, and two
|
|
1715
|
+
* badges would push the model name and the image/think checkboxes out of it.
|
|
1716
|
+
*/
|
|
1717
|
+
function selectPromoBadge(promos, nowMs) {
|
|
1718
|
+
if (promos === void 0 || promos.length === 0) return void 0;
|
|
1719
|
+
const sorted = [...promos].sort((a, b) => b.priority - a.priority || (a.source === b.source ? 0 : a.source === "promotion" ? -1 : 1));
|
|
1720
|
+
const running = sorted.find((promo) => hasSchedule(promo) && promoActiveAt(promo, nowMs));
|
|
1721
|
+
if (running !== void 0) return running;
|
|
1722
|
+
return sorted.find((promo) => promo.badge.display !== "activeOnly" && promoWithinAbsoluteWindow(promo, nowMs) && promo.schedule?.dailyUnusable !== true);
|
|
1723
|
+
}
|
|
1724
|
+
//#endregion
|
|
1517
1725
|
//#region src/probe.ts
|
|
1518
1726
|
/**
|
|
1519
1727
|
* The probe sizes the card offers, in tokens.
|
|
@@ -3592,53 +3800,135 @@ window.__ModuleLoader__.load({
|
|
|
3592
3800
|
});
|
|
3593
3801
|
}
|
|
3594
3802
|
//#endregion
|
|
3595
|
-
//#region src/
|
|
3803
|
+
//#region src/client/promo-badge.ts
|
|
3596
3804
|
/**
|
|
3597
|
-
*
|
|
3598
|
-
*
|
|
3805
|
+
* The UI's language, as the browser reports it.
|
|
3806
|
+
*
|
|
3807
|
+
* The card already formats dates and numbers with the browser's locale
|
|
3808
|
+
* (`formatDate`), so reading the same signal for the upstream's copy is
|
|
3809
|
+
* consistent: the badge says what the upstream says, in the language the
|
|
3810
|
+
* user reads.
|
|
3811
|
+
*
|
|
3812
|
+
* `navigator` is read defensively — this module is imported by unit tests
|
|
3813
|
+
* that run in a plain Node environment, where it does not exist.
|
|
3599
3814
|
*/
|
|
3600
|
-
|
|
3601
|
-
"
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
"
|
|
3606
|
-
|
|
3607
|
-
"deepseek-v4.1-flash": "multimodal",
|
|
3608
|
-
"glm-5.3-flash": "multimodal",
|
|
3609
|
-
"glm-5v-turbo": "multimodal",
|
|
3610
|
-
"kimi-k2.6": "multimodal",
|
|
3611
|
-
"kimi-k2.7": "multimodal",
|
|
3612
|
-
"kimi-k3-1": "multimodal",
|
|
3613
|
-
"minimax-m3": "multimodal"
|
|
3614
|
-
};
|
|
3615
|
-
/** The `auto` entry is a router that picks a model, not a model itself. */
|
|
3616
|
-
const ROUTER_MODEL_ID = "auto";
|
|
3815
|
+
function uiLanguage() {
|
|
3816
|
+
return typeof navigator === "undefined" ? "" : navigator.language;
|
|
3817
|
+
}
|
|
3818
|
+
/** Whether the given language is an English one. */
|
|
3819
|
+
function isEnglish(language) {
|
|
3820
|
+
return (language ?? "").toLowerCase().startsWith("en");
|
|
3821
|
+
}
|
|
3617
3822
|
/**
|
|
3618
|
-
*
|
|
3619
|
-
*
|
|
3620
|
-
*
|
|
3823
|
+
* The badge's own wording.
|
|
3824
|
+
*
|
|
3825
|
+
* The upstream's label is the region's own wording, so it is the default for
|
|
3826
|
+
* every UI. An English UI prefers the English label when the upstream
|
|
3827
|
+
* published one; the global feed publishes none, and there its label is
|
|
3828
|
+
* already English.
|
|
3621
3829
|
*/
|
|
3622
|
-
function
|
|
3623
|
-
|
|
3624
|
-
return
|
|
3830
|
+
function promoBadgeLabel(promo, language) {
|
|
3831
|
+
const badge = promo.badge;
|
|
3832
|
+
if (isEnglish(language)) return badge.labelEn ?? badge.label;
|
|
3833
|
+
return badge.label;
|
|
3625
3834
|
}
|
|
3626
3835
|
/**
|
|
3627
|
-
*
|
|
3836
|
+
* The chip's colour, when the upstream published a usable one.
|
|
3628
3837
|
*
|
|
3629
|
-
* Only a
|
|
3630
|
-
*
|
|
3631
|
-
*
|
|
3632
|
-
*
|
|
3633
|
-
|
|
3634
|
-
|
|
3838
|
+
* Only a hex colour is accepted: the value reaches an inline style, and the
|
|
3839
|
+
* upstream's feed publishes hex (`#009273`, `#FF0000`). Anything else is
|
|
3840
|
+
* refused rather than passed through, and the card falls back to its own
|
|
3841
|
+
* brand colour.
|
|
3842
|
+
*/
|
|
3843
|
+
function promoBadgeColor(promo) {
|
|
3844
|
+
const color = promo.badge.color;
|
|
3845
|
+
if (color === void 0) return void 0;
|
|
3846
|
+
return /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color) ? color : void 0;
|
|
3847
|
+
}
|
|
3848
|
+
/**
|
|
3849
|
+
* A calendar day, in the user's own locale — read in the ZONE the upstream
|
|
3850
|
+
* wrote the instant in.
|
|
3635
3851
|
*
|
|
3636
|
-
* The
|
|
3637
|
-
*
|
|
3852
|
+
* The zone is part of the statement, exactly as it is for the daily hours. The
|
|
3853
|
+
* upstream publishes `validFrom: "2026-07-06T00:00:00+08:00"` and describes the
|
|
3854
|
+
* same campaign as "7月6日–10月31日". Formatting that instant in the reader's
|
|
3855
|
+
* own zone shows 7月5日 to anyone west of UTC, contradicting the upstream's own
|
|
3856
|
+
* copy in the same tooltip. Keeping the upstream's zone makes the tooltip
|
|
3857
|
+
* self-consistent wherever it is read.
|
|
3858
|
+
*
|
|
3859
|
+
* `medium` rather than the card's numeric `MM/DD` form: this is a promotion's
|
|
3860
|
+
* lifetime, and "Jul 6, 2026" reads as a date while "07/06" reads as a number
|
|
3861
|
+
* the user has to decode.
|
|
3638
3862
|
*/
|
|
3639
|
-
function
|
|
3640
|
-
|
|
3641
|
-
|
|
3863
|
+
function formatDay(ms, language, timeZone) {
|
|
3864
|
+
try {
|
|
3865
|
+
return new Intl.DateTimeFormat(language === void 0 || language === "" ? void 0 : language, {
|
|
3866
|
+
year: "numeric",
|
|
3867
|
+
month: "short",
|
|
3868
|
+
day: "numeric",
|
|
3869
|
+
...timeZone === void 0 ? {} : { timeZone }
|
|
3870
|
+
}).format(new Date(ms));
|
|
3871
|
+
} catch {
|
|
3872
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
/**
|
|
3876
|
+
* The promotion's lifetime, as the upstream's own two ends.
|
|
3877
|
+
*
|
|
3878
|
+
* `validUntil` is EXCLUSIVE — see `promoWithinAbsoluteWindow`: the promotion is
|
|
3879
|
+
* over AT that instant. The upstream writes it as the midnight that opens the
|
|
3880
|
+
* day after the campaign, and says "– Oct 31" in its own copy for a
|
|
3881
|
+
* `validUntil` of `2026-11-01T00:00:00+08:00`. Printing the raw instant would
|
|
3882
|
+
* put "Nov 1, 2026" in the same tooltip and read as if the campaign ran a day
|
|
3883
|
+
* longer than it does. One millisecond before the boundary is the last instant
|
|
3884
|
+
* that still belongs to it, so that is what gets rendered.
|
|
3885
|
+
*/
|
|
3886
|
+
function promoRangeText(promo, language) {
|
|
3887
|
+
const schedule = promo.schedule;
|
|
3888
|
+
if (schedule === void 0) return void 0;
|
|
3889
|
+
const zone = schedule.timezone;
|
|
3890
|
+
const from = schedule.validFrom === void 0 ? void 0 : formatDay(schedule.validFrom, language, zone);
|
|
3891
|
+
const to = schedule.validUntil === void 0 ? void 0 : formatDay(schedule.validUntil - 1, language, zone);
|
|
3892
|
+
if (from === void 0 && to === void 0) return void 0;
|
|
3893
|
+
if (from === void 0) return `– ${to ?? ""}`;
|
|
3894
|
+
if (to === void 0) return `${from} –`;
|
|
3895
|
+
return `${from} – ${to}`;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* The hours the discount runs, in the zone the upstream named.
|
|
3899
|
+
*
|
|
3900
|
+
* The zone is part of the statement: "23:00–7:50" means nothing to a reader
|
|
3901
|
+
* in another zone, and the upstream's windows are written in ITS zone
|
|
3902
|
+
* (Asia/Shanghai for the CN feed), not the user's.
|
|
3903
|
+
*/
|
|
3904
|
+
function promoHoursText(promo) {
|
|
3905
|
+
const schedule = promo.schedule;
|
|
3906
|
+
const daily = schedule?.daily;
|
|
3907
|
+
if (daily === void 0 || daily.length === 0) return void 0;
|
|
3908
|
+
const windows = daily.map((window) => `${window.start}–${window.end}`).join(", ");
|
|
3909
|
+
const zone = schedule?.timezone;
|
|
3910
|
+
return zone === void 0 ? windows : `${windows} (${zone})`;
|
|
3911
|
+
}
|
|
3912
|
+
/**
|
|
3913
|
+
* The chip's tooltip: the upstream's own copy, its lifetime, and the hours
|
|
3914
|
+
* the discount runs.
|
|
3915
|
+
*
|
|
3916
|
+
* Empty pieces are left out rather than rendered as blanks, so a promotion
|
|
3917
|
+
* that publishes nothing but a label gets a chip with a label and no hover
|
|
3918
|
+
* text at all.
|
|
3919
|
+
*/
|
|
3920
|
+
function promoBadgeTooltip(promo, language) {
|
|
3921
|
+
const parts = [];
|
|
3922
|
+
const hover = promo.hover;
|
|
3923
|
+
if (hover !== void 0) {
|
|
3924
|
+
const copy = isEnglish(language) ? hover.textEn ?? hover.textZh : hover.textZh ?? hover.textEn;
|
|
3925
|
+
if (copy !== void 0) parts.push(copy);
|
|
3926
|
+
}
|
|
3927
|
+
const range = promoRangeText(promo, language);
|
|
3928
|
+
if (range !== void 0) parts.push(range);
|
|
3929
|
+
const hours = promoHoursText(promo);
|
|
3930
|
+
if (hours !== void 0) parts.push(hours);
|
|
3931
|
+
return parts.join(" · ");
|
|
3642
3932
|
}
|
|
3643
3933
|
//#endregion
|
|
3644
3934
|
//#region src/client/icon.ts
|
|
@@ -3894,6 +4184,12 @@ window.__ModuleLoader__.load({
|
|
|
3894
4184
|
.dsm-workbuddy-model-copy{display:flex;align-items:baseline;gap:8px;min-width:0}
|
|
3895
4185
|
.dsm-workbuddy-model-name{display:inline-flex;align-items:baseline;gap:7px;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:13px;font-weight:500;line-height:19px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3896
4186
|
.dsm-workbuddy-model-name-rate{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:400;line-height:16px;flex:none}
|
|
4187
|
+
/* The upstream's own promotion badge ("限时免费" / "Free now" / "Pro Only").
|
|
4188
|
+
The colour is the upstream's own, handed in as a custom property; without
|
|
4189
|
+
one the brand colour stands in. "flex:none" keeps the badge whole while the
|
|
4190
|
+
model name beside it truncates with an ellipsis. The plain background comes
|
|
4191
|
+
first so an engine without color-mix still gets a readable chip. */
|
|
4192
|
+
.dsm-workbuddy-model-promo{flex:none;display:inline-flex;align-items:center;padding:0 7px;border-radius:999px;font-size:10px;line-height:16px;font-weight:600;white-space:nowrap;color:var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe));background:rgba(255,255,255,.08);background:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 16%,transparent);border:1px solid transparent;border-color:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 40%,transparent)}
|
|
3897
4193
|
.dsm-workbuddy-model-id{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3898
4194
|
.dsm-workbuddy-model-desc{margin:0;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
|
|
3899
4195
|
.dsm-workbuddy-model-details{display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0}
|
|
@@ -4666,6 +4962,37 @@ window.__ModuleLoader__.load({
|
|
|
4666
4962
|
default: return t("row.modelImageUnverified");
|
|
4667
4963
|
}
|
|
4668
4964
|
}
|
|
4965
|
+
/**
|
|
4966
|
+
* The upstream's own promotion badge for one model, or nothing.
|
|
4967
|
+
*
|
|
4968
|
+
* Drawn from the model's UNRESOLVED promo list at RENDER time, because a badge
|
|
4969
|
+
* is a function of the clock: the upstream's promotions run on wall-clock
|
|
4970
|
+
* hours in a named zone, so a badge chosen when the document was read would
|
|
4971
|
+
* keep claiming the night after the night is over. The card re-renders at
|
|
4972
|
+
* least once a minute while it is open (it polls the usage route), so a
|
|
4973
|
+
* window edge is picked up within a minute without a second timer.
|
|
4974
|
+
*
|
|
4975
|
+
* The chip says what the UPSTREAM says — its label, its colour, its hover
|
|
4976
|
+
* copy — and adds nothing of its own: a badge the user cannot trace back to
|
|
4977
|
+
* the upstream's feed is worse than no badge.
|
|
4978
|
+
*
|
|
4979
|
+
* One badge per model: the upstream's `priority` orders the entries that name
|
|
4980
|
+
* a model, and a model row is one line wide — two badges would push the
|
|
4981
|
+
* model name out of the row.
|
|
4982
|
+
*/
|
|
4983
|
+
function promoChip(model) {
|
|
4984
|
+
const promo = selectPromoBadge(model.promos, Date.now());
|
|
4985
|
+
if (promo === void 0) return null;
|
|
4986
|
+
const language = uiLanguage();
|
|
4987
|
+
const color = promoBadgeColor(promo);
|
|
4988
|
+
const tooltip = promoBadgeTooltip(promo, language);
|
|
4989
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4990
|
+
className: "dsm-workbuddy-model-promo",
|
|
4991
|
+
title: tooltip === "" ? void 0 : tooltip,
|
|
4992
|
+
style: color === void 0 ? void 0 : { "--dsm-workbuddy-promo-color": color },
|
|
4993
|
+
children: promoBadgeLabel(promo, language)
|
|
4994
|
+
});
|
|
4995
|
+
}
|
|
4669
4996
|
/** Inject or refresh the shared card CSS for the current client bundle. */
|
|
4670
4997
|
if (typeof document !== "undefined") {
|
|
4671
4998
|
const cssId = "dsh-connect-workbuddy/client.css";
|
|
@@ -5938,9 +6265,9 @@ window.__ModuleLoader__.load({
|
|
|
5938
6265
|
onChange: () => {
|
|
5939
6266
|
toggleModel(model.id);
|
|
5940
6267
|
}
|
|
5941
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
6268
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5942
6269
|
className: "dsm-workbuddy-model-copy",
|
|
5943
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6270
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5944
6271
|
className: "dsm-workbuddy-model-name",
|
|
5945
6272
|
children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5946
6273
|
className: "dsm-workbuddy-model-name-rate",
|
|
@@ -5950,7 +6277,7 @@ window.__ModuleLoader__.load({
|
|
|
5950
6277
|
"x)"
|
|
5951
6278
|
]
|
|
5952
6279
|
})]
|
|
5953
|
-
})
|
|
6280
|
+
}), promoChip(model)]
|
|
5954
6281
|
})]
|
|
5955
6282
|
}),
|
|
5956
6283
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|