@iasiv5/dsh-quota-watch 0.1.26 → 0.1.27

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
@@ -58,7 +58,7 @@ Web 与 Desktop profile 均已实测适配。按 DSH 插件管理器提示完成
58
58
 
59
59
  > **重置卡是可消耗、不可撤销的订阅权益。** 使用前请确认适用范围、卡种与额度窗口。
60
60
 
61
- 仅支持配置为 `zai-coding-cn` 的**国内站个人版**。国际站 `zai` / `zai-coding` 不提供此入口;团队版未验证、不在支持范围内。重置卡列表按卡种显示可用张数与最早到期日;「刷新」只查询卡数,不会消费卡。手动使用前,宿主会重新查询当前卡列表,面板也会先显示二次确认。5 小时卡重置 5 小时窗口;每周卡同时重置每周与 5 小时窗口。
61
+ 仅支持配置为 `zai-coding-cn` 的**国内站个人版**。国际站 `zai` / `zai-coding` 不提供此入口;团队版未验证、不在支持范围内。宿主启动时查询重置卡,此后最多每 10 分钟周期查询一次;卡种、可用张数与最早到期日会随结果更新。两种卡都无可用且自动用卡均关闭、无待展示事件时,卡区收成保留分隔线的一行,仍可展开自动用卡设置。概览底部或详情标题栏的「立即刷新」会同时更新额度和重置卡,不会消费卡;手动使用前宿主仍会重新查询当前卡列表,面板也会先显示二次确认。5 小时卡重置 5 小时窗口;每周卡同时重置每周与 5 小时窗口。
62
62
 
63
63
  ![GLM 详情:额度窗口、重置卡与展开的自动用卡设置;两个窗口的消费授权均关闭(示例数据)](docs/assets/panel-glm-detail.webp)
64
64
 
