dsh-desktop-statusbar 2.1.1 → 2.1.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/README.en.md +130 -108
- package/README.md +128 -108
- package/docs/pricing.en.png +0 -0
- package/docs/pricing.png +0 -0
- package/docs/settings.en.png +0 -0
- package/docs/settings.png +0 -0
- package/lib/balance-api.js +11 -46
- package/lib/client.js +340 -49
- package/lib/index.js +572 -250
- package/lib/release-age.js +390 -0
- package/lib/self-update.js +368 -0
- package/package.json +6 -1
- package/tools/plugin-dir.cjs +112 -0
- package/tools/shot.cjs +365 -0
- package/tools/sync-from-plugin.cjs +49 -0
- package/tools/sync-to-plugin.cjs +71 -0
- package/tools/test.cjs +99 -0
package/lib/client.js
CHANGED
|
@@ -50,6 +50,8 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
const PRICES_URL = "/dsh-desktop-statusbar/api/prices";
|
|
51
51
|
const HOLIDAYS_URL = "/dsh-desktop-statusbar/api/holidays";
|
|
52
52
|
const VERSION_URL = "/dsh-desktop-statusbar/api/version";
|
|
53
|
+
/** 一键更新:由本插件自己的 host 端执行(不依赖任何别的插件,见 self-update 说明)。 */
|
|
54
|
+
const SELF_UPDATE_URL = "/dsh-desktop-statusbar/api/self-update";
|
|
53
55
|
/* 子智能体(Agent Teams 的队友)用量的聚合端点:队友是**独立会话**,它的消耗不在本会话
|
|
54
56
|
的投影里,只能由 host 按 parentSession 找出来,「总计」「本轮」才真的把队友算进去。
|
|
55
57
|
带 since(毫秒)= 只要那个时刻之后的用量,客户端拿它取「本轮」这一段。 */
|
|
@@ -66,7 +68,7 @@ window.__ModuleLoader__.load({
|
|
|
66
68
|
*/
|
|
67
69
|
const CONFIG_VERSION = 6;
|
|
68
70
|
/** 插件版本:显示在设置页底部,用来确认页面加载的是哪一版客户端代码。 */
|
|
69
|
-
const VERSION = "2.1.
|
|
71
|
+
const VERSION = "2.1.2";
|
|
70
72
|
/** 官方参考价的版本:官方调价时改这个数字,老价格库会自动并入新参考价。 */
|
|
71
73
|
const PRICE_VERSION = 2;
|
|
72
74
|
/** 项目主页:设置页那个「去 GitHub 点 star」按钮跳这里(与 package.json 的 repository 同址)。 */
|
|
@@ -650,6 +652,21 @@ window.__ModuleLoader__.load({
|
|
|
650
652
|
+ (bucket.output || 0) * tier.output) / 1e6;
|
|
651
653
|
}
|
|
652
654
|
|
|
655
|
+
/**
|
|
656
|
+
* 按**指定档位**算一段用量的费用(不按时刻判断)。返回未舍入数值;没单价返回 null。
|
|
657
|
+
* 专供 host 回传的 overflow 用 —— 那部分已经按档归好类(host 侧判的峰谷),
|
|
658
|
+
* 这里不能再拿一个时刻去猜档位。没勾「峰谷计价」的条目两档同价,取 peak 即可。
|
|
659
|
+
*/
|
|
660
|
+
function costOfTier(bucket, model, peak) {
|
|
661
|
+
const price = priceOf(model);
|
|
662
|
+
if (price === null || bucket === undefined || bucket === null) return null;
|
|
663
|
+
const tier = price.tiered === false ? price.peak : (peak === true ? price.peak : price.offPeak);
|
|
664
|
+
return ((bucket.input || 0) * tier.input
|
|
665
|
+
+ (bucket.cacheRead || 0) * tier.cacheRead
|
|
666
|
+
+ (bucket.cacheWrite || 0) * tier.cacheWrite
|
|
667
|
+
+ (bucket.output || 0) * tier.output) / 1e6;
|
|
668
|
+
}
|
|
669
|
+
|
|
653
670
|
/**
|
|
654
671
|
* 费用显示:一律两位小数,不满一分钱也进到 0.01(0.0075 → 0.01),不出现更长的小数位。
|
|
655
672
|
* 累加用 costOf 的原始值,只在这里做一次舍入。
|
|
@@ -984,6 +1001,11 @@ window.__ModuleLoader__.load({
|
|
|
984
1001
|
updateTo: "更新 {version} 版本",
|
|
985
1002
|
updateRunning: "正在更新中",
|
|
986
1003
|
updateFailed: "更新失败",
|
|
1004
|
+
updateDone: "更新成功,重启 DSH 后生效",
|
|
1005
|
+
/* 开发模式:连点版本号 5 次进入。不做成可见控件 —— 它面向作者调试,
|
|
1006
|
+
普通用户装了插件也不该看到一个「预览」勾选框。进入后标题右侧挂一枚胶囊。 */
|
|
1007
|
+
devModeBadge: "开发模式",
|
|
1008
|
+
devModeHint: "开发模式:点更新按钮只演示状态,不会真的安装",
|
|
987
1009
|
sectionTitle: "状态栏设置",
|
|
988
1010
|
intro: "接管对话区底部的统计行。勾选要显示的字段、拖动调整顺序或配置模型费用单价。",
|
|
989
1011
|
segContext: "上下文占用",
|
|
@@ -1095,6 +1117,9 @@ window.__ModuleLoader__.load({
|
|
|
1095
1117
|
updateTo: "Update to {version}",
|
|
1096
1118
|
updateRunning: "Updating",
|
|
1097
1119
|
updateFailed: "Update failed",
|
|
1120
|
+
updateDone: "Update succeeded — restart DSH to apply",
|
|
1121
|
+
devModeBadge: "Dev mode",
|
|
1122
|
+
devModeHint: "Dev mode: the update button only simulates states — nothing is installed",
|
|
1098
1123
|
sectionTitle: "Status Bar Settings",
|
|
1099
1124
|
intro: "Replaces the stats line under the message input. Select the fields to show, drag to reorder, or set model prices.",
|
|
1100
1125
|
segContext: "Context usage",
|
|
@@ -1875,7 +1900,16 @@ window.__ModuleLoader__.load({
|
|
|
1875
1900
|
tool: "var(--dsw-static-blue-450)"
|
|
1876
1901
|
};
|
|
1877
1902
|
|
|
1878
|
-
/**
|
|
1903
|
+
/**
|
|
1904
|
+
* 日期键(YYYY-MM-DD),用来认「今天」那根柱子。
|
|
1905
|
+
*
|
|
1906
|
+
* 按**装机者所在时区**取,与 host 的 localDateKey 同一口径:使用者看的是自己
|
|
1907
|
+
* 日历上的今天/昨天。曾经固定用北京时间,UTC+8 装机时看不出差别,非 UTC+8 的
|
|
1908
|
+
* 机器上整块活跃总览会错一天(「今天」高亮成昨天、未来/过去的灰阶也偏)。
|
|
1909
|
+
*
|
|
1910
|
+
* 注意与 isPeakTime 区分:**峰谷计价**仍按北京时间,那是 DeepSeek 的官方规则;
|
|
1911
|
+
* 只有「日期归属」跟着装机者时区走。
|
|
1912
|
+
*/
|
|
1879
1913
|
function dateKeyOf(ms) {
|
|
1880
1914
|
const d = new Date(ms === undefined || ms === null ? Date.now() : ms);
|
|
1881
1915
|
const pad = (n) => (n < 10 ? "0" + n : String(n));
|
|
@@ -2128,7 +2162,6 @@ window.__ModuleLoader__.load({
|
|
|
2128
2162
|
const hasPeak = pkHit + pkMiss + pkOut > 0;
|
|
2129
2163
|
return h("span", {
|
|
2130
2164
|
className: "dsb-day"
|
|
2131
|
-
+ (day.date === tip.today ? " dsb-day-today" : "")
|
|
2132
2165
|
+ (activeBar === index ? " dsb-day-active" : ""),
|
|
2133
2166
|
key: "__wd" + index,
|
|
2134
2167
|
/* 悬停整列都让判定区变灰 —— 没有用量的天也一样;浮层只在有用量时弹 */
|
|
@@ -2817,6 +2850,27 @@ window.__ModuleLoader__.load({
|
|
|
2817
2850
|
priced = true;
|
|
2818
2851
|
total += toDisplayCurrency(one, src.models, callModel, symbolCurrency, src.defaultCurrency);
|
|
2819
2852
|
}
|
|
2853
|
+
/* 被 host 的 MAX_CALLS 截掉的那部分(host 按「模型 + 峰谷档」累计好给回来)。
|
|
2854
|
+
不补这一段的话,长会话的费用只算了最近 4000 条 —— 实测 5000 次少 20%、
|
|
2855
|
+
2 万次少 80%,而「总计用量」用的是官方全量数据,两者会在同一个界面上对不上。
|
|
2856
|
+
档位由 host 按北京时间判定(与逐条计价同一套规则),所以这里直接按档取价即可。 */
|
|
2857
|
+
const overflow = sessionUsage === undefined || sessionUsage === null
|
|
2858
|
+
|| sessionUsage.overflow === undefined || sessionUsage.overflow === null
|
|
2859
|
+
|| typeof sessionUsage.overflow !== "object"
|
|
2860
|
+
? {}
|
|
2861
|
+
: sessionUsage.overflow;
|
|
2862
|
+
const overflowNames = Object.keys(overflow);
|
|
2863
|
+
for (let i = 0; i < overflowNames.length; i += 1) {
|
|
2864
|
+
const overflowModel = overflowNames[i];
|
|
2865
|
+
const byTier = overflow[overflowModel];
|
|
2866
|
+
if (byTier === undefined || byTier === null) continue;
|
|
2867
|
+
const peakCost = costOfTier(byTier.peak, overflowModel, true);
|
|
2868
|
+
const offPeakCost = costOfTier(byTier.offPeak, overflowModel, false);
|
|
2869
|
+
if (peakCost === null && offPeakCost === null) continue;
|
|
2870
|
+
priced = true;
|
|
2871
|
+
total += toDisplayCurrency((peakCost === null ? 0 : peakCost) + (offPeakCost === null ? 0 : offPeakCost),
|
|
2872
|
+
src.models, overflowModel, symbolCurrency, src.defaultCurrency);
|
|
2873
|
+
}
|
|
2820
2874
|
/* 队友(子智能体)是**独立会话**,它的用量不在上面这份本会话调用表里,
|
|
2821
2875
|
host 按 parentSession 算好单独给回来;这里直接把数字并进「总计」,
|
|
2822
2876
|
不另开一行也不加任何提示字 —— 用户报的正是"总计没算队友"。
|
|
@@ -3086,8 +3140,9 @@ window.__ModuleLoader__.load({
|
|
|
3086
3140
|
* 两条不依赖官方构建 hash 的要点:
|
|
3087
3141
|
* 1. 用 .dsb-root 认自己,而不是官方组件的类名(hash 会随 DSH 更新变,2.0.10 踩过);
|
|
3088
3142
|
* 2. 自己带标记与“后代里带标记”都要判 —— :has() 只看后代,漏掉前者会把自己的行也藏掉。
|
|
3089
|
-
* 整条规则挂在 html.dsb-on
|
|
3090
|
-
*
|
|
3143
|
+
* 整条规则挂在 html.dsb-on 上:这个类由插件挂载/卸载时增删(设置页里
|
|
3144
|
+
* **没有**「启用状态栏」开关,别按那个理解)。卸下后官方原本的统计行与
|
|
3145
|
+
* 上下文按钮随即恢复(否则关掉插件只剩一片空白)。 */
|
|
3091
3146
|
'html.dsb-on [data-slot="conversation.composer.dock"] > *:not(.dsb-root):not(:has(.dsb-root)){display:none !important}',
|
|
3092
3147
|
/* 官方新版在输入卡片下方右侧另有一个上下文占用按钮:已被本插件的圆环接管,藏掉它。
|
|
3093
3148
|
* 用 aria-label 认它(中文含"上下文"、英文含"context used",随语言变),不碰官方 hash 类名。 */
|
|
@@ -3371,6 +3426,20 @@ window.__ModuleLoader__.load({
|
|
|
3371
3426
|
".dsb-settings .dsb-star-wrap:hover .dsb-star-tip,.dsb-settings .dsb-star-wrap:focus-within .dsb-star-tip{visibility:visible;opacity:1}",
|
|
3372
3427
|
/* 导航项后面的「发现新版本」胶囊:细描边圆角胶囊 + 次级文字色,字号刻意比导航文字小一档 */
|
|
3373
3428
|
".dsb-nav-pill{box-sizing:border-box;margin-left:4px;padding:0 5px;border:1px solid var(--dsw-alias-separator-primary,rgba(127,127,127,.3));border-radius:999px;font-size:8px;line-height:1.4;font-weight:400;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
|
|
3429
|
+
/* 开发模式**不给可见开关**:入口是页脚版本号的「连点 5 次」。
|
|
3430
|
+
胶囊做成标题的上标角标(像 x² 的那个 2):
|
|
3431
|
+
· 行内 flex + 靠左对齐 → 胶囊紧跟着标题文字,不甩到面板最右;
|
|
3432
|
+
· align-items:flex-start → 胶囊浮在标题文字的右上角,而不是垂直居中。
|
|
3433
|
+
三个刻意的取值:
|
|
3434
|
+
· font-size 与「发现新版本」那枚导航胶囊**逐字一致**(都走 .dsb-nav-pill 的 8px),
|
|
3435
|
+
所以这里不覆盖字号 —— 覆盖过一次(10px)就显得比导航那枚大,两处观感不统一。
|
|
3436
|
+
· **不做负 margin-top**:父级 .dsb-head 是纵向 flex,把胶囊往上顶出容器会被裁掉
|
|
3437
|
+
(实测缺陷:胶囊上边框被切)。
|
|
3438
|
+
· 用 `margin-top:2px` **往下压一点**:标题是 18px、行盒有约 2px 的行距余量,
|
|
3439
|
+
胶囊要落在这段余量里,顶端才不高于标题文字的上边界。 */
|
|
3440
|
+
".dsb-settings .dsb-titlerow{display:flex;align-items:flex-start;justify-content:flex-start;gap:4px}",
|
|
3441
|
+
".dsb-settings .dsb-dev-pill{margin-left:0;margin-top:2px;padding:0 5px;cursor:default}",
|
|
3442
|
+
".dsb-settings .dsb-version{user-select:none;-webkit-user-select:none}",
|
|
3374
3443
|
].join("");
|
|
3375
3444
|
|
|
3376
3445
|
/* 导航图标使用外部传入的 gauge(24x24 lucide,stroke=currentColor,随主题变色) */
|
|
@@ -3383,15 +3452,39 @@ window.__ModuleLoader__.load({
|
|
|
3383
3452
|
/* 导航项里挂「发现新版本」胶囊用:标记本插件那一项,避免胶囊文字混进文案后匹配不上。 */
|
|
3384
3453
|
const NAV_MARK = "data-dsb-nav";
|
|
3385
3454
|
|
|
3386
|
-
/* 宿主上下文(apply 时存下来):一键更新要用它的 remote 命名空间。 */
|
|
3387
|
-
let pluginCtx = null;
|
|
3388
|
-
|
|
3389
3455
|
/**
|
|
3390
3456
|
* 版本自检结果 —— 导航胶囊和设置页那颗「更新」按钮共用这一份,两边同进同出。
|
|
3391
3457
|
* 默认什么都没有:只有 registry 上确实更新才显示,查不到就安静地不显示。
|
|
3392
|
-
* note 是更新动作的结果:null / "done" / "failed"
|
|
3458
|
+
* note 是更新动作的结果:null / "done" / "failed"。
|
|
3393
3459
|
*/
|
|
3394
3460
|
let updateState = { hasUpdate: false, latest: null, busy: false, note: null, reason: null };
|
|
3461
|
+
|
|
3462
|
+
/**
|
|
3463
|
+
* 开发模式(连点版本号 5 次进入):让作者在没有新版本的时候也能看到更新按钮的
|
|
3464
|
+
* **全部**状态与动效,用来检查「点下去有没有反馈、文案看不看得懂、失败会不会静默」。
|
|
3465
|
+
*
|
|
3466
|
+
* 打开后:
|
|
3467
|
+
* - 按钮**常驻**显示(不看 registry 有没有新版),版本号用一个假的演示版本;
|
|
3468
|
+
* - 每点一次,依次演示「更新中 → 已更新 → 更新失败 → 回到空闲」四种状态;
|
|
3469
|
+
* - **绝不发安装请求**:走的是纯本地的状态机,不碰任何网络与子进程。
|
|
3470
|
+
*
|
|
3471
|
+
* 为什么不给一个可见的开关:普通用户装了这插件,界面上不该多出一个面向作者的调试项。
|
|
3472
|
+
* 入口做成「连点版本号 5 次」——记得住、够隐蔽,也不会被误触(这是常见的开发者模式手势)。
|
|
3473
|
+
* 进入后标题右侧挂一枚「开发模式」胶囊,明确告诉你当前处于这个状态;再连点 5 次退出。
|
|
3474
|
+
*
|
|
3475
|
+
* 它只影响本插件的界面,不改变任何真实配置;退出即恢复原样。
|
|
3476
|
+
*/
|
|
3477
|
+
let devMode = false;
|
|
3478
|
+
/** 演示用的假版本号:一眼看出不是真版本,避免被误当成真实更新。 */
|
|
3479
|
+
const DEV_VERSION = "9.9.9";
|
|
3480
|
+
/** 演示用的假失败原因,走的就是真实失败时那条显示路径。 */
|
|
3481
|
+
const DEV_FAIL_REASON = "release-age";
|
|
3482
|
+
/** 「更新中」演示的时长:够看清三个点的动效,又不至于让人等。 */
|
|
3483
|
+
const DEV_BUSY_MS = 2200;
|
|
3484
|
+
/** 进入/退出开发模式需要连点几次版本号。 */
|
|
3485
|
+
const DEV_CLICKS = 5;
|
|
3486
|
+
/** 连点计数:两次点击间隔超过这个毫秒数就重新计数(否则慢慢点 5 次也会进去)。 */
|
|
3487
|
+
const DEV_CLICK_WINDOW_MS = 1500;
|
|
3395
3488
|
const updateListeners = new Set();
|
|
3396
3489
|
function subscribeUpdate(fn) {
|
|
3397
3490
|
updateListeners.add(fn);
|
|
@@ -3413,7 +3506,13 @@ window.__ModuleLoader__.load({
|
|
|
3413
3506
|
if (has !== null && typeof has.remove === "function") has.remove();
|
|
3414
3507
|
return;
|
|
3415
3508
|
}
|
|
3416
|
-
|
|
3509
|
+
/* 已经挂上就只更新文字。**必须先比值**:写 textContent 即使内容一样也会
|
|
3510
|
+
替换文本节点、产生 childList 记录,而上面那个 observer 监听的正是
|
|
3511
|
+
childList+subtree —— 不比较就会「写 → 触发 → 下一帧再写」一直空转。 */
|
|
3512
|
+
if (has !== null) {
|
|
3513
|
+
if (has.textContent !== text) has.textContent = text;
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3417
3516
|
const pill = document.createElement("span");
|
|
3418
3517
|
pill.className = "dsb-nav-pill";
|
|
3419
3518
|
pill.textContent = text;
|
|
@@ -3436,38 +3535,144 @@ window.__ModuleLoader__.load({
|
|
|
3436
3535
|
.catch(() => null);
|
|
3437
3536
|
}
|
|
3438
3537
|
|
|
3439
|
-
/**
|
|
3538
|
+
/** 从更新结果里抠出人能看懂的原因;拿不到就给空串。 */
|
|
3440
3539
|
function reasonOf(result) {
|
|
3441
3540
|
const raw = result === null || result === undefined ? null : result.reason;
|
|
3442
3541
|
return typeof raw === "string" && raw.length > 0 ? raw : "";
|
|
3443
3542
|
}
|
|
3444
3543
|
|
|
3445
3544
|
/**
|
|
3446
|
-
*
|
|
3447
|
-
*
|
|
3545
|
+
* 一键更新:调**本插件自己的** host 路由,由它自己跑 pnpm 把新版本装上。
|
|
3546
|
+
*
|
|
3547
|
+
* 为什么不用 DSH 官方的 `ctx.remote.pluginManager.installBundle`:
|
|
3548
|
+
* 1. 那个服务由「插件管理器」这个**别的插件**提供。没装它的 profile 上,
|
|
3549
|
+
* 读 `ctx.remote` 会直接抛错(DSH 的依赖注入对未声明服务就这个行为),
|
|
3550
|
+
* 异常冒出 onClick 没人接 —— 界面表现就是「点了完全没反应」。
|
|
3551
|
+
* 2. 它跑裸 `pnpm add pkg@版本`,会被 pnpm 11 的 24 小时发布年龄策略拦下
|
|
3552
|
+
* (报 NO_MATURE_MATCHING_VERSION),新版本刚发布时装不上。
|
|
3553
|
+
*
|
|
3554
|
+
* 所以更新必须由插件自己完成:host 端先把这个精确版本写进 profile 的
|
|
3555
|
+
* `minimumReleaseAgeExclude` 放行,再用精确版本安装。
|
|
3556
|
+
*
|
|
3557
|
+
* **装完要重启 DSH,光刷新页面不生效**:插件包元数据(含 client.js 的 revision)
|
|
3558
|
+
* 在 DSH 里「缓存至重启」(见 dsh-client-modules 的说明),刷新只会重新拉同一份
|
|
3559
|
+
* 旧 bundle。官方插件管理器也是这个口径(「已安装,下次启动后加载」)。
|
|
3560
|
+
* 按钮文案据此提示重启,而不是刷新。
|
|
3448
3561
|
*/
|
|
3449
3562
|
function startPluginUpdate() {
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
/* 官方桌面端一定带插件管理器;真取不到就按失败处理,不另开一个用户看不懂的分支 */
|
|
3453
|
-
if (manager === null || manager === undefined || typeof manager.installBundle !== "function") {
|
|
3454
|
-
setUpdateState({ busy: false, note: "failed", reason: null });
|
|
3455
|
-
return;
|
|
3456
|
-
}
|
|
3563
|
+
/* 开发模式下只演示:不真实安装(见 runDevPreview) */
|
|
3564
|
+
if (devMode === true) { runDevPreview(); return; }
|
|
3457
3565
|
if (updateState.latest === null || updateState.busy === true) return;
|
|
3458
3566
|
const target = updateState.latest;
|
|
3459
3567
|
setUpdateState({ busy: true, note: null, reason: null });
|
|
3460
|
-
|
|
3461
|
-
"
|
|
3462
|
-
{
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3568
|
+
window.fetch(SELF_UPDATE_URL, {
|
|
3569
|
+
method: "POST",
|
|
3570
|
+
headers: { "content-type": "application/json" },
|
|
3571
|
+
body: JSON.stringify({ version: target })
|
|
3572
|
+
}).then((response) => (response.ok === true ? response.json() : null))
|
|
3573
|
+
.then((payload) => {
|
|
3574
|
+
/* 响应都解不出来(服务端崩了、被代理拦了):按失败处理并说清楚 */
|
|
3575
|
+
if (payload === null || payload === undefined) {
|
|
3576
|
+
setUpdateState({ busy: false, note: "failed", reason: "no-response" });
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
const ok = payload.ok === true;
|
|
3580
|
+
setUpdateState({
|
|
3581
|
+
busy: false,
|
|
3582
|
+
note: ok ? "done" : "failed",
|
|
3583
|
+
reason: ok ? null : reasonOf(payload)
|
|
3584
|
+
});
|
|
3585
|
+
})
|
|
3586
|
+
.catch((error) => {
|
|
3587
|
+
setUpdateState({ busy: false, note: "failed", reason: "network" });
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
/**
|
|
3592
|
+
* 开发模式下的一次点击:纯本地状态机,走完「更新中 → 已更新 → 更新失败 → 空闲」。
|
|
3593
|
+
*
|
|
3594
|
+
* 用真的 setUpdateState / 真的渲染分支,所以看到的动效、文案、按钮禁用行为
|
|
3595
|
+
* 与真实更新**完全一致**;唯一的区别是不发请求、不安装。
|
|
3596
|
+
*/
|
|
3597
|
+
function runDevPreview() {
|
|
3598
|
+
const phase = updateState.note;
|
|
3599
|
+
if (updateState.busy === true) return;
|
|
3600
|
+
if (phase === null) {
|
|
3601
|
+
/* ① 更新中:亮起那三个点 */
|
|
3602
|
+
setUpdateState({ busy: true, note: null, reason: null, latest: DEV_VERSION });
|
|
3603
|
+
devTimer = setTimeout(() => {
|
|
3604
|
+
devTimer = null;
|
|
3605
|
+
/* ② 已更新 */
|
|
3606
|
+
setUpdateState({ busy: false, note: "done", reason: null });
|
|
3607
|
+
}, DEV_BUSY_MS);
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
if (phase === "done") {
|
|
3611
|
+
/* ③ 更新失败:走真实失败那条显示路径 */
|
|
3612
|
+
setUpdateState({ busy: false, note: "failed", reason: DEV_FAIL_REASON });
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
/* ④ 回到空闲,可以再点一遍 */
|
|
3616
|
+
setUpdateState({ busy: false, note: null, reason: null, latest: DEV_VERSION });
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
/** 开发模式里「更新中」那一步的定时器;退出开发模式或卸载时要清掉。 */
|
|
3620
|
+
let devTimer = null;
|
|
3621
|
+
|
|
3622
|
+
/** 开发模式当前是否开着(给测试与自检读)。 */
|
|
3623
|
+
function isDevMode() {
|
|
3624
|
+
return devMode;
|
|
3469
3625
|
}
|
|
3470
3626
|
|
|
3627
|
+
/**
|
|
3628
|
+
* 开关开发模式。
|
|
3629
|
+
* 打开时把状态摆成「有新版本可更新」的样子(但版本号是假的一眼可辨),
|
|
3630
|
+
* 关闭时清掉定时器并恢复成「没有更新」。
|
|
3631
|
+
*/
|
|
3632
|
+
function setDevMode(on) {
|
|
3633
|
+
devMode = on === true;
|
|
3634
|
+
if (devTimer !== null) { clearTimeout(devTimer); devTimer = null; }
|
|
3635
|
+
if (devMode === true) {
|
|
3636
|
+
setUpdateState({ hasUpdate: true, latest: DEV_VERSION, busy: false, note: null, reason: null });
|
|
3637
|
+
} else {
|
|
3638
|
+
/* 关掉时**同步**把演示相关状态一次清干净:忙碌态、结论、演示版本、按钮可见性。
|
|
3639
|
+
这几项都先落到「没有更新」,再去问真实的 registry 结果 —— 反过来(先等网络)
|
|
3640
|
+
会让按钮在关掉之后还多显示一会儿,看起来像没关掉。
|
|
3641
|
+
清 busy 尤其必要:演示可能正停在「更新中」,只撤按钮不撤 busy,
|
|
3642
|
+
界面会留在一个永远转下去的「正在更新中」。 */
|
|
3643
|
+
setUpdateState({ hasUpdate: false, latest: null, busy: false, note: null, reason: null });
|
|
3644
|
+
loadUpdateFlag().then((result) => {
|
|
3645
|
+
if (result !== null) setUpdateState(result);
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
/**
|
|
3651
|
+
* 版本号被点了一下:连点 DEV_CLICKS 次进入(或退出)开发模式。
|
|
3652
|
+
*
|
|
3653
|
+
* 为什么用「连点」而不是一个可见开关:普通用户装了这插件,界面上不该多出
|
|
3654
|
+
* 一个面向作者的调试项。连点是常见的开发者模式手势,记得住、不会被误触。
|
|
3655
|
+
* 两次点击间隔超过 DEV_CLICK_WINDOW_MS 就重新计数 —— 否则慢慢点 5 次也会进去。
|
|
3656
|
+
*
|
|
3657
|
+
* @returns {object} 供测试断言的计数快照
|
|
3658
|
+
*/
|
|
3659
|
+
function onVersionClick() {
|
|
3660
|
+
const now = Date.now();
|
|
3661
|
+
if (devClickAt !== 0 && now - devClickAt > DEV_CLICK_WINDOW_MS) devClickCount = 0;
|
|
3662
|
+
devClickAt = now;
|
|
3663
|
+
devClickCount += 1;
|
|
3664
|
+
if (devClickCount >= DEV_CLICKS) {
|
|
3665
|
+
devClickCount = 0;
|
|
3666
|
+
devClickAt = 0;
|
|
3667
|
+
setDevMode(devMode !== true);
|
|
3668
|
+
}
|
|
3669
|
+
return { count: devClickCount, on: devMode };
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
/** 连点计数与最近一次点击时刻。 */
|
|
3673
|
+
let devClickCount = 0;
|
|
3674
|
+
let devClickAt = 0;
|
|
3675
|
+
|
|
3471
3676
|
/**
|
|
3472
3677
|
* 把设置导航里本插件那一项的前导图标换成 gauge。
|
|
3473
3678
|
* 定位方式是按导航文案匹配(DSH 没给导航项留 id 属性),匹配不到就什么都不做。
|
|
@@ -3505,10 +3710,13 @@ window.__ModuleLoader__.load({
|
|
|
3505
3710
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
3506
3711
|
/* 版本状态一变就重挂一次胶囊:查完发现没新版(撤掉)、或有新版(挂上) */
|
|
3507
3712
|
const stopWatch = subscribeUpdate(apply);
|
|
3508
|
-
/* 启动自检一次。卸载之后结果才到的话直接丢弃,别再往 DOM 里塞东西。
|
|
3713
|
+
/* 启动自检一次。卸载之后结果才到的话直接丢弃,别再往 DOM 里塞东西。
|
|
3714
|
+
开发模式期间也要丢弃:它是模块级开关,用户可能在这次网络请求返回前就连点
|
|
3715
|
+
进去了,此时真实结果会把演示状态冲掉 —— 胶囊还在、按钮却没了。 */
|
|
3509
3716
|
let alive = true;
|
|
3510
3717
|
loadUpdateFlag().then((result) => {
|
|
3511
3718
|
if (alive !== true || result === null) return;
|
|
3719
|
+
if (devMode === true) return;
|
|
3512
3720
|
setUpdateState(result);
|
|
3513
3721
|
});
|
|
3514
3722
|
return () => {
|
|
@@ -3546,7 +3754,6 @@ window.__ModuleLoader__.load({
|
|
|
3546
3754
|
const t = props.t;
|
|
3547
3755
|
const useProjection = props.useProjection;
|
|
3548
3756
|
const useSession = props.useSession;
|
|
3549
|
-
const useSessions = props.useSessions;
|
|
3550
3757
|
const sessionId = props.sessionId;
|
|
3551
3758
|
const cfg = useConfig();
|
|
3552
3759
|
|
|
@@ -3596,7 +3803,20 @@ window.__ModuleLoader__.load({
|
|
|
3596
3803
|
method: "POST",
|
|
3597
3804
|
headers: { "content-type": "application/json" },
|
|
3598
3805
|
body: JSON.stringify({ models: models })
|
|
3599
|
-
}).
|
|
3806
|
+
}).then((response) => {
|
|
3807
|
+
/* 失败要说出来。原先只有 `.catch(() => {})`,而 fetch 对 4xx/5xx 是
|
|
3808
|
+
**resolve** 不是 reject —— 上报被拒(如体积超限返回 400)时既不进
|
|
3809
|
+
catch 也没人看 response,于是用户的价目表一直送不到 host。
|
|
3810
|
+
后果分两种:**新产生**的用量会按内置官方价计费(不是用户填的价);
|
|
3811
|
+
只在自定义表里的模型则永远算不出钱、一直留在欠费明细里。
|
|
3812
|
+
已算好的费用不受影响,也不会被改写 —— 那是既定语义。
|
|
3813
|
+
底栏本身不受影响,所以只告警、不改界面。 */
|
|
3814
|
+
if (response.ok !== true) {
|
|
3815
|
+
console.warn("[dsh-desktop-statusbar] 价格库上报被拒 HTTP " + String(response.status) + ",host 会继续用内置参考价给新用量记账");
|
|
3816
|
+
}
|
|
3817
|
+
}).catch((error) => {
|
|
3818
|
+
console.warn("[dsh-desktop-statusbar] 价格库上报失败:" + String(error && error.message));
|
|
3819
|
+
});
|
|
3600
3820
|
}, [reportedPrices]);
|
|
3601
3821
|
|
|
3602
3822
|
const running = pick(useSession, (s) => s.running, undefined);
|
|
@@ -3618,7 +3838,7 @@ window.__ModuleLoader__.load({
|
|
|
3618
3838
|
/* 「本轮」显示值的粘性下限盒子(同轮内只增不减,换轮作废):一个可变盒子,
|
|
3619
3839
|
由 lastCost 段在渲染时更新,跨渲染靠这个 ref 留住(见 applyTurnFloor)。 */
|
|
3620
3840
|
const lastCostHoldRef = react.useRef({ sessionId: null, turnKey: null, value: 0 });
|
|
3621
|
-
/* 点开「总计」气泡时 +1:强制重拉一次队友用量(别的触发条件都带
|
|
3841
|
+
/* 点开「总计」气泡时 +1:强制重拉一次队友用量(别的触发条件都带 SUBAGENT_REFRESH_MS 节流) */
|
|
3622
3842
|
const [subagentTick, setSubagentTick] = react.useState(0);
|
|
3623
3843
|
const [tipId, setTipId] = react.useState(null);
|
|
3624
3844
|
const [tipPos, setTipPos] = react.useState(null);
|
|
@@ -4141,7 +4361,8 @@ window.__ModuleLoader__.load({
|
|
|
4141
4361
|
/* 点开余额 / 峰谷用量:顺手重查一次(收起那一下不查),数据回来气泡自己更新 */
|
|
4142
4362
|
if (id === "balance" && balanceReloadRef.current !== null) balanceReloadRef.current();
|
|
4143
4363
|
if (id === "status" && dailyReloadRef.current !== null) dailyReloadRef.current();
|
|
4144
|
-
/* 总计:队友的数字也要重取一次 ——
|
|
4364
|
+
/* 总计:队友的数字也要重取一次 —— 节流靠的是那条每秒时钟(SUBAGENT_REFRESH_MS),
|
|
4365
|
+
而会话完全空闲时
|
|
4145
4366
|
它不跳(睡到下一个峰谷边界),光靠它会让数字停在旧值 */
|
|
4146
4367
|
if (id === "cost" && subagentReloadRef.current !== null) subagentReloadRef.current();
|
|
4147
4368
|
const target = event !== undefined && event !== null ? event.currentTarget : null;
|
|
@@ -4259,6 +4480,26 @@ window.__ModuleLoader__.load({
|
|
|
4259
4480
|
const t = props.t;
|
|
4260
4481
|
const cfg = useConfig();
|
|
4261
4482
|
const update = useUpdateState();
|
|
4483
|
+
/* 开发模式是本组件内的界面状态;它不写配置、不动真实更新。
|
|
4484
|
+
**不给它可见开关**:入口是页脚版本号的「连点 5 次」(见 onVersionClick)。
|
|
4485
|
+
进入后标题右侧挂一枚「开发模式」胶囊,这是唯一的可见迹象。 */
|
|
4486
|
+
const [devOn, setDevOn] = react.useState(false);
|
|
4487
|
+
/* 版本号被点击:累计连点次数,够 5 次就切换开发模式。
|
|
4488
|
+
计数与时间窗在模块级(onVersionClick),这里只负责把结果同步给 React。 */
|
|
4489
|
+
const onVersionClicked = () => {
|
|
4490
|
+
const state = onVersionClick();
|
|
4491
|
+
setDevOn(state.on === true);
|
|
4492
|
+
};
|
|
4493
|
+
/* 卸载时清掉演示的定时器,并把开发模式**一并关掉**。
|
|
4494
|
+
只清定时器是不够的:devMode 是模块级变量,而 devOn 是本组件的 useState。
|
|
4495
|
+
卸载后重挂时 devOn 回到 false,devMode 若还是 true,两者就不同步了 ——
|
|
4496
|
+
界面看不见「开发模式」胶囊,更新按钮却仍显示演示版本号,点下去走
|
|
4497
|
+
startPluginUpdate 的 devMode 分支只播演示、不安装也不报错,用户无从判断。
|
|
4498
|
+
只在**真的开着**时才复位:setDevMode(false) 会顺手重查一次 registry,
|
|
4499
|
+
没开还调用等于每次卸载白打一次网络请求,并且会把已有的真实更新提示清掉。 */
|
|
4500
|
+
react.useEffect(() => () => {
|
|
4501
|
+
if (devMode === true) setDevMode(false);
|
|
4502
|
+
}, []);
|
|
4262
4503
|
|
|
4263
4504
|
/* 自绘拖拽:全程用 pointer 事件自己跟手。不用原生 HTML5 拖拽 ——
|
|
4264
4505
|
原生拖拽会话期间浏览器不给页面 wheel 事件(滚轮失灵),而且只有松手才换位。 */
|
|
@@ -4566,7 +4807,15 @@ window.__ModuleLoader__.load({
|
|
|
4566
4807
|
const sections = [];
|
|
4567
4808
|
|
|
4568
4809
|
sections.push(h("div", { className: "dsb-head", key: "__head" }, [
|
|
4569
|
-
|
|
4810
|
+
/* 标题那一行:开发模式开着时,标题文字右上角跟一枚胶囊 —— 像 x² 的上标那样
|
|
4811
|
+
紧贴着标题,而不是甩到面板最右边(那样看不出是「这个标题的」标记)。
|
|
4812
|
+
关着时这一行与原来**完全一样** —— 不多任何元素。 */
|
|
4813
|
+
h("div", { className: "dsb-titlerow", key: "__titlerow" },
|
|
4814
|
+
h("h2", { className: "dsb-pagetitle" }, t("sectionTitle")),
|
|
4815
|
+
devOn === true
|
|
4816
|
+
? h("span", { className: "dsb-nav-pill dsb-dev-pill", key: "__devpill", title: t("devModeHint") }, t("devModeBadge"))
|
|
4817
|
+
: null
|
|
4818
|
+
),
|
|
4570
4819
|
h("p", { className: "dsb-intro" }, t("intro"))
|
|
4571
4820
|
]));
|
|
4572
4821
|
|
|
@@ -4994,9 +5243,17 @@ window.__ModuleLoader__.load({
|
|
|
4994
5243
|
type: "button",
|
|
4995
5244
|
className: "dsb-action",
|
|
4996
5245
|
key: "__star",
|
|
4997
|
-
/*
|
|
5246
|
+
/* 点完**主动松开焦点**再跳转。气泡的显示条件是 `:hover` 或 `:focus-within`;
|
|
5247
|
+
点击会让按钮获得 DOM 焦点,而跳去浏览器只让**窗口**失焦、DOM 焦点还留在按钮上 ——
|
|
5248
|
+
于是 `:focus-within` 一直为真,用户关掉浏览器回来时气泡就卡住常显(实测缺陷)。
|
|
5249
|
+
blur 掉之后气泡回到只跟鼠标悬停走,行为才符合预期。
|
|
5250
|
+
window.open 会被主窗口的 setWindowOpenHandler 接住,转 shell.openExternal → 默认浏览器;
|
|
4998
5251
|
noopener 只是不给新窗口留 window.opener(这里窗口本来就被拒了) */
|
|
4999
|
-
onClick: () =>
|
|
5252
|
+
onClick: (event) => {
|
|
5253
|
+
const button = event === null || event === undefined ? null : event.currentTarget;
|
|
5254
|
+
if (button !== null && button !== undefined && typeof button.blur === "function") button.blur();
|
|
5255
|
+
window.open(REPO_URL, "_blank", "noopener");
|
|
5256
|
+
}
|
|
5000
5257
|
}, t("starRepo"))
|
|
5001
5258
|
]),
|
|
5002
5259
|
h("button", {
|
|
@@ -5026,10 +5283,20 @@ window.__ModuleLoader__.load({
|
|
|
5026
5283
|
});
|
|
5027
5284
|
}
|
|
5028
5285
|
}, t("reset")),
|
|
5029
|
-
/* 有新版才出现:与导航胶囊同一个来源(updateState
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5286
|
+
/* 有新版才出现:与导航胶囊同一个来源(updateState)。
|
|
5287
|
+
点它由本插件自己的 host 端执行更新(不依赖插件管理器,也不吃 pnpm 的
|
|
5288
|
+
24 小时发布年龄限制)。三种状态都写在按钮自己身上:空闲报版本号、
|
|
5289
|
+
更新中带动效点、失败就说「更新失败」。
|
|
5290
|
+
|
|
5291
|
+
**装完必须重启 DSH,刷新页面没有用** —— 这不是保守说法,是 DSH 的设计:
|
|
5292
|
+
· `@deepseek-ai/dsh-hmr` 的已知限制原文:「通过插件管理器替换已安装包版本
|
|
5293
|
+
仍需要重启。浏览器 Client 模块图保留独立的浏览器侧加载机制。」
|
|
5294
|
+
· `dsh-client-modules` 的包元数据(含 client.js 的 revision)
|
|
5295
|
+
「缓存至重启」,所以刷新只会重新拉同一份旧 bundle。
|
|
5296
|
+
· 官方插件管理器的文案也是同一口径:「已安装,下次启动后加载。」
|
|
5297
|
+
桌面宿主只提供退出能力、没有自我重启能力(`app.relaunch()` 只在开发模式
|
|
5298
|
+
的菜单里),所以这里只如实提示,不假装能替用户重启。 */
|
|
5299
|
+
update.hasUpdate === true
|
|
5033
5300
|
? h("button", {
|
|
5034
5301
|
type: "button",
|
|
5035
5302
|
className: "dsb-action",
|
|
@@ -5042,12 +5309,25 @@ window.__ModuleLoader__.load({
|
|
|
5042
5309
|
h("span", { key: "__d2" }, "."),
|
|
5043
5310
|
h("span", { key: "__d3" }, ".")
|
|
5044
5311
|
])]
|
|
5045
|
-
: (update.note === "
|
|
5046
|
-
? t("
|
|
5047
|
-
:
|
|
5312
|
+
: (update.note === "done"
|
|
5313
|
+
? t("updateDone")
|
|
5314
|
+
: (update.note === "failed"
|
|
5315
|
+
? t("updateFailed")
|
|
5316
|
+
: t("updateTo", { version: update.latest === null ? "" : update.latest }))))
|
|
5048
5317
|
: null,
|
|
5049
|
-
/*
|
|
5050
|
-
|
|
5318
|
+
/* 版本标记贴右、与行内底边对齐:这行字让「现在跑的是哪一版」一眼可见 ——
|
|
5319
|
+
注意**光刷新页面看不出差别**:插件包元数据缓存至重启,刷新拿到的还是旧 bundle;
|
|
5320
|
+
要换版本必须重启 DSH,重启后这行字才会变成新号。
|
|
5321
|
+
|
|
5322
|
+
它同时是**开发模式的隐藏入口**:连点 5 次进入 / 退出(见 onVersionClick)。
|
|
5323
|
+
挂在版本号上的原因:这个入口面向作者调试,普通用户的界面里不该多出
|
|
5324
|
+
任何可见元素;连点是常见手势,记得住又不会被误触。
|
|
5325
|
+
进入后的可见迹象是标题右侧那枚「开发模式」胶囊,不在这一行。 */
|
|
5326
|
+
h("span", {
|
|
5327
|
+
className: "dsb-version",
|
|
5328
|
+
key: "__version",
|
|
5329
|
+
onClick: onVersionClicked
|
|
5330
|
+
}, "dsh-desktop-statusbar " + VERSION)
|
|
5051
5331
|
]));
|
|
5052
5332
|
|
|
5053
5333
|
return h("div", { className: "dsb-settings" }, sections);
|
|
@@ -5062,12 +5342,23 @@ window.__ModuleLoader__.load({
|
|
|
5062
5342
|
exports.__test = {
|
|
5063
5343
|
nextSubagentState: nextSubagentState,
|
|
5064
5344
|
applyTurnFloor: applyTurnFloor,
|
|
5065
|
-
SUBAGENT_STATE_EMPTY: SUBAGENT_STATE_EMPTY
|
|
5345
|
+
SUBAGENT_STATE_EMPTY: SUBAGENT_STATE_EMPTY,
|
|
5346
|
+
/* 开发模式的驱动入口:测试要能直接触发连点、并读回状态,
|
|
5347
|
+
不然这段只存在于设置页组件里的逻辑没法离线验证。 */
|
|
5348
|
+
setDevMode: setDevMode,
|
|
5349
|
+
runDevPreview: runDevPreview,
|
|
5350
|
+
isDevMode: isDevMode,
|
|
5351
|
+
onVersionClick: onVersionClick,
|
|
5352
|
+
DEV_VERSION: DEV_VERSION,
|
|
5353
|
+
DEV_FAIL_REASON: DEV_FAIL_REASON,
|
|
5354
|
+
DEV_CLICKS: DEV_CLICKS,
|
|
5355
|
+
DEV_CLICK_WINDOW_MS: DEV_CLICK_WINDOW_MS
|
|
5066
5356
|
};
|
|
5067
5357
|
|
|
5068
5358
|
function apply(ctx) {
|
|
5069
|
-
/*
|
|
5070
|
-
|
|
5359
|
+
/* 注意:这里**不保存 ctx**。更新走本插件自己的 host 路由
|
|
5360
|
+
(见 startPluginUpdate 的说明),界面其余部分也不需要宿主上下文 ——
|
|
5361
|
+
曾经存过一份 `pluginCtx` 只写不读,已删除。 */
|
|
5071
5362
|
ctx.effect(installStyles, "dsh-desktop-statusbar: styles");
|
|
5072
5363
|
ctx.effect(() => ctx.locale.register(NS, { zh: zh, en: en }), "dsh-desktop-statusbar: locale");
|
|
5073
5364
|
ctx.effect(() => installNavIcon(
|