@iasiv5/dsh-quota-watch 0.1.17 → 0.1.19

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.md CHANGED
@@ -23,7 +23,7 @@
23
23
  - **🧩 侧边栏卡片(可选,默认隐藏)**:胶囊是默认界面;需要侧栏卡片时从面板底部按钮开启(Web profile 侧栏底部区块存在时才出现该按钮)。卡片提供摘要行 + 行点击开面板 + 行首 provider 名称点击立即探测。侧栏收起(Web 窄栏)或容器过窄(<160px,跨 profile 通用判据)时自动隐藏,侧栏整体不存在(Desktop 收起态)同样自然缺席、无报错——入口始终由悬浮胶囊承载。
24
24
  - **🌏 语言跟随宿主**:界面文案跟随 DSH「设置 → 语言」——宿主 locale 特性把解析结果实时镜像到 `<html lang>`,插件监听该属性,切换即热重渲染(无需刷新页面);宿主未同步时回退浏览器语言。
25
25
  - **🛟 降级与容错**:凭据缺失的 provider 不渲染摘要行与胶囊分段(两者皆缺时悬浮胶囊与卡片一并隐藏);探测失败时保留最后一次成功数据并在行上加 ⚠ 过期标记(⚠ 只出现在面板与卡片行内,不在胶囊上显示)。
26
- - **🔁 刷新节奏**:宿主默认每 60 秒探测(窗口 + 用量 + Copilot),profile 可配 30–3600 秒;浏览器页签可见时每 30 秒刷新视图,隐藏时暂停。所有 UI 收在 Shadow DOM 里(ADR 0001),颜色走 DSH 主题语义变量(含硬编码兜底),自动适配浅色 / 深色主题。
26
+ - **🔁 刷新节奏**:宿主默认每 60 秒探测(窗口 + 用量 + Copilot),profile 可配 30–3600 秒;浏览器页签可见时每 30 秒刷新视图,隐藏时暂停。重启冷启动零等待:宿主 0 秒即发首轮探测(网络未就绪时以 5 秒冷重试直至拿到数据),客户端在无数据期间以 2 秒起步的指数退避快速重试、拿到数据后回到常规节奏——重启后数秒内胶囊即出现,不再有 ~30 秒空窗。所有 UI 收在 Shadow DOM 里(ADR 0001),颜色走 DSH 主题语义变量(含硬编码兜底),自动适配浅色 / 深色主题。
27
27
 
28
28
  ## 📸 界面巡礼
29
29
 
@@ -39,20 +39,16 @@ GLM 详情(双大数字 + 窗口网格 + 分模型用量)与 Copilot 详情
39
39
 
40
40
  ## 🚀 安装
41
41
 
42
- 安装到 `web` profile:
42
+ **方式一(推荐)· DSH 插件市场(dsh-m)**:打开 DSH 侧栏的「插件市场」,搜索 **Quota Watch**(或「额度」)一键安装——市场锁定精确版本、自动处理依赖,后续升级也在市场里完成。
43
43
 
44
- ```sh
45
- dsh plugin --profile web add @iasiv5/dsh-quota-watch
46
- ```
47
-
48
- 更新与卸载:
44
+ **方式二 · 命令行**(无市场环境或脚本化装机):
49
45
 
50
46
  ```sh
51
- dsh plugin --profile web update @iasiv5/dsh-quota-watch # 更新
52
- dsh plugin --profile web remove @iasiv5/dsh-quota-watch # 卸载
47
+ dsh plugin --profile web add @iasiv5/dsh-quota-watch # web profile
48
+ dsh plugin --profile desktop add @iasiv5/dsh-quota-watch # desktop profile
53
49
  ```
54
50
 
55
- 插件管理器会应用包内 `cordis.patch.yml`。宿主半区的插件变更需要重启 `dsh web` 后生效;客户端 bundle 随插件加载。装好后刷新页面,右下角即是额度胶囊。禁用插件请使用 DSH 插件管理器。
51
+ **web 与 desktop 两种 profile 均已适配**:悬浮胶囊在两端都是常驻入口;侧边栏卡片是渐进增强——只要侧栏底部区块存在即可从面板按钮开启,侧栏收起或不存在(Desktop 收起态)时自动缺席。插件管理器会应用包内 `cordis.patch.yml`;宿主半区的变更需要重启 DSH 后生效(web 重启 `dsh web` 服务,desktop 重启应用),客户端 bundle 随插件加载,装好后刷新页面,右下角即是额度胶囊。npm 发布新版本后,市场 / 镜像同步可能略有延迟,立即装不到最新版时稍候重试即可。禁用插件请使用 DSH 插件管理器。
56
52
 
57
53
  ## ⚙️ 配置
58
54
 
