dsh-connect-trae 3.10.0 → 3.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.10.2 (2026-10-19)
4
+
5
+ > 设置页的账号列表**先出、积分后补**。打开卡片原本要等**一整份**用量文档组装完才画第一笔:那份文档的
6
+ > 尾巴上挂着「逐账号读上游余额」和「逐账号读签到状态」,每个账号一次网络往返,单次超时 30 秒、没有重试,
7
+ > 上游一慢就层层等待——这正是兄弟插件 dsh-connect-workbuddy 的 issue #77(实测 30–60 秒才看到账号)。
8
+ > Trae 这边同一份文档里 `poolMembersOf` 甚至被调了 **4 次**(`members`、`effectiveMemberAccountIds`、
9
+ > `memberNames`、`otherAccounts` 各一次),签到那条路还要再触发一次,所以同样的机器上比 workbuddy 更慢。
10
+ > 本版把首屏与「测量」拆开:首屏只用本机已存的东西,测量结果随后补上。
11
+
12
+ ### 修复
13
+
14
+ - **账号列表不再等上游余额**。`GET /usage?region=…&detail=fast` 返回一份只用本机状态的「首屏文档」:
15
+ `store.current()` 读已存凭据(不做网络刷新)、账号行来自新增的 `localMembers`(就是请求路径本来就在用的
16
+ `localPoolMembers`,零上游调用)、签到状态来自本进程已测过的缓存。**实测 0 次上游请求**,本机 1.3 秒的
17
+ 完整文档变成毫秒级首屏。
18
+ - **卡片分两笔画**:先请求 `detail=fast` 上屏,再请求完整文档补齐积分/签到/模型探测结果。首屏文档**永不
19
+ 覆盖**已经在屏的完整文档——否则每次轮询都会把积分闪回「—」。
20
+ - **首屏之前不再谎称「未登录」**。旧代码在 `statusByRegion` 里预置了一份 `signed-out` 占位文档并直接渲染,
21
+ 于是整段等待时间里已登录的用户看到的是「去登录」引导;现在状态表初始为空,未拿到任何文档时显示新增的
22
+ 「正在加载账号…」,真正的 `signed-out` 文档到了才显示登录引导。
23
+ - **完整文档故意保持不变**:`detail` 缺省、或值不是精确的 `fast`(比如大小写写错、拼成 `fastest`)时一律
24
+ 退回完整文档——宁可慢一会儿,也不能给出一份更空的卡片。
25
+ - Host 侧配套:`TraePoolDeps` 新增可选 `localMembers` / `cachedCheckinState`,`src/index.ts` 里分别接到
26
+ `localPoolMembers` 与新增的签到状态缓存(`CHECKIN_CACHE_MS = 10 * 60_000`)。老版本 Host 没有这两个
27
+ dep 时,首屏文档**省略** pool 段而不是回头去付那次昂贵读取。
28
+
29
+ ### 测试
30
+
31
+ - 新增 `tests/issue77-fast-first-paint.spec.ts`(11 例):首屏文档的上游调用数为 0、用的是 `current()`
32
+ 而不是 `resolve()`、缺 dep 时省略 pool 段、`detail` 缺省/拼错一律退回完整文档。
33
+ - 新增 `tests/issue77-card-first-paint.spec.tsx`(3 例):完整文档还在路上时账号行已经上屏、未拿到任何
34
+ 文档时显示「正在加载账号」而不是「未登录」引导、迟到的首屏文档不会覆盖已在屏的完整文档。
35
+ - 全仓 **902** 例通过,0 skipped。
36
+
37
+
38
+ ## 3.10.1 (2026-10-19)
39
+
40
+ > 「一键签到所有账号」下线。Trae 限制**每台设备每天只能签到一次**:本机任一账号签过之后,这个按钮
41
+ > 对整批账号在当天剩余时间里只能是灰的——一个永远点不动的控件,还得在下方专门写一句话解释它为什么灰。
42
+ > 单账号签到仍在名册表里,那才是能真正执行的入口。
43
+
44
+ ### 变更
45
+
46
+ - **移除账号池动作行里的「一键签到所有账号」按钮**,动作行现在只剩「一键测试所有账号」。连带移除
47
+ 批量签到的请求逻辑(`runCheckin`)、`busy` 的 `checkin` 态、签到进行中的状态行,以及只服务于这个
48
+ 按钮的 8 个文案键(中英各一份:`poolCheckinAll` / `poolCheckingIn` / `poolLogCheckinStart` /
49
+ `poolLogCheckinClaimed` / `poolLogCheckinAlready` / `poolLogCheckinFailed` / `poolLogCheckinDone` /
50
+ `poolActionNoteCheckinDevice`),并删掉卡片里已不可达的 `checkin-unsupported` 409 分支。
51
+ - **单账号签到不受影响**:名册表(「Trae 账号」表格)的签到列仍按账号逐个签到,签到列的状态文字与
52
+ 本机设备限制提示照旧。
53
+ - Host 侧 `?action=checkin` 路由与 `checkinSupported` 字段保留不删:浏览器里可能还有旧的 client
54
+ bundle 在调它,删掉会让它们直接 503。新卡片不再调用该 action。
55
+
56
+ ### 测试
57
+
58
+ - `card-pool` 的批量签到用例改为断言按钮缺席:不再渲染 `poolCheckinAll`、不再出现「本机今日的签到
59
+ 已用掉」提示行;同时保留「成员为空」「面板无免费模型」两条原因断言,确保这次删除没有顺带删掉测试
60
+ 按钮的说明。全仓 **888** 例通过,0 skipped。
61
+
3
62
  ## 3.10.0 (2026-10-18)
