@eddyskywalker/dsh-chatgpt-subscription 0.36.0 → 0.36.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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.36.1 - 2026-10-22
6
+ - **[cline-pass] 额度不再只在手动刷新时动:状态读取按 TTL 在答背后刷新,设置卡与输入框徽标 60 秒自动更新**
7
+ - **现象**:Cline Pass 设置页打开后额度数字就定住了——同一次会话里消耗掉的部分,要重新打开页面或点一次「刷新额度」才会反映;输入框旁的额度徽标同样停在首次读到的数字。
8
+ - **根因**:两层各缺一半。宿主侧 `GET /cline-pass/api/status` 只答 `store.usage()` 的内存快照,只有显式 `POST /usage` 才真正向上游读(模块注释还写着「读取不触发上游调用」);客户端侧设置页只在挂载时读一次 status,而共享徽标虽然自带 60 秒轮询,cline 这一行的 `readStatus` 却把 `refresh` 参数丢掉了,点击于是退化成又一次廉价 GET。
9
+ - **改法(宿主)**:新增 `CLINE_PASS_QUOTA_TTL_MS`(5 分钟)与 `usageStale()`(`fetchedAt <= 0` 同样算过期);`GET /status` 先答缓存,快照缺失或过期时用 `QuotaRefresh` 在答背后起一轮刷新,并把 `quotaRefreshing` 放进状态 DTO,让客户端知道该立刻补读;扇出逻辑抽成 `refreshUsage()`,显式 `POST /usage` 与后台刷新共用同一份实现,同源校验、400 文案、凭据零外泄全部保持不变。单飞由 `start()` 的 `pending` 保证:N 次轮询只扇出一轮。另给路由选项加了 `quotaTtlMs` 测试缝。
10
+ - **改法(客户端)**:`ClinePassApi.status(refresh)` 分流——`false` 走 `GET /status`,`true` 走 `POST /usage` 并只取其中的 status 半边;设置页补上 60 秒可见轮询 + `visibilitychange` + `createQuotaFollowUp`(读到 `quotaRefreshing: true` 就 1.5 秒后补读,最多 3 次);轮询是静默的——失败既不刷红字也不清掉用户正在看的红字,半输入的草稿由既有两道闸保护;徽标把 `refresh` 转发出去,点击才真正强制刷新。
11
+ - **测试**:新增 `test/cline-pass-quota-poll.test.ts`(9 例)与 `test/cline-pass-quota-poll.test.tsx`(10 例)——冷缓存即排刷新并回 `quotaRefreshing`、新鲜快照 0 次上游调用、假 Date 拨过 TTL 后缓存照答而背后替换、2 账号 3 次轮询只扇出 2 次、显式动作立即刷新且 400/403 原样、徽标点击走 POST /usage、隐藏标签页不读、轮询失败静默、卸载清理定时器与监听。
12
+ - **验证**:`npm run typecheck` 三工程 0 错误;`npm test` 274 文件 / 4470 例通过(1 条 `minimax-code-video-history` 的 Windows `rename EPERM` 抖动,单跑 22/22 通过,与本改动无关);`npm run build` 通过。
13
+ - **已知边界**:`QuotaRefresh.run()` 不与正在跑的 `start()` 合流,所以「手动刷新正好撞上后台刷新」这一步仍会多扇出一轮;两轮写入形状相同、后写的 `setUsage` 生效,代价是一次多余上游请求。真正合流要给共享 helper 加 `wait`,会牵动另外六条线路,本次不做,代码里按此写明。
5
14
  ## 0.36.0 - 2026-10-22
6
15
  - **[client] 修复俄语语言包与 `@linxin666/dsh-i18n` 等已安装语言包冲突导致插件客户端激活失败崩溃的问题**
7
16
  - **现象**:Web 界面启动失败,提示 `web boot: 1 entry did not activate`,`@eddyskywalker/dsh-chatgpt-subscription: failed`。