package/lib/client.js CHANGED
@@ -470,7 +470,9 @@ function mountQuotaCard({
470
470
  doc = globalThis.document,
471
471
  win = globalThis.window,
472
472
  fetchImpl = globalThis.fetch,
473
- pollIntervalMs = CLIENT_POLL_INTERVAL_MS
473
+ pollIntervalMs = CLIENT_POLL_INTERVAL_MS,
474
+ retryNoDataMs = 2e3,
475
+ retryBackoff = 2
474
476
  } = {}) {
475
477
  if (!doc?.body || typeof fetchImpl !== "function") return () => {
476
478
  };
@@ -670,11 +672,12 @@ function mountQuotaCard({
670
672
  let snapshot;
671
673
  let requestSequence = 0;
672
674
  let timer;
675
+ let noDataRetries = 0;
673
676
  let disposed = false;
674
677
  let queuedPlace = false;
675
678
  let openKey;
676
679
  let panelNav = false;
677
- let dataHidden = false;
680
+ let dataHidden = true;
678
681
  let surfaceFlags = loadSurfaceFlags(win.localStorage);
679
682
  const setCardHidden = (hidden) => {
680
683
  surfaceFlags = { cardHidden: hidden };
@@ -1059,6 +1062,13 @@ function mountQuotaCard({
1059
1062
  if (sequence === requestSequence && !disposed) render(next);
1060
1063
  } catch {
1061
1064
  if (sequence === requestSequence && !disposed) showTransportError();
1065
+ } finally {
1066
+ if (timer === void 0 && sequence === requestSequence && !disposed && doc.visibilityState !== "hidden") {
1067
+ timer = win.setTimeout(() => {
1068
+ timer = void 0;
1069
+ if (!disposed && doc.visibilityState !== "hidden") void poll();
1070
+ }, nextPollDelay());
1071
+ }
1062
1072
  }
1063
1073
  };
1064
1074
  const place = () => {
@@ -1089,16 +1099,23 @@ function mountQuotaCard({
1089
1099
  if (!disposed) renderAll();
1090
1100
  }) : null;
1091
1101
  langObserver?.observe(doc.documentElement, { attributeFilter: ["lang"] });
1102
+ syncFloatVisibility();
1092
1103
  place();
1104
+ const nextPollDelay = () => {
1105
+ if (!dataHidden) {
1106
+ noDataRetries = 0;
1107
+ return pollIntervalMs;
1108
+ }
1109
+ const delay = retryNoDataMs * retryBackoff ** noDataRetries;
1110
+ noDataRetries += 1;
1111
+ return Math.min(delay, pollIntervalMs);
1112
+ };
1093
1113
  const startPolling = () => {
1094
1114
  if (timer !== void 0 || doc.visibilityState === "hidden" || disposed) return;
1095
1115
  void poll();
1096
- timer = win.setInterval(() => {
1097
- void poll();
1098
- }, pollIntervalMs);
1099
1116
  };
1100
1117
  const stopPolling = () => {
1101
- if (timer !== void 0) win.clearInterval(timer);
1118
+ if (timer !== void 0) win.clearTimeout(timer);
1102
1119
  timer = void 0;
1103
1120
  };
1104
1121
  const onVisibilityChange = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "DSH Web sidebar monitor for GLM Coding Plan quotas and GitHub Copilot credits",
5
5
  "type": "module",
6
6
  "main": "./src/host.mjs",
package/src/client.mjs CHANGED
@@ -373,6 +373,8 @@ export function mountQuotaCard({
373
373
  win = globalThis.window,
374
374
  fetchImpl = globalThis.fetch,
375
375
  pollIntervalMs = CLIENT_POLL_INTERVAL_MS,
376
+ retryNoDataMs = 2_000,
377
+ retryBackoff = 2,
376
378
  } = {}) {
377
379
  if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
378
380
  if (doc.querySelector(CARD_SELECTOR) || doc.querySelector(FLOAT_SELECTOR)) return () => {}
@@ -610,11 +612,15 @@ export function mountQuotaCard({
610
612
  let snapshot
611
613
  let requestSequence = 0
612
614
  let timer
615
+ let noDataRetries = 0
613
616
  let disposed = false
614
617
  let queuedPlace = false
615
618
  let openKey
616
619
  let panelNav = false
617
- let dataHidden = false
620
+ // Cold until the first successful render: a mount whose first poll fails or
621
+ // stalls must behave like the documented no-data state (hidden shell, fast
622
+ // retries), not like a warm client showing an empty capsule.
623
+ let dataHidden = true
618
624
  let surfaceFlags = loadSurfaceFlags(win.localStorage)
619
625
  const setCardHidden = (hidden) => {
620
626
  surfaceFlags = { cardHidden: hidden }
@@ -1052,6 +1058,18 @@ export function mountQuotaCard({
1052
1058
  if (sequence === requestSequence && !disposed) render(next)
1053
1059
  } catch {
1054
1060
  if (sequence === requestSequence && !disposed) showTransportError()
1061
+ } finally {
1062
+ // Self-scheduling chain (replaces the fixed interval): every completed
1063
+ // poll — success OR transport error — arms the next one, so the no-data
1064
+ // backoff also covers a cold start whose first request fails outright.
1065
+ // timer === undefined guards against a concurrent force poll forking a
1066
+ // second chain; the already-armed timer keeps the cadence.
1067
+ if (timer === undefined && sequence === requestSequence && !disposed && doc.visibilityState !== 'hidden') {
1068
+ timer = win.setTimeout(() => {
1069
+ timer = undefined
1070
+ if (!disposed && doc.visibilityState !== 'hidden') void poll()
1071
+ }, nextPollDelay())
1072
+ }
1055
1073
  }
1056
1074
  }
1057
1075
 
@@ -1091,15 +1109,33 @@ export function mountQuotaCard({
1091
1109
  ? new win.MutationObserver(() => { if (!disposed) renderAll() })
1092
1110
  : null
1093
1111
  langObserver?.observe(doc.documentElement, { attributeFilter: ['lang'] })
1112
+ // Cold until data lands: keep the shell hidden so a slow or failed first
1113
+ // probe never flashes an empty capsule (startPolling arms the retry chain).
1114
+ syncFloatVisibility()
1094
1115
  place()
1095
1116
 
1117
+ /** Cold-start cadence: while no provider has rendered any data, re-poll on
1118
+ * a shortening backoff (retryNoDataMs × retryBackoffⁿ, capped at
1119
+ * pollIntervalMs) so a desktop cold start surfaces the capsule in seconds
1120
+ * instead of waiting out the fixed interval. A successful render resets the
1121
+ * chain to the normal cadence; a transport error keeps the previous
1122
+ * dataHidden state, so a cold client keeps retrying fast and a warm one
1123
+ * stays calm. */
1124
+ const nextPollDelay = () => {
1125
+ if (!dataHidden) {
1126
+ noDataRetries = 0
1127
+ return pollIntervalMs
1128
+ }
1129
+ const delay = retryNoDataMs * (retryBackoff ** noDataRetries)
1130
+ noDataRetries += 1
1131
+ return Math.min(delay, pollIntervalMs)
1132
+ }
1096
1133
  const startPolling = () => {
1097
1134
  if (timer !== undefined || doc.visibilityState === 'hidden' || disposed) return
1098
1135
  void poll()
1099
- timer = win.setInterval(() => { void poll() }, pollIntervalMs)
1100
1136
  }
1101
1137
  const stopPolling = () => {
1102
- if (timer !== undefined) win.clearInterval(timer)
1138
+ if (timer !== undefined) win.clearTimeout(timer)
1103
1139
  timer = undefined
1104
1140
  }
1105
1141
  const onVisibilityChange = () => {
package/src/host.mjs CHANGED
@@ -311,7 +311,18 @@ export function createQuotaService(ctx, options = {}) {
311
311
  timer = setTimeout(async () => {
312
312
  timer = undefined
313
313
  await refresh()
314
- schedule(pollIntervalSec * 1000)
314
+ // Cold-start retry: until at least one provider has reached ready/stale,
315
+ // a failed or still-loading round re-arms shortly instead of waiting a
316
+ // full interval — a desktop cold start with a slow network would
317
+ // otherwise stare at no capsule for up to a minute. (updatedAt alone
318
+ // cannot signal "cold": refresh() stamps it even when every probe
319
+ // failed.) All-'missing' (no credentials configured) and warm states
320
+ // keep the normal cadence, so nobody without credentials gets
321
+ // high-frequency probing.
322
+ const states = [...providers.values()].map((provider) => provider.status)
323
+ const retryable = states.some((status) => status === 'loading' || status === 'error')
324
+ && states.every((status) => status !== 'ready' && status !== 'stale')
325
+ schedule(retryable ? (options.coldRetryMs ?? 5_000) : pollIntervalSec * 1000)
315
326
  }, delayMs)
316
327
  timer.unref?.()
317
328
  }
@@ -321,7 +332,9 @@ export function createQuotaService(ctx, options = {}) {
321
332
  refresh,
322
333
  start() {
323
334
  if (disposed || timer !== undefined) return
324
- schedule(2_000)
335
+ // Routes register synchronously in apply() before start(), so probing
336
+ // immediately is safe; the legacy 2s delay only padded the cold start.
337
+ schedule(0)
325
338
  },
326
339
  stop() {
327
340
  disposed = true