4
63
 
5
64
  > 本版把「账号在本机到底存了几份、删一个账号到底删掉了什么」讲清楚并做对:本机每个 Trae 登录都会
package/lib/client.js CHANGED
@@ -136,6 +136,22 @@ window.__ModuleLoader__.load({
136
136
  function withTraeTransferAction(path, panel, action) {
137
137
  return `${path}?${TRAE_REGION_PARAM}=${panel}&action=${action}`;
138
138
  }
139
+ /** Query parameter asking for a thinner first-paint document (issue #77). */
140
+ const TRAE_DETAIL_PARAM = "detail";
141
+ /** The only recognised `detail` value: local state only, no upstream read. */
142
+ const TRAE_DETAIL_FAST = "fast";
143
+ /**
144
+ * Address one panel's usage route asking for the FAST document, e.g.
145
+ * `/plugins/dsh-connect-trae/usage?region=cn&detail=fast`.
146
+ *
147
+ * Kept beside {@link withTraeRegion} for the same reason as
148
+ * {@link withTraeTransferAction}: the region is already a parameter, so a caller
149
+ * appending `&detail=` by hand is one typo away from a request that silently
150
+ * asks for the wrong document.
151
+ */
152
+ function withTraeRegionFastDetail(path, panel) {
153
+ return `${withTraeRegion(path, panel)}&${TRAE_DETAIL_PARAM}=${TRAE_DETAIL_FAST}`;
154
+ }
139
155
  /**
140
156
  * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
141
157
  * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
@@ -1269,7 +1285,6 @@ window.__ModuleLoader__.load({
1269
1285
  if (status === 409) switch (body?.reason) {
1270
1286
  case "no-members": return t("row.poolErrNoMembers");
1271
1287
  case "no-live-members": return t("row.poolErrNoLiveMembers");
1272
- case "checkin-unsupported": return t("row.poolErrCheckinUnsupported");
1273
1288
  case "target-model-stale": return t("row.poolErrTargetStale");
1274
1289
  case "no-free-model": return t("row.poolErrNoFreeModel");
1275
1290
  default: return t("row.poolErrConflict");
@@ -1497,43 +1512,6 @@ window.__ModuleLoader__.load({
1497
1512
  if (mountRef.current) setBusy(void 0);
1498
1513
  }
1499
1514
  };
1500
- const runCheckin = async () => {
1501
- if (busy !== void 0) return;
1502
- const members = [...selected];
1503
- if (members.length === 0) {
1504
- pushLog("warn", t("row.poolLogNoMembers"));
1505
- return;
1506
- }
1507
- setBusy("checkin");
1508
- setNow(Date.now());
1509
- pushLog("info", t("row.poolLogCheckinStart", { count: members.length }));
1510
- try {
1511
- const response = await fetch(withTraePoolAction(panel, "checkin"), { method: "POST" });
1512
- const body = await response.json();
1513
- if (!response.ok) throw new Error(renderRouteError(t, response.status, body));
1514
- for (const row of body?.rows ?? []) switch (row.status) {
1515
- case "claimed":
1516
- pushLog("ok", t("row.poolLogCheckinClaimed", { accountName: row.accountName }));
1517
- break;
1518
- case "already":
1519
- case "device-already":
1520
- pushLog("info", t("row.poolLogCheckinAlready", { accountName: row.accountName }));
1521
- break;
1522
- case "failed":
1523
- case "disabled": pushLog("error", t("row.poolLogCheckinFailed", {
1524
- accountName: row.accountName,
1525
- message: row.message ?? ""
1526
- }));
1527
- }
1528
- pushLog(members.length > 0 && body?.rows ? "ok" : "info", t("row.poolLogCheckinDone"));
1529
- onRefresh?.();
1530
- setNow(Date.now());
1531
- } catch (error) {
1532
- pushLog("error", t("row.poolLogBatchFailed", { message: error instanceof Error ? error.message : String(error) }));
1533
- } finally {
1534
- if (mountRef.current) setBusy(void 0);
1535
- }
1536
- };
1537
1515
  const runTest = async () => {
1538
1516
  if (busy !== void 0) return;
1539
1517
  const members = [...selected];
@@ -1763,7 +1741,6 @@ window.__ModuleLoader__.load({
1763
1741
  */
1764
1742
  const actionNotes = [];
1765
1743
  if (selected.size === 0) actionNotes.push(t("row.poolActionNoteNoMembers"));
1766
- if (deviceClaimed) actionNotes.push(t("row.poolActionNoteCheckinDevice"));
1767
1744
  if (pool.targetModelSource === "none") actionNotes.push(t("row.poolActionNoteTestNone"));
1768
1745
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1769
1746
  className: "dsm-trae-pool",
@@ -1798,16 +1775,6 @@ window.__ModuleLoader__.load({
1798
1775
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1799
1776
  className: "dsm-trae-pool-actions",
1800
1777
  children: [
1801
- pool.checkinSupported === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1802
- type: "button",
1803
- className: "dsm-btn dsm-btn-outline dsm-trae-pool-action-checkin",
1804
- disabled: busy !== void 0 || !writable || selected.size === 0 || deviceClaimed,
1805
- title: deviceClaimed ? t("row.checkinDeviceSpent") : void 0,
1806
- onClick: () => {
1807
- runCheckin();
1808
- },
1809
- children: busy === "checkin" ? t("row.poolCheckingIn") : t("row.poolCheckinAll")
1810
- }) : null,
1811
1778
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1812
1779
  type: "button",
1813
1780
  className: "dsm-btn dsm-btn-outline dsm-trae-pool-action-test",
@@ -1818,7 +1785,7 @@ window.__ModuleLoader__.load({
1818
1785
  },
1819
1786
  children: busy === "test" ? t("row.poolTesting") : t("row.poolTestAll")
1820
1787
  }),
1821
- busy === "checkin" || busy === "test" || testing !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1788
+ busy === "test" || testing !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1822
1789
  className: "dsm-trae-pool-busy",
1823
1790
  role: "status",
1824
1791
  children: t("row.poolBusyHint")
@@ -3086,21 +3053,18 @@ window.__ModuleLoader__.load({
3086
3053
  const [open, setOpen] = (0, react.useState)(view === "page");
3087
3054
  /** The region whose tab is on screen; each tab is its own provider stack. */
3088
3055
  const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
3089
- /** Last-known usage per region, so tab dots survive tab switches. */
3090
- const [statusByRegion, setStatusByRegion] = (0, react.useState)({
3091
- cn: {
3092
- status: "signed-out",
3093
- accounts: []
3094
- },
3095
- enterprise: {
3096
- status: "signed-out",
3097
- accounts: []
3098
- },
3099
- ai: {
3100
- status: "signed-out",
3101
- accounts: []
3102
- }
3103
- });
3056
+ /**
3057
+ * Last-known usage per region, so tab dots survive tab switches.
3058
+ *
3059
+ * A region is ABSENT until something has been read for it (issue #77). It used
3060
+ * to be seeded with a signed-out document, which meant the whole wait for the
3061
+ * first paint rendered as "not signed in — sign in again / scan for accounts"
3062
+ * to a user who was perfectly signed in. The difference between "nothing known
3063
+ * yet" and "known to be signed out" is the difference between a loading line
3064
+ * and a wrong instruction, so the map starts empty and the render below says
3065
+ * which one it is.
3066
+ */
3067
+ const [statusByRegion, setStatusByRegion] = (0, react.useState)({});
3104
3068
  const [busy, setBusy] = (0, react.useState)(false);
3105
3069
  /**
3106
3070
  * Availability verdicts from the one-click test, per region, keyed by model
@@ -3373,10 +3337,54 @@ window.__ModuleLoader__.load({
3373
3337
  return;
3374
3338
  }
3375
3339
  }, [t]);
3340
+ /**
3341
+ * Paint the fast document (issue #77).
3342
+ *
3343
+ * The full document above cannot answer before the Host has read every member's
3344
+ * balance — one upstream round-trip per account, and this panel's pool assembly
3345
+ * happens to walk the membership more than once. That is the wait the issue
3346
+ * reports: tens of seconds in which the card used to show nothing at all.
3347
+ *
3348
+ * The fast document is built from local state only (no upstream read at all), so
3349
+ * it answers in milliseconds and carries everything needed to DRAW: the bound
3350
+ * account, its expiry, the model list and the pool rows. The live figures it
3351
+ * omits (credits, check-in column) are exactly the values that need the round
3352
+ * trip, and the card already renders their absence as `—` / unknown.
3353
+ *
3354
+ * Deliberately separate from {@link refreshUsage} rather than a parameter of it:
3355
+ * a first paint never writes an error state (a slow upstream is not the user's
3356
+ * problem until the full document says so), and it must never downgrade a region
3357
+ * that has already been painted in full.
3358
+ */
3359
+ const refreshUsageFast = (0, react.useCallback)(async (region, signal) => {
3360
+ try {
3361
+ const response = await fetch(withTraeRegionFastDetail(TRAE_USAGE_PATH, region), {
3362
+ headers: { accept: "application/json" },
3363
+ credentials: "same-origin",
3364
+ ...signal === void 0 ? {} : { signal }
3365
+ });
3366
+ if (!response.ok) return;
3367
+ const usage = await response.json().catch(() => void 0);
3368
+ if (usage === void 0) return;
3369
+ if (!mounted.current || signal?.aborted === true) return;
3370
+ setStatusByRegion((prev) => {
3371
+ const previous = prev[region];
3372
+ if (previous !== void 0 && previous.status === "signed-in") return prev;
3373
+ return {
3374
+ ...prev,
3375
+ [region]: usage
3376
+ };
3377
+ });
3378
+ } catch {}
3379
+ }, []);
3376
3380
  (0, react.useEffect)(() => {
3377
3381
  if (!open || !activeRegionOn) return;
3378
3382
  const controller = new AbortController();
3379
- refreshUsage(activeRegion, controller.signal);
3383
+ (async () => {
3384
+ await refreshUsageFast(activeRegion, controller.signal);
3385
+ if (controller.signal.aborted) return;
3386
+ refreshUsage(activeRegion, controller.signal);
3387
+ })();
3380
3388
  return () => {
3381
3389
  controller.abort();
3382
3390
  };
@@ -3384,9 +3392,19 @@ window.__ModuleLoader__.load({
3384
3392
  open,
3385
3393
  activeRegion,
3386
3394
  activeRegionOn,
3387
- refreshUsage
3395
+ refreshUsage,
3396
+ refreshUsageFast
3388
3397
  ]);
3389
- const status = statusByRegion[activeRegion] ?? {
3398
+ /**
3399
+ * The document actually on screen for this region, and whether one has arrived.
3400
+ *
3401
+ * `loaded === undefined` is the only state that may render the loading line: a
3402
+ * signed-out document is a real answer (the user must sign in), never a stand-in
3403
+ * for "still fetching".
3404
+ */
3405
+ const loaded = statusByRegion[activeRegion];
3406
+ const loading = loaded === void 0;
3407
+ const status = loaded ?? {
3390
3408
  status: "signed-out",
3391
3409
  accounts: []
3392
3410
  };
@@ -4072,7 +4090,7 @@ window.__ModuleLoader__.load({
4072
4090
  children: view.label
4073
4091
  });
4074
4092
  };
4075
- const label = status.status === "signed-in" ? t("row.signedIn", { accountName: status.accountName }) : status.status === "error" ? t("row.requestFailed") : t("row.signedOut");
4093
+ const label = loading ? t("row.accountsLoading") : status.status === "signed-in" ? t("row.signedIn", { accountName: status.accountName }) : status.status === "error" ? t("row.requestFailed") : t("row.signedOut");
4076
4094
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
4077
4095
  className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
4078
4096
  children: [
@@ -4707,7 +4725,12 @@ window.__ModuleLoader__.load({
4707
4725
  ]
4708
4726
  })
4709
4727
  ] }) : null,
4710
- status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4728
+ status.status === "signed-out" && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4729
+ className: "dsm-trae-usage-text",
4730
+ role: "status",
4731
+ children: t("row.accountsLoading")
4732
+ }) }) : null,
4733
+ status.status === "signed-out" && !loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4711
4734
  className: "dsm-trae-usage-text",