package/lib/client.js CHANGED
@@ -12801,14 +12801,37 @@ window.__ModuleLoader__.load({
12801
12801
  //#region src/client/cline-pass/api.ts
12802
12802
  const api$7 = createLineApi(CLINE_PASS_ROUTE_PREFIX, "Cline Pass");
12803
12803
  /**
12804
+ * Whether the host is refreshing the quota behind this answer.
12805
+ *
12806
+ * The field is optional by contract, because a host built before it existed
12807
+ * answers without it. Every caller only ever compares it to `true`, so "absent"
12808
+ * and "false" read the same — nothing is in flight — and a stale host degrades to
12809
+ * plain polling instead of crashing. Kept as one reader so that convention lives
12810
+ * in a single place rather than being re-derived at each call site.
12811
+ */
12812
+ function quotaRefreshingOf(status) {
12813
+ return status.quotaRefreshing === true;
12814
+ }
12815
+ /**
12804
12816
  * The Cline Pass settings API, as the card uses it.
12805
12817
  *
12806
12818
  * Deliberately the same envelope and the same fetch shape every other provider
12807
12819
  * section uses, so the shared account-pool card and this helper read alike.
12808
12820
  */
12809
12821
  var ClinePassApi = class {
12810
- status() {
12811
- return api$7.get("/status");
12822
+ /**
12823
+ * One status read.
12824
+ *
12825
+ * A plain call is the GET the first paint, the 60 s poll and the composer
12826
+ * badge use: it is cheap and never fans out one upstream request per account.
12827
+ * `refresh` is the forced path — the same POST /usage the card's own button
12828
+ * drives — and the host answers it with `{ usage, status }`; only the status
12829
+ * half is returned, because every caller renders a status card rather than the
12830
+ * raw quota payload.
12831
+ */
12832
+ status(refresh = false) {
12833
+ if (!refresh) return api$7.get("/status");
12834
+ return api$7.post("/usage", {}).then((outcome) => outcome.status);
12812
12835
  }
12813
12836
  updateSettings(patch) {
12814
12837
  return api$7.post("/settings", patch).then(() => this.status());
@@ -13186,6 +13209,14 @@ window.__ModuleLoader__.load({
13186
13209
  //#endregion
13187
13210
  //#region src/client/cline-pass/ClinePassSection.tsx
13188
13211
  const API$3 = new ClinePassApi();
13212
+ /**
13213
+ * How often an open card re-reads the quota.
13214
+ *
13215
+ * The same 60 s every other line's card and composer badge uses: a window that
13216
+ * moves on a multi-hour scale does not need a faster poll, and a slower one
13217
+ * would leave a card visibly stale after the user spends a request.
13218
+ */
13219
+ const QUOTA_POLL_MS = 6e4;
13189
13220
  function detail$1(error) {
13190
13221
  return error instanceof Error ? error.message : String(error);
13191
13222
  }
@@ -13231,19 +13262,46 @@ window.__ModuleLoader__.load({
13231
13262
  const [settingsForm, setSettingsForm] = (0, react.useState)(null);
13232
13263
  const [history, setHistory] = (0, react.useState)(null);
13233
13264
  const [loginFlow, setLoginFlow] = (0, react.useState)(null);
13234
- const reload = (0, react.useCallback)(async () => {
13265
+ /** Follow-up poll owed while the host refreshes the quota behind an answer. */
13266
+ const quotaFollowUp = (0, react.useRef)(null);
13267
+ /**
13268
+ * One status read; `quiet` is every read nobody pressed a button for.
13269
+ *
13270
+ * A quiet read is silent on purpose: it never paints an error and never clears
13271
+ * one, so a poll that fails while a user reads the page, or types into a
13272
+ * settings field, does not change what is on screen. The manual actions below
13273
+ * still report their own failures loudly. The draft guards inside the two
13274
+ * setters are what keep a background answer from overwriting typed text.
13275
+ */
13276
+ const loadStatus = (0, react.useCallback)(async (quiet = false) => {
13235
13277
  try {
13236
13278
  const next = await API$3.status();
13237
13279
  setStatus(next);
13238
13280
  setSettingsForm((current) => current ?? (next.settings === void 0 ? null : settingsFormOf$1(next)));
13239
- setError(null);
13281
+ if (!quiet) setError(null);
13282
+ quotaFollowUp.current ??= createQuotaFollowUp();
13283
+ quotaFollowUp.current.observe(quotaRefreshingOf(next), () => {
13284
+ loadStatus(true);
13285
+ });
13240
13286
  } catch (cause) {
13241
- setError(detail$1(cause));
13287
+ if (!quiet) setError(detail$1(cause));
13242
13288
  }
13243
13289
  }, []);
13244
13290
  (0, react.useEffect)(() => {
13245
- reload();
13246
- }, [reload]);
13291
+ loadStatus();
13292
+ }, [loadStatus]);
13293
+ (0, react.useEffect)(() => {
13294
+ const refreshWhenVisible = () => {
13295
+ if (document.visibilityState === "visible") loadStatus(true);
13296
+ };
13297
+ document.addEventListener("visibilitychange", refreshWhenVisible);
13298
+ const timer = window.setInterval(refreshWhenVisible, QUOTA_POLL_MS);
13299
+ return () => {
13300
+ window.clearInterval(timer);
13301
+ document.removeEventListener("visibilitychange", refreshWhenVisible);
13302
+ quotaFollowUp.current?.cancel();
13303
+ };
13304
+ }, [loadStatus]);
13247
13305
  (0, react.useEffect)(() => {
13248
13306
  if (status === null) return;
13249
13307
  setDrafts((current) => {
@@ -13271,12 +13329,12 @@ window.__ModuleLoader__.load({
13271
13329
  if (next.status === "complete") {
13272
13330
  setNotice(zh$4.loginComplete.replace("{account}", next.accountAlias === "" ? "Cline Pass" : next.accountAlias));
13273
13331
  setShowKeyForm(false);
13274
- reload();
13332
+ loadStatus();
13275
13333
  } else if (next.status === "failed") setError(zh$4.loginFailed.replace("{detail}", next.error));
13276
13334
  }).catch((cause) => setError(detail$1(cause)));
13277
13335
  }, Math.max(2, flow.intervalSeconds) * 1e3);
13278
13336
  return () => window.clearInterval(timer);
13279
- }, [loginFlow, reload]);
13337
+ }, [loginFlow, loadStatus]);
13280
13338
  const startLogin = (0, react.useCallback)(() => {
13281
13339
  setBusy("login");
13282
13340
  setError(null);
@@ -21467,8 +21525,11 @@ window.__ModuleLoader__.load({
21467
21525
  *
21468
21526
  * Quota follows the ACCOUNT, and a pool has several. The badge therefore shows
21469
21527
  * the most-consumed window across the accounts the host has already read, which
21470
- * is the one that will stop the next request first; it never triggers a quota
21471
- * read of its own, so a poll cannot fan out one upstream call per account.
21528
+ * is the one that will stop the next request first. Its own 60 s poll is a plain
21529
+ * read, so a poll never fans out one upstream call per account; only an explicit
21530
+ * click on the badge asks the host for the forced refresh, the same POST the
21531
+ * card's own "refresh usage" button drives — which is exactly what the shared
21532
+ * badge's `refresh` argument is for, and what this line used to drop.
21472
21533
  */
21473
21534
  function ClinePassComposerQuota(props) {
21474
21535
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerQuotaBadge, {
@@ -21476,7 +21537,7 @@ window.__ModuleLoader__.load({
21476
21537
  loadModelDirectory: props.loadModelDirectory,
21477
21538
  providerId: "cline-pass",
21478
21539
  label: zh$4.composerLabel,
21479
- readStatus: () => API$1.status(),
21540
+ readStatus: (refresh) => API$1.status(refresh),
21480
21541
  selectFacts: selectClinePassFacts
21481
21542
  });
21482
21543
  }