dsh-connect-trae 3.10.1 → 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 +35 -0
- package/lib/client.js +94 -20
- package/lib/index.d.ts +51 -1
- package/lib/index.js +116 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
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
|
+
|
|
3
38
|
## 3.10.1 (2026-10-19)
|
|
4
39
|
|
|
5
40
|
> 「一键签到所有账号」下线。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}).
|
|
@@ -3037,21 +3053,18 @@ window.__ModuleLoader__.load({
|
|
|
3037
3053
|
const [open, setOpen] = (0, react.useState)(view === "page");
|
|
3038
3054
|
/** The region whose tab is on screen; each tab is its own provider stack. */
|
|
3039
3055
|
const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
|
|
3040
|
-
/**
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
accounts: []
|
|
3053
|
-
}
|
|
3054
|
-
});
|
|
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)({});
|
|
3055
3068
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
3056
3069
|
/**
|
|
3057
3070
|
* Availability verdicts from the one-click test, per region, keyed by model
|
|
@@ -3324,10 +3337,54 @@ window.__ModuleLoader__.load({
|
|
|
3324
3337
|
return;
|
|
3325
3338
|
}
|
|
3326
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
|
+
}, []);
|
|
3327
3380
|
(0, react.useEffect)(() => {
|
|
3328
3381
|
if (!open || !activeRegionOn) return;
|
|
3329
3382
|
const controller = new AbortController();
|
|
3330
|
-
|
|
3383
|
+
(async () => {
|
|
3384
|
+
await refreshUsageFast(activeRegion, controller.signal);
|
|
3385
|
+
if (controller.signal.aborted) return;
|
|
3386
|
+
refreshUsage(activeRegion, controller.signal);
|
|
3387
|
+
})();
|
|
3331
3388
|
return () => {
|
|
3332
3389
|
controller.abort();
|
|
3333
3390
|
};
|
|
@@ -3335,9 +3392,19 @@ window.__ModuleLoader__.load({
|
|
|
3335
3392
|
open,
|
|
3336
3393
|
activeRegion,
|
|
3337
3394
|
activeRegionOn,
|
|
3338
|
-
refreshUsage
|
|
3395
|
+
refreshUsage,
|
|
3396
|
+
refreshUsageFast
|
|
3339
3397
|
]);
|
|
3340
|
-
|
|
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 ?? {
|
|
3341
3408
|
status: "signed-out",
|
|
3342
3409
|
accounts: []
|
|
3343
3410
|
};
|
|
@@ -4023,7 +4090,7 @@ window.__ModuleLoader__.load({
|
|
|
4023
4090
|
children: view.label
|
|
4024
4091
|
});
|
|
4025
4092
|
};
|
|
4026
|
-
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");
|
|
4027
4094
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4028
4095
|
className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
|
|
4029
4096
|
children: [
|
|
@@ -4658,7 +4725,12 @@ window.__ModuleLoader__.load({
|
|
|
4658
4725
|
]
|
|
4659
4726
|
})
|
|
4660
4727
|
] }) : null,
|
|
4661
|
-
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
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", {
|
|
4662
4734
|
className: "dsm-trae-usage-text",
|
|
4663
4735
|
children: status.message ?? t("row.signedOutHint")
|
|
4664
4736
|
}), status.searched && status.searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
@@ -5472,6 +5544,7 @@ window.__ModuleLoader__.load({
|
|
|
5472
5544
|
"row.transferNoToken": "no token, will be skipped",
|
|
5473
5545
|
"row.accountsRescan": "Detect accounts again",
|
|
5474
5546
|
"row.accountsScanning": "Detecting…",
|
|
5547
|
+
"row.accountsLoading": "Loading your accounts…",
|
|
5475
5548
|
"row.accountsNone": "This region reports no usable account.",
|
|
5476
5549
|
"row.accountCurrentBadge": "in use",
|
|
5477
5550
|
"row.accountSwitch": "Switch",
|
|
@@ -5793,6 +5866,7 @@ window.__ModuleLoader__.load({
|
|
|
5793
5866
|
"row.transferNoToken": "无 Token,将被跳过",
|
|
5794
5867
|
"row.accountsRescan": "重新检测账号",
|
|
5795
5868
|
"row.accountsScanning": "正在检测…",
|
|
5869
|
+
"row.accountsLoading": "正在加载账号…",
|
|
5796
5870
|
"row.accountsNone": "该区域没有可列出的账号。",
|
|
5797
5871
|
"row.accountCurrentBadge": "当前使用",
|
|
5798
5872
|
"row.accountSwitch": "切换",
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
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
|
-
|
|
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.
|
|
5
|
+
"version": "3.10.2",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|