4712
4735
  children: status.message ?? t("row.signedOutHint")
4713
4736
  }), status.searched && status.searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
@@ -5521,6 +5544,7 @@ window.__ModuleLoader__.load({
5521
5544
  "row.transferNoToken": "no token, will be skipped",
5522
5545
  "row.accountsRescan": "Detect accounts again",
5523
5546
  "row.accountsScanning": "Detecting…",
5547
+ "row.accountsLoading": "Loading your accounts…",
5524
5548
  "row.accountsNone": "This region reports no usable account.",
5525
5549
  "row.accountCurrentBadge": "in use",
5526
5550
  "row.accountSwitch": "Switch",
@@ -5615,7 +5639,6 @@ window.__ModuleLoader__.load({
5615
5639
  "row.poolCurrentBadge": "serving",
5616
5640
  "row.poolMember": "in pool",
5617
5641
  "row.poolNotMember": "not in pool",
5618
- "row.poolCheckinAll": "Check in all accounts",
5619
5642
  "row.poolBusyHint": "Working through accounts one at a time…",
5620
5643
  "row.poolTestAll": "Test all accounts",
5621
5644
  "row.poolTesting": "Testing…",
@@ -5658,7 +5681,6 @@ window.__ModuleLoader__.load({
5658
5681
  "row.poolTargetStaleClear": "Back to automatic",
5659
5682
  "row.poolTargetNone": "No free model available for this panel",
5660
5683
  "row.poolTargetNoneHint": "The pool can only pick a free model; none resolves right now, so tests are disabled.",
5661
- "row.poolActionNoteCheckinDevice": "This machine already used today's check-in (Trae allows one per device per day) — check-in is unavailable until tomorrow.",
5662
5684
  "row.poolActionNoteTestNone": "No free model resolves in this panel — pick a target model before testing.",
5663
5685
  "row.poolActionNoteNoMembers": "Tick the accounts you want in the pool and save first.",
5664
5686
  "row.poolTargetAutoOption": "Automatic (free model)",
@@ -5683,12 +5705,7 @@ window.__ModuleLoader__.load({
5683
5705
  "row.poolLogTitle": "Activity",
5684
5706
  "row.poolLogClear": "Clear",
5685
5707
  "row.poolLogEmpty": "No pool activity yet.",
5686
- "row.poolLogCheckinStart": "Check-in started for {count} account(s).",
5687
5708
  "row.poolLogTestStart": "Test started for {count} account(s) against {model}.",
5688
- "row.poolLogCheckinClaimed": "{accountName} checked in.",
5689
- "row.poolLogCheckinAlready": "{accountName} already checked in today.",
5690
- "row.poolLogCheckinFailed": "{accountName} check-in failed: {message}",
5691
- "row.poolLogCheckinDone": "Check-in finished.",
5692
5709
  "row.poolLogTestRow": "{accountName}: {outcome}.",
5693
5710
  "row.poolLogTestRowMalformed": "{accountName}: malformed result.",
5694
5711
  "row.poolLogTestDone": "Test finished.",
@@ -5741,7 +5758,6 @@ window.__ModuleLoader__.load({
5741
5758
  "row.poolErrNoFreeModel": "no free model is available for this panel",
5742
5759
  "row.poolErrConflict": "the pool refused the request",
5743
5760
  "row.poolErrGeneric": "pool request failed (HTTP {status})",
5744
- "row.poolCheckingIn": "Checking in…",
5745
5761
  "row.poolSettingsHint": "The switch and the target model are preferences — save them. Test, check in and delete act at once.",
5746
5762
  "row.poolLogSaved": "Pool settings saved.",
5747
5763
  "row.poolLogSaveFailed": "Could not save the pool settings.",
@@ -5850,6 +5866,7 @@ window.__ModuleLoader__.load({
5850
5866
  "row.transferNoToken": "无 Token,将被跳过",
5851
5867
  "row.accountsRescan": "重新检测账号",
5852
5868
  "row.accountsScanning": "正在检测…",
5869
+ "row.accountsLoading": "正在加载账号…",
5853
5870
  "row.accountsNone": "该区域没有可列出的账号。",
5854
5871
  "row.accountCurrentBadge": "当前使用",
5855
5872
  "row.accountSwitch": "切换",
@@ -5944,7 +5961,6 @@ window.__ModuleLoader__.load({
5944
5961
  "row.poolCurrentBadge": "派单中",
5945
5962
  "row.poolMember": "已在池中",
5946
5963
  "row.poolNotMember": "未入池",
5947
- "row.poolCheckinAll": "一键签到所有账号",
5948
5964
  "row.poolBusyHint": "逐个处理账号中…",
5949
5965
  "row.poolTestAll": "一键测试所有账号",
5950
5966
  "row.poolTesting": "测速中…",
@@ -5987,7 +6003,6 @@ window.__ModuleLoader__.load({
5987
6003
  "row.poolTargetStaleClear": "改回自动",
5988
6004
  "row.poolTargetNone": "本面板暂无可用免费模型",
5989
6005
  "row.poolTargetNoneHint": "账号池只会挑免费模型;当前无法解析出可用模型,测速暂被禁用。",
5990
- "row.poolActionNoteCheckinDevice": "本机今日的签到已用掉(Trae 按每台设备每天一次限制),明天才能再签。",
5991
6006
  "row.poolActionNoteTestNone": "本面板没有解析出免费模型——先指定一个目标模型再测速。",
5992
6007
  "row.poolActionNoteNoMembers": "先勾选要入池的账号并保存。",
5993
6008
  "row.poolTargetAutoOption": "自动(免费模型)",
@@ -6012,12 +6027,7 @@ window.__ModuleLoader__.load({
6012
6027
  "row.poolLogTitle": "活动记录",
6013
6028
  "row.poolLogClear": "清空",
6014
6029
  "row.poolLogEmpty": "暂无账号池活动。",
6015
- "row.poolLogCheckinStart": "开始为 {count} 个账号签到。",
6016
6030
  "row.poolLogTestStart": "开始为 {count} 个账号测速({model})。",
6017
- "row.poolLogCheckinClaimed": "{accountName} 已签到。",
6018
- "row.poolLogCheckinAlready": "{accountName} 今日已签到。",
6019
- "row.poolLogCheckinFailed": "{accountName} 签到失败:{message}",
6020
- "row.poolLogCheckinDone": "签到完成。",
6021
6031
  "row.poolLogTestRow": "{accountName}:{outcome}。",
6022
6032
  "row.poolLogTestRowMalformed": "{accountName}:结果异常。",
6023
6033
  "row.poolLogTestDone": "测速完成。",
@@ -6070,7 +6080,6 @@ window.__ModuleLoader__.load({
6070
6080
  "row.poolErrNoFreeModel": "本面板没有可用的免费模型",
6071
6081
  "row.poolErrConflict": "账号池拒绝了该请求",
6072
6082
  "row.poolErrGeneric": "账号池请求失败(HTTP {status})",
6073
- "row.poolCheckingIn": "签到中…",
6074
6083
  "row.poolSettingsHint": "开关与目标模型是偏好,改完要保存;行内的测速、签到与删除立即生效。",
6075
6084
  "row.poolLogSaved": "账号池设置已保存。",
6076
6085
  "row.poolLogSaveFailed": "无法保存账号池设置。",
package/lib/index.d.ts CHANGED
@@ -3459,6 +3459,31 @@ interface TraePoolDeps {
3459
3459
  * Only the status document's own assembly and an explicit batch call it.
3460
3460
  */
3461
3461
  members(panel: TraePanel): Promise<readonly TraePoolMember[]>;
3462
+ /**
3463
+ * One panel's pool membership from LOCAL state only — the first-paint source
3464
+ * (issue #77).
3465
+ *
3466
+ * Cheap by contract in the strongest sense: no upstream read at all, so the
3467
+ * account table can be painted before any billing endpoint answers. A member
3468
+ * whose credit total has never been measured carries no `credits`, which the
3469
+ * card already renders as `—` rather than as a wrong figure; the follow-up
3470
+ * full document fills the real numbers in.
3471
+ *
3472
+ * A Host built without it degrades to omitting the pool block from a
3473
+ * first-paint document rather than paying {@link members} there — the card
3474
+ * then shows its single-account view for one refresh cycle.
3475
+ */
3476
+ localMembers?(panel: TraePanel): Promise<readonly TraePoolMember[]>;
3477
+ /**
3478
+ * The panel's saved member names, from a local scan only.
3479
+ *
3480
+ * The first-paint counterpart of {@link memberNames}. The full document builds
3481
+ * these by walking {@link members} (which reads every balance), which is a
3482
+ * second full membership pass for a map of names; the fast document must not
3483
+ * pay that, and it does not need to: the saved-list rows it renders are the
3484
+ * ones {@link localMembers} already names.
3485
+ */
3486
+ localMemberNames?(panel: TraePanel): Promise<Record<string, string>>;
3462
3487
  /**
3463
3488
  * The checked ids that still resolve to a local sign-in, from a local scan only.
3464
3489
  * The batch route's empty-pool guard runs this on every request, so it must not
@@ -3482,6 +3507,16 @@ interface TraePoolDeps {
3482
3507
  * TODAY because this machine already spent its claim on another account.
3483
3508
  */
3484
3509
  checkinState?(panel: TraePanel): Promise<Record<string, TraePoolCheckinState>>;
3510
+ /**
3511
+ * Today's check-in state from the cells already measured this process — the
3512
+ * first-paint counterpart (issue #77) of {@link checkinState}.
3513
+ *
3514
+ * Synchronous by contract: it reads the Host's measurement cache and nothing
3515
+ * else, so it must never start a request. A Host without it degrades to an
3516
+ * unknown check-in column in the first paint, which the card already renders
3517
+ * as unknown; the full document fills the column in.
3518
+ */
3519
+ cachedCheckinState?(panel: TraePanel): Readonly<Record<string, TraePoolCheckinState>>;
3485
3520
  /** Run the check-in batch. Persists nothing to settings; drops its own caches. */
3486
3521
  checkin?(panel: TraePanel): Promise<readonly TraePoolCheckinRow[]>;
3487
3522
  /** Run the model-availability batch and persist its measurements. */
@@ -3500,6 +3535,19 @@ interface TraePoolCheckinState {
3500
3535
  /** Whether this account is offered the reward at all; false renders `disabled`. */
3501
3536
  enabled?: boolean;
3502
3537
  }
3538
+ /**
3539
+ * Which usage document a request asked for (issue #77).
3540
+ *
3541
+ * `full` is the document every earlier caller has always received: live credit
3542
+ * figures, fresh check-in state, the ranked pool. `fast` is the same document
3543
+ * built from LOCAL state only — no upstream read at all — so the card can paint
3544
+ * the account table immediately and then upgrade it with the full document.
3545
+ *
3546
+ * The distinction lives in ONE optional query parameter (`detail`) so an old
3547
+ * caller, a hand-written curl and a browser refresh all keep working: an absent
3548
+ * or unrecognised value is `full`.
3549
+ */
3550
+ type TraeStatusDetail = 'full' | 'fast';
3503
3551
  /**
3504
3552
  * Assemble one region's card document. `panel` is the tab the card is on; the
3505
3553
  * panel-scoped store already answers with only that region's accounts, so the
@@ -3507,7 +3555,9 @@ interface TraePoolCheckinState {
3507
3555
  * Sign-in state is read-only; credit is a live billing answer whose failure
3508
3556
  * degrades to `creditsError` rather than failing the whole document.
3509
3557
  */
3510
- declare function traeWebUsage(deps: TraeUsageRouteOptions, panel: TraePanel): Promise<TraeWebUsage>;
3558
+ declare function traeWebUsage(deps: TraeUsageRouteOptions, panel: TraePanel, options?: {
3559
+ detail?: TraeStatusDetail;
3560
+ }): Promise<TraeWebUsage>;
3511
3561
  /** Mount the GET usage route on an optional webServer context. */
3512
3562
  declare function registerTraeUsageRoute(ctx: Context$1, deps: TraeUsageRouteOptions): void;
3513
3563
  //#endregion
package/lib/index.js CHANGED
@@ -1089,6 +1089,23 @@ function regionOfTraeStatusUrl(url) {
1089
1089
  if (value === null || value === "") return "cn";
1090
1090
  return TRAE_PANELS.includes(value) ? value : void 0;
1091
1091
  }
1092
+ /** Query parameter asking for a thinner first-paint document (issue #77). */
1093
+ const TRAE_DETAIL_PARAM = "detail";
1094
+ /** The only recognised `detail` value: local state only, no upstream read. */
1095
+ const TRAE_DETAIL_FAST = "fast";
1096
+ /**
1097
+ * Whether a request URL asks for the fast document.
1098
+ *
1099
+ * Only the exact value counts — a typo (`fastest`, `FAST`) degrades to the FULL
1100
+ * document, because a thinner panel is a worse failure than a slower one: the
1101
+ * caller that made the mistake is asking about an optimisation, so the safe
1102
+ * answer is the truthful one.
1103
+ */
1104
+ function fastDetailOf(url) {
1105
+ const at = url.indexOf("?");
1106
+ if (at === -1) return false;
1107
+ return new URLSearchParams(url.slice(at + 1)).get(TRAE_DETAIL_PARAM) === TRAE_DETAIL_FAST;
1108
+ }
1092
1109
  /**
1093
1110
  * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
1094
1111
  * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
@@ -6856,7 +6873,8 @@ function toCheckin(status) {
6856
6873
  * Sign-in state is read-only; credit is a live billing answer whose failure
6857
6874
  * degrades to `creditsError` rather than failing the whole document.
6858
6875
  */
6859
- async function traeWebUsage(deps, panel) {
6876
+ async function traeWebUsage(deps, panel, options = {}) {
6877
+ const fast = options.detail === "fast";
6860
6878
  const store = deps.store(panel);
6861
6879
  const enabled = deps.regionEnabled(panel);
6862
6880
  /**
@@ -6887,7 +6905,8 @@ async function traeWebUsage(deps, panel) {
6887
6905
  }
6888
6906
  let credential;
6889
6907
  try {
6890
- credential = await store.resolve();
6908
+ credential = fast ? await store.current() : await store.resolve();
6909
+ if (credential === void 0) credential = await store.resolve();
6891
6910
  } catch (error) {
6892
6911
  return {
6893
6912
  status: "signed-out",
@@ -6916,7 +6935,16 @@ async function traeWebUsage(deps, panel) {
6916
6935
  enabledModelIds: [...deps.enabledModelIds(panel)],
6917
6936
  ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(panel) }
6918
6937
  };
6919
- const poolSection = await poolSectionOf(deps, panel);
6938
+ if (fast) {
6939
+ const poolSection = await poolSectionOf(deps, panel, true);
6940
+ return {
6941
+ status: "signed-in",
6942
+ ...account,
6943
+ ...poolSection,
6944
+ ...panel === "enterprise" ? { creditsUnavailable: "enterprise" } : {}
6945
+ };
6946
+ }
6947
+ const poolSection = await poolSectionOf(deps, panel, false);
6920
6948
  const client = deps.client(panel);
6921
6949
  if (regionOfPanel(panel) === "ai") try {
6922
6950
  const payStatus = await client.payStatus();
@@ -7009,10 +7037,10 @@ async function creditAlternatives(deps, panel, accounts) {
7009
7037
  * BOUND account, which `traeWebUsage` reports as `status: error` before the pool
7010
7038
  * is even assembled.
7011
7039
  */
7012
- async function poolSectionOf(deps, panel) {
7040
+ async function poolSectionOf(deps, panel, fast = false) {
7013
7041
  if (deps.pool === void 0) return {};
7014
7042
  try {
7015
- const pool = await traeWebPool(deps, panel);
7043
+ const pool = await traeWebPool(deps, panel, fast);
7016
7044
  return pool === void 0 ? {} : { pool };
7017
7045
  } catch {
7018
7046
  return {};
@@ -7030,20 +7058,35 @@ async function poolSectionOf(deps, panel) {
7030
7058
  * Every per-row read is failure-tolerant on purpose: one account whose balance
7031
7059
  * read throws must not remove the other members from the table.
7032
7060
  */
7033
- async function traeWebPool(deps, panel) {
7061
+ async function traeWebPool(deps, panel, fast = false) {
7034
7062
  const pool = deps.pool;
7035
7063
  if (pool === void 0) return void 0;
7036
7064
  const preferences = pool.preferences(panel);
7037
- const members = await pool.members(panel);
7065
+ const members = fast ? await pool.localMembers?.(panel) : await pool.members(panel);
7066
+ if (members === void 0) return void 0;
7038
7067
  const nowMs = Date.now();
7039
7068
  const ranked = rankPool(members, nowMs);
7040
7069
  const target = resolveTargetModel(pool.catalog?.(panel) ?? [], preferences.targetModelId);
7041
7070
  const byId = new Map(members.map((member) => [member.account.id, member]));
7042
7071
  const excludedById = new Map(ranked.flatMap((row) => row.excludedBy === void 0 ? [] : [[row.account.id, row.excludedBy]]));
7043
7072
  const rankedIds = new Set(ranked.map((row) => row.account.id));
7044
- const others = (await pool.otherAccounts?.(panel) ?? []).filter((account) => !rankedIds.has(account.id));
7045
- const checkinState = await pool.checkinState?.(panel).catch(() => ({})) ?? {};
7046
- const memberNames = await pool.memberNames?.(panel).catch(() => ({})) ?? {};
7073
+ /**
7074
+ * ISSUE #77: every dep below is shaped so the first paint can avoid the
7075
+ * upstream. `otherAccounts`, `memberNames` and `checkinState` are all derived
7076
+ * from a FULL membership pass on this Host (one balance read per account), and
7077
+ * three of them in a row is most of the reported wait. The fast document
7078
+ * answers the same questions from the roster it already has:
7079
+ *
7080
+ * - `otherAccounts`: the local roster minus the members already ranked. The
7081
+ * saved-but-unreachable ghosts are added below from `preferences` anyway, so
7082
+ * nothing a row is built from goes missing.
7083
+ * - `memberNames`: only consulted for ghosts, whose rows the fast document
7084
+ * carries from `localMemberNames` when the Host offers it.
7085
+ * - `checkinState`: the measurement cache, read synchronously.
7086
+ */
7087
+ const others = fast ? members.filter((member) => !rankedIds.has(member.account.id)).map((member) => member.account) : (await pool.otherAccounts?.(panel) ?? []).filter((account) => !rankedIds.has(account.id));
7088
+ const checkinState = fast ? { ...pool.cachedCheckinState?.(panel) ?? {} } : await pool.checkinState?.(panel).catch(() => ({})) ?? {};
7089
+ const memberNames = fast ? await pool.localMemberNames?.(panel).catch(() => ({})) ?? {} : await pool.memberNames?.(panel).catch(() => ({})) ?? {};
7047
7090
  const currentAccountId = await pool.currentAccountId?.(panel).catch(() => void 0);
7048
7091
  /**
7049
7092
  * A row per listed account, members first.
@@ -7239,8 +7282,9 @@ function registerTraeUsageRoute(ctx, deps) {
7239
7282
  }
7240
7283
  const panel = requestRegion(req, res);
7241
7284
  if (panel === void 0) return;
7285
+ const detail = fastDetailOf(req.url ?? "") ? "fast" : "full";
7242
7286
  try {
7243
- json(res, 200, await traeWebUsage(deps, panel));
7287
+ json(res, 200, await traeWebUsage(deps, panel, { detail }));
7244
7288
  } catch (error) {
7245
7289
  json(res, 500, { error: safeMessage(error) });
7246
7290
  }
@@ -8543,6 +8587,15 @@ const Config = z.object({
8543
8587
  /** Every region, in card tab order. */
8544
8588
  const REGION_KEYS = ["cn", "ai"];
8545
8589
  /**
8590
+ * How long a measured check-in state stays usable (issue #77).
8591
+ *
8592
+ * The upstream rewards ONE check-in per device per day, so a figure measured
8593
+ * minutes ago cannot have changed; re-reading it is a pure upstream round-trip.
8594
+ * Ten minutes matches the sibling plugin's cache and is comfortably shorter than
8595
+ * a day, so a genuine second check-in still shows up on the next poll after it.
8596
+ */
8597
+ const CHECKIN_CACHE_MS = 6e5;
8598
+ /**
8546
8599
  * What ONE failed real request means for the account that served it (issue #44).
8547
8600
  *
8548
8601
  * The shim only reports account-scoped kinds here, so this table never has to
@@ -8992,6 +9045,24 @@ function apply(ctx, config) {
8992
9045
  enterprise: /* @__PURE__ */ new Map(),
8993
9046
  ai: /* @__PURE__ */ new Map()
8994
9047
  };
9048
+ /**
9049
+ * Today's check-in state per account, measured by the LAST full read.
9050
+ *
9051
+ * Issue #77: the check-in column used to be an upstream call per account with
9052
+ * no memory at all, so every open of the card paid for every member twice (the
9053
+ * pool's own membership pass plus one `checkinStatus()` each). The first-paint
9054
+ * document may not wait for any of that, and re-measuring what was measured
9055
+ * minutes ago buys nothing: a check-in happens at most once per device per day.
9056
+ *
9057
+ * Entries older than {@link CHECKIN_CACHE_MS} are treated as absent rather than
9058
+ * as stale truth, so a document never claims "already checked in" off yesterday's
9059
+ * measurement. A cold cache means "not read", which the card renders as unknown.
9060
+ */
9061
+ const checkinStateCache = {
9062
+ cn: {},
9063
+ enterprise: {},
9064
+ ai: {}
9065
+ };
8995
9066
  const poolCreditsOfAccount = async (panel, accountId) => {
8996
9067
  const credits = await stacks[panel].creditsOfAccount(accountId);
8997
9068
  if (credits === void 0) return void 0;
@@ -9067,6 +9138,24 @@ function apply(ctx, config) {
9067
9138
  }));
9068
9139
  };
9069
9140
  /**
9141
+ * Today's check-in state from the cells already measured — the first-paint read
9142
+ * (issue #77).
9143
+ *
9144
+ * Synchronous and local by contract: it never starts a request, so the card's
9145
+ * first paint cannot be held by the upstream. Only FRESH cells are returned; an
9146
+ * expired entry is dropped rather than reported, because "checked in" from four
9147
+ * hours ago is a claim about a day that may already be over.
9148
+ */
9149
+ function cachedCheckinStateOf(panel) {
9150
+ const nowMs = Date.now();
9151
+ const state = {};
9152
+ for (const [accountId, entry] of Object.entries(checkinStateCache[panel])) {
9153
+ if (nowMs - entry.atMs >= CHECKIN_CACHE_MS) continue;
9154
+ state[accountId] = entry.state;
9155
+ }
9156
+ return state;
9157
+ }
9158
+ /**
9070
9159
  * Pool members from LOCAL state only: the store's logins, the saved probes and
9071
9160
  * the cached credit totals. No upstream call, so it is safe on the request
9072
9161
  * path (see {@link poolCreditCache} for why the cache exists).
@@ -9291,7 +9380,8 @@ function apply(ctx, config) {
9291
9380
  checkinSupported: (stored) => regionOfPanel(stored) === "cn",
9292
9381
  checkin: async (stored) => checkinAllAccounts(await poolTargetsOf(stored), probeDeps()),
9293
9382
  checkinState: async (stored) => {
9294
- const state = {};
9383
+ const state = cachedCheckinStateOf(stored);
9384
+ const nowMs = Date.now();
9295
9385
  for (const target of await poolTargetsOf(stored)) {
9296
9386
  const credential = await stacks[stored].store.resolveCredentialOf(target.accountId);
9297
9387
  if (credential === void 0) continue;
@@ -9301,15 +9391,21 @@ function apply(ctx, config) {
9301
9391
  });
9302
9392
  try {
9303
9393
  const status = await client.checkinStatus();
9304
- state[target.accountId] = {
9394
+ const measured = {
9305
9395
  checkedIn: status.checkedIn,
9306
9396
  deviceCheckedIn: status.didCheckedIn,
9307
9397
  enabled: status.enabled
9308
9398
  };
9399
+ state[target.accountId] = measured;
9400
+ checkinStateCache[stored][target.accountId] = {
9401
+ state: measured,
9402
+ atMs: nowMs
9403
+ };
9309
9404
  } catch {}
9310
9405
  }
9311
9406
  return state;
9312
9407
  },
9408
+ cachedCheckinState: (stored) => cachedCheckinStateOf(stored),
9313
9409
  test: async (stored, modelId, onRow) => {
9314
9410
  await warmWireMap(stored);
9315
9411
  const probeInputTokens = probeInputTokensOf(stored);
@@ -9409,6 +9505,12 @@ function apply(ctx, config) {
9409
9505
  pool: {
9410
9506
  preferences: (panel) => poolPreferencesOf(panel),
9411
9507
  members: (panel) => poolMembersOf(panel),
9508
+ localMembers: (panel) => localPoolMembers(panel),
9509
+ localMemberNames: async (panel) => {
9510
+ const names = {};
9511
+ for (const member of await localPoolMembers(panel)) names[member.account.id] = member.account.accountName;
9512
+ return names;
9513
+ },
9412
9514
  effectiveMemberAccountIds: async (panel) => {
9413
9515
  return effectiveMembersOf(poolPreferencesOf(panel).memberAccountIds ?? [], new Set((await poolMembersOf(panel)).map((member) => member.account.id)));
9414
9516
  },
@@ -9423,6 +9525,7 @@ function apply(ctx, config) {
9423
9525
  checkinSupported: (panel) => regionOfPanel(panel) === "cn",
9424
9526
  checkin: (panel) => poolDepsOf(panel).checkin?.(panel) ?? Promise.resolve([]),
9425
9527
  checkinState: (panel) => poolDepsOf(panel).checkinState?.(panel) ?? Promise.resolve({}),
9528
+ cachedCheckinState: (panel) => cachedCheckinStateOf(panel),
9426
9529
  test: (panel, modelId, onRow) => poolDepsOf(panel).test?.(panel, modelId, onRow) ?? Promise.resolve([]),
9427
9530
  testOne: (panel, accountId) => poolDepsOf(panel).testOne?.(panel, accountId) ?? Promise.resolve(void 0),
9428
9531
  removeOne: (panel, accountId) => poolDepsOf(panel).removeOne?.(panel, accountId) ?? Promise.resolve(false)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
4
  "description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
5
- "version": "3.10.0",
5
+ "version": "3.10.2",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",