package/lib/client.js CHANGED
@@ -249,7 +249,6 @@ var COPY = {
249
249
  count: "{count} \u5F20",
250
250
  none: "\u65E0\u53EF\u7528",
251
251
  use: "\u91CD\u7F6E",
252
- reloadLink: "\uFF08 \u5237\u65B0 \uFF09",
253
252
  autoToggle: "\u81EA\u52A8\u7528\u5361",
254
253
  autoActive: "\u81EA\u52A8\u7528\u5361 \xB7 \u5DF2\u542F\u7528",
255
254
  expiry: "{date} \u524D\u6709\u6548",
@@ -321,7 +320,6 @@ var COPY = {
321
320
  count: "{count}",
322
321
  none: "none",
323
322
  use: "Reset",
324
- reloadLink: "( Refresh )",
325
323
  autoToggle: "Auto-use",
326
324
  autoActive: "Auto-use \xB7 on",
327
325
  expiry: "expires {date}",
@@ -410,6 +408,8 @@ var FLOAT_STYLE_TEXT = `
410
408
  .dqw-panel-back { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 28px; height: 28px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; }
411
409
  .dqw-panel-back svg { display: block; width: 16px; height: 16px; }
412
410
  .dqw-panel-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
411
+ .dqw-panel-refresh { flex: none; margin: 0; padding: 4px 6px; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; font: inherit; font-size: 10px; line-height: 14px; opacity: .72; }
412
+ .dqw-panel-refresh:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); opacity: 1; }
413
413
  .dqw-panel-close { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 24px; height: 24px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; font: inherit; font-size: 12px; line-height: 1; }
414
414
  .dqw-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
415
415
  .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
@@ -449,18 +449,11 @@ var FLOAT_STYLE_TEXT = `
449
449
  .dqw-win-mid { opacity: .65; white-space: nowrap; }
450
450
  .dqw-win-used { opacity: .65; white-space: nowrap; }
451
451
  .dqw-win-val { min-width: 0; font-weight: 600; }
452
- /* Reset cards: a bordered block below the window grid. Every control is a real
453
- <button> so keyboard focus and the disabled state come from the platform. */
452
+ /* Reset cards: keep the dashed divider even when an empty wallet collapses to
453
+ one row. The panel's \u7ACB\u5373\u5237\u65B0 is the only manual re-check control. */
454
454
  .dqw-reset { border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 4px; }
455
- .dqw-reset-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 10px; line-height: 14px; }
455
+ .dqw-reset-head, .dqw-reset-collapsed { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 10px; line-height: 14px; }
456
456
  .dqw-reset-title { opacity: .65; }
457
- /* No underline: the full-width \u300C\uFF08 \u5237\u65B0 \uFF09\u300D brackets carry the affordance, the
458
- hover opacity bump is the only hover signal. */
459
- .dqw-reset-reload { border: 0; background: none; padding: 0; font: inherit; font-size: 10px; color: var(--dsw-alias-label-secondary, inherit); opacity: .65; cursor: pointer; }
460
- .dqw-reset-reload:hover { opacity: 1; }
461
- .dqw-reset-reload:disabled { cursor: default; opacity: .35; }
462
- .dqw-reset-head-left { display: flex; align-items: baseline; min-width: 0; }
463
- .dqw-reset-head-left.is-en { gap: 4px; }
464
457
  .dqw-auto-master { display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 10px; line-height: 14px; }
465
458
  .dqw-auto-master .dqw-auto-box { accent-color: var(--dsw-alias-button-primary-fill, #5b8def); }
466
459
  .dqw-reset-lines { display: grid; grid-template-columns: max-content max-content minmax(0, 1fr) max-content max-content; column-gap: 6px; row-gap: 4px; align-items: center; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
@@ -871,6 +864,7 @@ function mountQuotaCard({
871
864
  let queuedPlace = false;
872
865
  let openKey;
873
866
  let panelNav = false;
867
+ let knownResetCards;
874
868
  let resetInfo;
875
869
  let resetLoading = false;
876
870
  let resetPending;
@@ -932,7 +926,7 @@ function mountQuotaCard({
932
926
  row.append(text(doc, "span", "dqw-chev", openKey === key ? "\u25BE" : "\u25B8"));
933
927
  return [row];
934
928
  };
935
- const panelHeader = ({ title, showBack = false }) => {
929
+ const panelHeader = ({ title, showBack = false, showRefresh = false }) => {
936
930
  const header = doc.createElement("div");
937
931
  header.className = "dqw-panel-header";
938
932
  if (showBack) {
@@ -946,6 +940,15 @@ function mountQuotaCard({
946
940
  header.append(back);
947
941
  }
948
942
  header.append(text(doc, "p", "dqw-panel-title", title));
943
+ if (showRefresh) {
944
+ const refresh = doc.createElement("button");
945
+ refresh.type = "button";
946
+ refresh.className = "dqw-panel-refresh";
947
+ refresh.dataset.action = "panel-refresh";
948
+ refresh.textContent = copy.actions.refreshNow;
949
+ refresh.setAttribute("aria-label", copy.actions.refreshNow);
950
+ header.append(refresh);
951
+ }
949
952
  const close = doc.createElement("button");
950
953
  close.type = "button";
951
954
  close.className = "dqw-panel-close";
@@ -1007,6 +1010,7 @@ function mountQuotaCard({
1007
1010
  try {
1008
1011
  const payload = await fetchResetEnvelope(fetchImpl, CLIENT_ROUTES.resetCards);
1009
1012
  resetInfo = payload;
1013
+ if (payload.ok === true && payload.cards !== void 0) knownResetCards = payload.cards;
1010
1014
  } catch {
1011
1015
  resetInfo = { ok: false, error: "request-failed" };
1012
1016
  } finally {
@@ -1025,6 +1029,7 @@ function mountQuotaCard({
1025
1029
  if (payload.ok === true) {
1026
1030
  resetNotice = { kind: "ok", message: copy.resetCards.succeeded };
1027
1031
  resetInfo = payload.cards !== void 0 ? { ok: true, cards: payload.cards } : resetInfo;
1032
+ if (payload.cards !== void 0) knownResetCards = payload.cards;
1028
1033
  } else {
1029
1034
  resetNotice = { kind: "error", message: payload.message ?? copy.resetCards.failed };
1030
1035
  }
@@ -1136,51 +1141,60 @@ function mountQuotaCard({
1136
1141
  }
1137
1142
  return wrap;
1138
1143
  }
1144
+ function resetCardsKnownEmpty() {
1145
+ if (knownResetCards === void 0) return false;
1146
+ return Array.isArray(knownResetCards.fiveHour) && Array.isArray(knownResetCards.week) && knownResetCards.fiveHour.length === 0 && knownResetCards.week.length === 0;
1147
+ }
1148
+ function resetCollapsed() {
1149
+ if (!resetCardsKnownEmpty() || autoOpen || resetPending !== void 0 || resetBusy !== void 0 || resetNotice !== void 0 || autoSaveError !== void 0) return false;
1150
+ const auto = snapshot?.autoReset;
1151
+ if (auto?.fiveHour?.enabled === true || auto?.week?.enabled === true || auto?.persistError !== void 0 || auto?.lastEvent !== void 0) return false;
1152
+ return true;
1153
+ }
1154
+ function buildAutoMaster(auto) {
1155
+ if (auto === void 0 || typeof auto !== "object") return void 0;
1156
+ const master = doc.createElement("label");
1157
+ master.className = "dqw-auto-master";
1158
+ const masterBox = doc.createElement("input");
1159
+ masterBox.type = "checkbox";
1160
+ masterBox.className = "dqw-auto-box";
1161
+ masterBox.checked = autoOpen === true;
1162
+ masterBox.disabled = autoSaving === true;
1163
+ masterBox.addEventListener("change", () => {
1164
+ autoOpen = masterBox.checked;
1165
+ saveAutoOpen(win.localStorage, autoOpen);
1166
+ syncPop();
1167
+ });
1168
+ const anyEnabled = auto.fiveHour?.enabled === true || auto.week?.enabled === true;
1169
+ master.append(text(
1170
+ doc,
1171
+ "span",
1172
+ "dqw-reset-title",
1173
+ anyEnabled ? copy.resetCards.autoActive : copy.resetCards.autoToggle
1174
+ ), masterBox);
1175
+ return master;
1176
+ }
1139
1177
  function renderResetSection() {
1140
1178
  const section = doc.createElement("div");
1141
1179
  section.className = "dqw-reset";
1142
- const head = doc.createElement("div");
1143
- head.className = "dqw-reset-head";
1144
- const headLeft = doc.createElement("div");
1145
- headLeft.className = `dqw-reset-head-left${locale === "en" ? " is-en" : ""}`;
1146
- headLeft.append(text(doc, "span", "dqw-reset-title", copy.resetCards.section));
1147
- const reload = doc.createElement("button");
1148
- reload.type = "button";
1149
- reload.className = "dqw-reset-reload";
1150
- reload.textContent = resetLoading ? copy.loading : copy.resetCards.reloadLink;
1151
- reload.disabled = resetLoading || resetBusy !== void 0;
1152
- reload.addEventListener("click", () => {
1153
- loadResetCards();
1154
- });
1155
- headLeft.append(reload);
1156
- head.append(headLeft);
1157
1180
  const autoHeader = snapshot?.autoReset;
1158
- if (autoHeader !== void 0 && typeof autoHeader === "object") {
1159
- const master = doc.createElement("label");
1160
- master.className = "dqw-auto-master";
1161
- const masterBox = doc.createElement("input");
1162
- masterBox.type = "checkbox";
1163
- masterBox.className = "dqw-auto-box";
1164
- masterBox.checked = autoOpen === true;
1165
- masterBox.disabled = autoSaving === true;
1166
- masterBox.addEventListener("change", () => {
1167
- autoOpen = masterBox.checked;
1168
- saveAutoOpen(win.localStorage, autoOpen);
1169
- syncPop();
1170
- });
1171
- const anyEnabled = autoHeader.fiveHour?.enabled === true || autoHeader.week?.enabled === true;
1172
- master.append(text(
1173
- doc,
1174
- "span",
1175
- "dqw-reset-title",
1176
- anyEnabled ? copy.resetCards.autoActive : copy.resetCards.autoToggle
1177
- ), masterBox);
1178
- head.append(master);
1181
+ const master = buildAutoMaster(autoHeader);
1182
+ if (resetCollapsed()) {
1183
+ const row = doc.createElement("div");
1184
+ row.className = "dqw-reset-collapsed";
1185
+ row.append(text(doc, "span", "dqw-reset-title", `${copy.resetCards.section} \xB7 ${copy.resetCards.none}`));
1186
+ if (master !== void 0) row.append(master);
1187
+ section.append(row);
1188
+ return section;
1179
1189
  }
1190
+ const head = doc.createElement("div");
1191
+ head.className = "dqw-reset-head";
1192
+ head.append(text(doc, "span", "dqw-reset-title", copy.resetCards.section));
1193
+ if (master !== void 0) head.append(master);
1180
1194
  section.append(head);
1181
- const cards = resetInfo?.ok === true && resetInfo.cards !== void 0 ? resetInfo.cards : void 0;
1195
+ const cards = knownResetCards;
1182
1196
  if (cards === void 0) {
1183
- const message = resetLoading ? copy.loading : resetInfo?.message ?? (resetInfo === void 0 ? copy.loading : copy.resetCards.loadFailed);
1197
+ const message = resetLoading ? copy.loading : resetInfo?.message ?? snapshot?.reset?.error ?? (resetInfo === void 0 ? copy.loading : copy.resetCards.loadFailed);
1184
1198
  section.append(text(doc, "p", "dqw-muted", message));
1185
1199
  return section;
1186
1200
  }
@@ -1255,7 +1269,7 @@ function mountQuotaCard({
1255
1269
  }
1256
1270
  function renderDetail(detail, key, provider, fromPanelNav = false) {
1257
1271
  detail.replaceChildren();
1258
- detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav }));
1272
+ detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav, showRefresh: key === "glm" }));
1259
1273
  if (provider?.status === "stale") {
1260
1274
  const bannerPrefix = provider.errorKind === "auth" ? copy.authRejected : copy.stale;
1261
1275
  detail.append(text(doc, "p", "dqw-error", `${bannerPrefix}${provider.error ? `: ${provider.error}` : ""}`));
@@ -1331,7 +1345,7 @@ function mountQuotaCard({
1331
1345
  }
1332
1346
  if (provider?.credential === "configured" && provider?.providerId === PROVIDER_IDS.glmDomestic) {
1333
1347
  detail.append(renderResetSection());
1334
- if (resetInfo === void 0 && !resetLoading) {
1348
+ if (resetInfo === void 0 && knownResetCards === void 0 && !resetLoading) {
1335
1349
  void loadResetCards();
1336
1350
  }
1337
1351
  }
@@ -1512,6 +1526,8 @@ function mountQuotaCard({
1512
1526
  };
1513
1527
  const render = (next) => {
1514
1528
  snapshot = next;
1529
+ const cards = next?.reset?.status === "ready" ? next.reset.cards : void 0;
1530
+ if (Array.isArray(cards?.fiveHour) && Array.isArray(cards?.week)) knownResetCards = cards;
1515
1531
  renderAll();
1516
1532
  };
1517
1533
  const showTransportError = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
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
@@ -66,7 +66,6 @@ const COPY = {
66
66
  count: '{count} 张',
67
67
  none: '无可用',
68
68
  use: '重置',
69
- reloadLink: '( 刷新 )',
70
69
  autoToggle: '自动用卡',
71
70
  autoActive: '自动用卡 · 已启用',
72
71
  expiry: '{date} 前有效',
@@ -138,7 +137,6 @@ const COPY = {
138
137
  count: '{count}',
139
138
  none: 'none',
140
139
  use: 'Reset',
141
- reloadLink: '( Refresh )',
142
140
  autoToggle: 'Auto-use',
143
141
  autoActive: 'Auto-use · on',
144
142
  expiry: 'expires {date}',
@@ -234,6 +232,8 @@ const FLOAT_STYLE_TEXT = `
234
232
  .dqw-panel-back { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 28px; height: 28px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; }
235
233
  .dqw-panel-back svg { display: block; width: 16px; height: 16px; }
236
234
  .dqw-panel-back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
235
+ .dqw-panel-refresh { flex: none; margin: 0; padding: 4px 6px; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; font: inherit; font-size: 10px; line-height: 14px; opacity: .72; }
236
+ .dqw-panel-refresh:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); opacity: 1; }
237
237
  .dqw-panel-close { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 24px; height: 24px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer; font: inherit; font-size: 12px; line-height: 1; }
238
238
  .dqw-panel-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); color: var(--dsw-alias-label-primary, inherit); }
239
239
  .dqw-overview-list { display: flex; flex-direction: column; gap: 2px; }
@@ -273,18 +273,11 @@ const FLOAT_STYLE_TEXT = `
273
273
  .dqw-win-mid { opacity: .65; white-space: nowrap; }
274
274
  .dqw-win-used { opacity: .65; white-space: nowrap; }
275
275
  .dqw-win-val { min-width: 0; font-weight: 600; }
276
- /* Reset cards: a bordered block below the window grid. Every control is a real
277
- <button> so keyboard focus and the disabled state come from the platform. */
276
+ /* Reset cards: keep the dashed divider even when an empty wallet collapses to
277
+ one row. The panel's 立即刷新 is the only manual re-check control. */
278
278
  .dqw-reset { border-top: 1px dashed var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 4px; }
279
- .dqw-reset-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 10px; line-height: 14px; }
279
+ .dqw-reset-head, .dqw-reset-collapsed { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; font-size: 10px; line-height: 14px; }
280
280
  .dqw-reset-title { opacity: .65; }
281
- /* No underline: the full-width 「( 刷新 )」 brackets carry the affordance, the
282
- hover opacity bump is the only hover signal. */
283
- .dqw-reset-reload { border: 0; background: none; padding: 0; font: inherit; font-size: 10px; color: var(--dsw-alias-label-secondary, inherit); opacity: .65; cursor: pointer; }
284
- .dqw-reset-reload:hover { opacity: 1; }
285
- .dqw-reset-reload:disabled { cursor: default; opacity: .35; }
286
- .dqw-reset-head-left { display: flex; align-items: baseline; min-width: 0; }
287
- .dqw-reset-head-left.is-en { gap: 4px; }
288
281
  .dqw-auto-master { display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 10px; line-height: 14px; }
289
282
  .dqw-auto-master .dqw-auto-box { accent-color: var(--dsw-alias-button-primary-fill, #5b8def); }
290
283
  .dqw-reset-lines { display: grid; grid-template-columns: max-content max-content minmax(0, 1fr) max-content max-content; column-gap: 6px; row-gap: 4px; align-items: center; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
@@ -817,9 +810,11 @@ export function mountQuotaCard({
817
810
  let queuedPlace = false
818
811
  let openKey
819
812
  let panelNav = false
820
- // Reset-card state is client-local: the card list is fetched only while the GLM
821
- // detail is open, so a closed panel never costs an extra upstream round trip.
822
- let resetInfo // last listResetCards payload (or a synthesized error)
813
+ // The host now refreshes the wallet on a slow independent clock; the detail
814
+ // still fetches on first open. Keep the last authoritative list through a
815
+ // transient list failure so the section never vanishes or invents zero cards.
816
+ let knownResetCards
817
+ let resetInfo // last on-demand listResetCards payload (or an error)
823
818
  let resetLoading = false
824
819
  let resetPending // reset type awaiting confirmation, or undefined
825
820
  let resetBusy // reset type currently being spent, or undefined
@@ -890,8 +885,8 @@ export function mountQuotaCard({
890
885
  return [row]
891
886
  }
892
887
 
893
- /** Panel chrome: optional back, title, always a close affordance. */
894
- const panelHeader = ({ title, showBack = false }) => {
888
+ /** Panel chrome: optional back/refresh, title and close affordance. */
889
+ const panelHeader = ({ title, showBack = false, showRefresh = false }) => {
895
890
  const header = doc.createElement('div')
896
891
  header.className = 'dqw-panel-header'
897
892
  if (showBack) {
@@ -905,6 +900,15 @@ export function mountQuotaCard({
905
900
  header.append(back)
906
901
  }
907
902
  header.append(text(doc, 'p', 'dqw-panel-title', title))
903
+ if (showRefresh) {
904
+ const refresh = doc.createElement('button')
905
+ refresh.type = 'button'
906
+ refresh.className = 'dqw-panel-refresh'
907
+ refresh.dataset.action = 'panel-refresh'
908
+ refresh.textContent = copy.actions.refreshNow
909
+ refresh.setAttribute('aria-label', copy.actions.refreshNow)
910
+ header.append(refresh)
911
+ }
908
912
  const close = doc.createElement('button')
909
913
  close.type = 'button'
910
914
  close.className = 'dqw-panel-close'
@@ -971,11 +975,8 @@ export function mountQuotaCard({
971
975
  panel.append(overviewActions())
972
976
  }
973
977
 
974
- /**
975
- * Fetch the reset-card list and repaint the open panel.
976
- * Called when the GLM detail opens and after a spend; never on the poller, so a
977
- * closed panel costs nothing upstream.
978
- */
978
+ /** Fetch a live card list for the detail or after spending, independently of
979
+ * the host's ten-minute background snapshot and its force-refresh route. */
979
980
  async function loadResetCards() {
980
981
  if (resetLoading || disposed) return
981
982
  resetLoading = true
@@ -983,6 +984,7 @@ export function mountQuotaCard({
983
984
  try {
984
985
  const payload = await fetchResetEnvelope(fetchImpl, CLIENT_ROUTES.resetCards)
985
986
  resetInfo = payload
987
+ if (payload.ok === true && payload.cards !== undefined) knownResetCards = payload.cards
986
988
  } catch {
987
989
  resetInfo = { ok: false, error: 'request-failed' }
988
990
  } finally {
@@ -1010,6 +1012,7 @@ export function mountQuotaCard({
1010
1012
  // The host returns the post-reset card list; fall back to a re-list when it
1011
1013
  // could not refresh, so the panel never offers a card that is already gone.
1012
1014
  resetInfo = payload.cards !== undefined ? { ok: true, cards: payload.cards } : resetInfo
1015
+ if (payload.cards !== undefined) knownResetCards = payload.cards
1013
1016
  } else {
1014
1017
  resetNotice = { kind: 'error', message: payload.message ?? copy.resetCards.failed }
1015
1018
  }
@@ -1135,57 +1138,74 @@ export function mountQuotaCard({
1135
1138
  return wrap
1136
1139
  }
1137
1140
 
1141
+ /** Only an authoritative successful list can mean "no cards". Errors retain
1142
+ * the last good inventory, preventing a transient upstream failure from hiding
1143
+ * controls or claiming that a still-available card is gone. */
1144
+ function resetCardsKnownEmpty() {
1145
+ if (knownResetCards === undefined) return false
1146
+ return Array.isArray(knownResetCards.fiveHour) && Array.isArray(knownResetCards.week)
1147
+ && knownResetCards.fiveHour.length === 0 && knownResetCards.week.length === 0
1148
+ }
1149
+
1150
+ function resetCollapsed() {
1151
+ if (!resetCardsKnownEmpty() || autoOpen || resetPending !== undefined
1152
+ || resetBusy !== undefined || resetNotice !== undefined || autoSaveError !== undefined) return false
1153
+ const auto = snapshot?.autoReset
1154
+ if (auto?.fiveHour?.enabled === true || auto?.week?.enabled === true
1155
+ || auto?.persistError !== undefined || auto?.lastEvent !== undefined) return false
1156
+ return true
1157
+ }
1158
+
1159
+ /** This checkbox only reveals the real per-window switches; keep it reachable
1160
+ * on the single-line empty state so an account without cards can still pre-arm. */
1161
+ function buildAutoMaster(auto) {
1162
+ if (auto === undefined || typeof auto !== 'object') return undefined
1163
+ const master = doc.createElement('label')
1164
+ master.className = 'dqw-auto-master'
1165
+ const masterBox = doc.createElement('input')
1166
+ masterBox.type = 'checkbox'
1167
+ masterBox.className = 'dqw-auto-box'
1168
+ masterBox.checked = autoOpen === true
1169
+ masterBox.disabled = autoSaving === true
1170
+ masterBox.addEventListener('change', () => {
1171
+ autoOpen = masterBox.checked
1172
+ saveAutoOpen(win.localStorage, autoOpen)
1173
+ syncPop()
1174
+ })
1175
+ // An unchecked disclosure must never imply the host's auto-use switch is off.
1176
+ const anyEnabled = auto.fiveHour?.enabled === true || auto.week?.enabled === true
1177
+ master.append(text(doc, 'span', 'dqw-reset-title',
1178
+ anyEnabled ? copy.resetCards.autoActive : copy.resetCards.autoToggle), masterBox)
1179
+ return master
1180
+ }
1181
+
1138
1182
  /** The reset block: counts, per-type reset buttons, inline confirm and last result. */
1139
1183
  function renderResetSection() {
1140
1184
  const section = doc.createElement('div')
1141
1185
  section.className = 'dqw-reset'
1142
- const head = doc.createElement('div')
1143
- head.className = 'dqw-reset-head'
1144
- // Left: the title carries the manual reload affordance —「额度重置卡( 刷新 )」.
1145
- const headLeft = doc.createElement('div')
1146
- headLeft.className = `dqw-reset-head-left${locale === 'en' ? ' is-en' : ''}`
1147
- headLeft.append(text(doc, 'span', 'dqw-reset-title', copy.resetCards.section))
1148
- const reload = doc.createElement('button')
1149
- reload.type = 'button'
1150
- reload.className = 'dqw-reset-reload'
1151
- reload.textContent = resetLoading ? copy.loading : copy.resetCards.reloadLink
1152
- reload.disabled = resetLoading || resetBusy !== undefined
1153
- reload.addEventListener('click', () => { loadResetCards() })
1154
- headLeft.append(reload)
1155
- head.append(headLeft)
1156
- // Right: a pure visibility toggle. Checking it only reveals the two auto-use
1157
- // rows below; actually enabling a window stays with its own per-window switch,
1158
- // so the default panel is three lines and no single click can arm auto-spend.
1159
1186
  const autoHeader = snapshot?.autoReset
1160
- if (autoHeader !== undefined && typeof autoHeader === 'object') {
1161
- const master = doc.createElement('label')
1162
- master.className = 'dqw-auto-master'
1163
- const masterBox = doc.createElement('input')
1164
- masterBox.type = 'checkbox'
1165
- masterBox.className = 'dqw-auto-box'
1166
- masterBox.checked = autoOpen === true
1167
- masterBox.disabled = autoSaving === true
1168
- masterBox.addEventListener('change', () => {
1169
- autoOpen = masterBox.checked
1170
- saveAutoOpen(win.localStorage, autoOpen)
1171
- syncPop()
1172
- })
1173
- // Expansion is not authorization: disclose the persisted host switch state
1174
- // even when these rows are collapsed, so an unchecked box cannot imply off.
1175
- const anyEnabled = autoHeader.fiveHour?.enabled === true || autoHeader.week?.enabled === true
1176
- master.append(text(doc, 'span', 'dqw-reset-title',
1177
- anyEnabled ? copy.resetCards.autoActive : copy.resetCards.autoToggle), masterBox)
1178
- head.append(master)
1187
+ const master = buildAutoMaster(autoHeader)
1188
+ if (resetCollapsed()) {
1189
+ const row = doc.createElement('div')
1190
+ row.className = 'dqw-reset-collapsed'
1191
+ row.append(text(doc, 'span', 'dqw-reset-title', `${copy.resetCards.section} · ${copy.resetCards.none}`))
1192
+ if (master !== undefined) row.append(master)
1193
+ section.append(row)
1194
+ return section
1179
1195
  }
1196
+ const head = doc.createElement('div')
1197
+ head.className = 'dqw-reset-head'
1198
+ head.append(text(doc, 'span', 'dqw-reset-title', copy.resetCards.section))
1199
+ if (master !== undefined) head.append(master)
1180
1200
  section.append(head)
1181
1201
 
1182
- const cards = resetInfo?.ok === true && resetInfo.cards !== undefined ? resetInfo.cards : undefined
1202
+ const cards = knownResetCards
1183
1203
  if (cards === undefined) {
1184
1204
  // Nothing fetched yet, a failed fetch, or no GLM credential on the host: say so
1185
1205
  // instead of drawing two rows of zeroes that look like "no cards left".
1186
1206
  const message = resetLoading
1187
1207
  ? copy.loading
1188
- : resetInfo?.message ?? (resetInfo === undefined ? copy.loading : copy.resetCards.loadFailed)
1208
+ : resetInfo?.message ?? snapshot?.reset?.error ?? (resetInfo === undefined ? copy.loading : copy.resetCards.loadFailed)
1189
1209
  section.append(text(doc, 'p', 'dqw-muted', message))
1190
1210
  return section
1191
1211
  }
@@ -1260,7 +1280,7 @@ export function mountQuotaCard({
1260
1280
 
1261
1281
  function renderDetail(detail, key, provider, fromPanelNav = false) {
1262
1282
  detail.replaceChildren()
1263
- detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav }))
1283
+ detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav, showRefresh: key === 'glm' }))
1264
1284
  if (provider?.status === 'stale') {
1265
1285
  const bannerPrefix = provider.errorKind === 'auth' ? copy.authRejected : copy.stale
1266
1286
  detail.append(text(doc, 'p', 'dqw-error', `${bannerPrefix}${provider.error ? `: ${provider.error}` : ''}`))
@@ -1343,10 +1363,9 @@ export function mountQuotaCard({
1343
1363
  // international (z.ai) provider would only ever render a permanent failure.
1344
1364
  if (provider?.credential === 'configured' && provider?.providerId === PROVIDER_IDS.glmDomestic) {
1345
1365
  detail.append(renderResetSection())
1346
- // Cards are fetched lazily: opening the GLM detail is the first moment the
1347
- // list is worth an upstream call, and the fetch repaints this panel when it
1348
- // lands.
1349
- if (resetInfo === undefined && !resetLoading) {
1366
+ // The host snapshot carries the full recent wallet. Fetch lazily only
1367
+ // when neither it nor a prior on-demand read has a usable card list.
1368
+ if (resetInfo === undefined && knownResetCards === undefined && !resetLoading) {
1350
1369
  void loadResetCards()
1351
1370
  }
1352
1371
  }
@@ -1563,6 +1582,8 @@ export function mountQuotaCard({
1563
1582
 
1564
1583
  const render = (next) => {
1565
1584
  snapshot = next
1585
+ const cards = next?.reset?.status === 'ready' ? next.reset.cards : undefined
1586
+ if (Array.isArray(cards?.fiveHour) && Array.isArray(cards?.week)) knownResetCards = cards
1566
1587
  renderAll()
1567
1588
  }
1568
1589
 
@@ -1680,6 +1701,7 @@ export function mountQuotaCard({
1680
1701
  // sit inside this shadow tree, so a panel-level listener sees the real
1681
1702
  // target with no retargeting ambiguity.
1682
1703
  if (event.target?.closest?.('[data-action="panel-refresh"]')) {
1704
+ // The host force-refresh re-lists cards too; do not issue a duplicate GET.
1683
1705
  void poll(true)
1684
1706
  return
1685
1707
  }
package/src/host.mjs CHANGED
@@ -516,13 +516,24 @@ export function createQuotaService(ctx, options = {}) {
516
516
  // same window is retried, so a persistent error cannot hammer
517
517
  // the upstream every poll.
518
518
 
519
- function refresh() {
520
- if (pollPromise !== undefined) return pollPromise
519
+ function refresh(forceCards = false) {
520
+ if (pollPromise !== undefined) {
521
+ // A manual refresh must still re-list cards when it overlaps a scheduled
522
+ // round whose ten-minute gate skipped the list.
523
+ if (forceCards !== true) return pollPromise
524
+ return pollPromise.then(async () => {
525
+ if (!disposed) await probeResetCards(true)
526
+ return view()
527
+ })
528
+ }
521
529
  if (disposed) return Promise.resolve(view())
522
530
  pollPromise = Promise.all([probeGlm(), probeCopilot()])
523
531
  .then(async () => {
524
532
  updatedAt = now()
533
+ // Spending gets priority over the read-only background list: otherwise
534
+ // two wallet reads could race and a manual spend could slip between them.
525
535
  await maybeAutoReset()
536
+ await probeResetCards(forceCards === true)
526
537
  return view()
527
538
  })
528
539
  .finally(() => { pollPromise = undefined })
@@ -530,11 +541,31 @@ export function createQuotaService(ctx, options = {}) {
530
541
  }
531
542
 
532
543
  // --- Reset cards -----------------------------------------------------------
533
- // Card state is kept apart from the quota snapshot: a card list is only fetched
534
- // on demand (opening the panel, or right before spending one), and a failed fetch
535
- // must never blank the provider rows the poller maintains.
544
+ // Card state is independent of the quota probes: poll it on its own slow
545
+ // clock, as well as on demand and before spending. A failed list never blanks
546
+ // the provider rows or turns a known card count into an authoritative zero.
536
547
  let resetState
537
548
  let resetSpendInFlight = false
549
+ let resetProbePromise
550
+ const RESET_CARDS_POLL_INTERVAL_MS = 10 * 60_000
551
+
552
+ async function probeResetCards(force = false) {
553
+ if (configuredGlmProvider(ctx) !== PROVIDER_IDS.glmDomestic || resetSpendInFlight) return
554
+ if (resetProbePromise !== undefined) return resetProbePromise
555
+ if (!force && resetState?.checkedAt !== undefined
556
+ && now() - resetState.checkedAt < RESET_CARDS_POLL_INTERVAL_MS) return
557
+ const probe = (async () => {
558
+ try {
559
+ await listResetCards()
560
+ } catch (error) {
561
+ // Even unexpected credential failures cannot break the quota poll round.
562
+ const { message, kind } = classifyProbeError(error)
563
+ resetState = { status: 'error', error: message, errorKind: kind, checkedAt: now() }
564
+ }
565
+ })()
566
+ resetProbePromise = probe
567
+ try { await probe } finally { resetProbePromise = undefined }
568
+ }
538
569
 
539
570
  /** Resolve the GLM provider + key, or report why no reset can be attempted. */
540
571
  async function resetCredential() {
@@ -548,11 +579,8 @@ export function createQuotaService(ctx, options = {}) {
548
579
  return { provider, apiKey }
549
580
  }
550
581
 
551
- /**
552
- * Fetch the available reset cards.
553
- * `force` is advisory only — the endpoint is cheap and the panel opens rarely, so
554
- * every call re-reads upstream rather than serving a stale count before a spend.
555
- */
582
+ /** Fetch the live available cards. The poll clock gates only the background
583
+ * probe; an explicit list or spend always re-reads upstream. */
556
584
  async function listResetCards() {
557
585
  const credential = await resetCredential()
558
586
  if (credential.error !== undefined) {
@@ -584,6 +612,9 @@ export function createQuotaService(ctx, options = {}) {
584
612
  if (resetSpendInFlight) return { ok: false, error: 'busy', message: '重置操作进行中,请稍后重试' }
585
613
  resetSpendInFlight = true
586
614
  try {
615
+ // A spend re-lists against live state; wait for any cosmetic background
616
+ // read first so its older result cannot overwrite the post-spend wallet.
617
+ if (resetProbePromise !== undefined) await resetProbePromise
587
618
  return await performResetCard(args, autoWindowKey)
588
619
  } finally {
589
620
  resetSpendInFlight = false
@@ -842,7 +873,7 @@ export function makeRefreshRoute(ctx, service) {
842
873
  handler: async (req, res) => {
843
874
  if (!requestAllowed(ctx, req, res) || !methodAllowed(req, res, 'POST')) return
844
875
  try {
845
- await service.refresh()
876
+ await service.refresh(true)
846
877
  responseJson(res, 200, service.overview())
847
878
  } catch {
848
879
  responseJson(res, 500, { ok: false, error: 'refresh failed' })