dsh-qoder-connect 0.2.3 → 0.2.5
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.en.md +1 -1
- package/README.md +1 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +294 -150
- package/lib/index.d.ts +95 -13
- package/lib/index.js +6 -1
- package/lib/{variants-BgysmR2W.js → variants-Dus_Yq3R.js} +183 -21
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -19,7 +19,7 @@ Configure only what you use: a variant with no saved PAT shows no model group an
|
|
|
19
19
|
- **One-toggle context window** — each variant card's *Context window* tab lists every model's capacity (the default and the largest declared window) and carries its own *“Use the largest declared context window”* preference (on by default): on, requests declare the maximum (e.g. Qwen3.8-Max at 1M); off, the default (200K). The two variants keep independent toggle state.
|
|
20
20
|
- **Model visibility toggles & batch controls** — each variant card provides a dedicated *Model Toggles* tab (between *Context window* and *Credit details*) to freely enable or hide individual models; disabled models are hidden from the DSH model picker, keeping it tidy; includes instant search by name/ID and one-click *Enable all* / *Disable all* batch operations.
|
|
21
21
|
- **Daily auto check-in & logs** — claims the daily 100 Credits automatically, **at a time you can set per variant on the card** (10:00 UTC+8 by default, the upstream's reset moment), with independent toggles and startup catch-up protection. The card features a dedicated *Check-in log* panel with audit trails, *Check in now*, *Refresh*, and *Clear logs* quick actions.
|
|
22
|
-
- **Sidebar quota card + credit details** — the *“Qoder sidebar display”* card toggles a per-variant sidebar quota widget (off by default; each toggle needs its variant's PAT saved) with one shared refresh interval (default 5 min, minimum 1 min). Clicking the sidebar widget opens the *Qoder quota* panel: one row per credit package — *“Remaining / Total + bar | Expires”* —
|
|
22
|
+
- **Sidebar quota card + credit details** — the *“Qoder sidebar display”* card toggles a per-variant sidebar quota widget (off by default; each toggle needs its variant's PAT saved) with one shared refresh interval (default 5 min, minimum 1 min). Clicking the sidebar widget opens the *Qoder quota* panel: one row per credit package — *“Remaining / Total + bar | Expires”* — where a campaign grant (the international edition’s *SOTA Exclusive Credits*, for one) keeps its own upstream-given title and **its own expiry**. When the account reports no overall deadline (the upstream’s year-9999 sentinel), the panel states when the PAT was authorized rather than a fabricated date. Click the same widget again to close; click the other to switch variants.
|
|
23
23
|
- **Rate display `x<priceFactor>`** — each model name is suffixed with the upstream-reported price multiplier (e.g. `Some Model · x0.79`, free is `x0`), spelled `x<n>` from the catalog's `price_factor`. Display only — it never changes the request; models whose rate the upstream did not report simply show no suffix.
|
|
24
24
|
|
|
25
25
|
## Screenshots
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- **上下文窗口一键切换**:两张变体卡片的「上下文窗口」页列出每个模型的容量(默认值与上游声明的最大值),并各自提供「使用上游声明的最大上下文窗口」开关(默认勾选):勾选按最大窗口(如 Qwen3.8-Max 的 1M)发请求,不勾选按默认窗口(200K),两版开关状态独立保存、互不影响。
|
|
20
20
|
- **模型自由开关与批量管理**:两张变体卡片均提供「模型开关」页(位于上下文窗口与额度明细之间),可自由开启或关闭指定模型;关闭的模型将自动从 DeepSeek Harness 的模型选择器中隐藏,避免误选与列表冗长;顶部提供按名称/ID的即时搜索框,并支持全选与一键「批量开启」/「批量关闭」。
|
|
21
21
|
- **每日自动签到与日志明细**:支持每天自动签到领取 100 Credits 算力额度,**签到时刻可在卡片上自定义**(默认 10:00,即上游刷新时间,按 UTC+8),配备双端开关与开机防漏补签机制;卡片常驻「签到日志」面板,提供执行流水记录与「立即签到」、「刷新」、「清空日志」快捷操作。
|
|
22
|
-
- **侧栏额度展示 + 额度明细**:「Qoder 侧栏展示」卡片可分别为两版开启侧栏底部的额度小卡(默认关,开启前需为该变体保存 PAT),共享一个刷新间隔(默认 5 分钟,最小 1 分钟)。点侧栏小卡在中间面板打开「Qoder 额度」明细:按资源包逐行给出「剩余 / 总量 +
|
|
22
|
+
- **侧栏额度展示 + 额度明细**:「Qoder 侧栏展示」卡片可分别为两版开启侧栏底部的额度小卡(默认关,开启前需为该变体保存 PAT),共享一个刷新间隔(默认 5 分钟,最小 1 分钟)。点侧栏小卡在中间面板打开「Qoder 额度」明细:按资源包逐行给出「剩余 / 总量 + 进度条|到期时间」,活动赠送包(如国际版「SOTA 专属积分」)按上游给定的名称与**各自的到期日**单独成行;账号整体无到期(上游回 9999 占位值)时展示 PAT 的授权时间而非假日期;再点同一张卡关闭,点另一张切换到对应版本。
|
|
23
23
|
- **倍率显示 `x<priceFactor>`**:模型名后直接拼上游报价的价格倍率(如 `某模型 · x0.79`,免费为 `x0`),来自目录接口的 `price_factor` 字段,仅作展示、不影响请求;上游未报倍率的模型不显示后缀,不会凭空补一个数。
|
|
24
24
|
|
|
25
25
|
## 界面一览
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as QoderCatalogStore, O as qoderCatalogPath, U as qoderCredentialIdentity, V as QoderCredentialStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, k as FALLBACK_QODER_MODELS, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-
|
|
2
|
+
import { D as QoderCatalogStore, O as qoderCatalogPath, U as qoderCredentialIdentity, V as QoderCredentialStore, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, k as FALLBACK_QODER_MODELS, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey } from "./variants-Dus_Yq3R.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
package/lib/client.js
CHANGED
|
@@ -40,6 +40,227 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
const QODER_GLOBAL_PROBE_PATH = "/plugins/dsh-qoder-connect/global/probe";
|
|
41
41
|
const QODER_GLOBAL_AUTH_PATH = "/plugins/dsh-qoder-connect/global/auth";
|
|
42
42
|
//#endregion
|
|
43
|
+
//#region src/client/quota-merge.ts
|
|
44
|
+
/**
|
|
45
|
+
* The active UI language, kept here because the sidebar card and the dashboard
|
|
46
|
+
* label the SAME package and must not pick different words for it.
|
|
47
|
+
*
|
|
48
|
+
* The client entry mirrors `ctx.locale` into this one setter; a build or a test
|
|
49
|
+
* that never calls it leaves `zh` as the answer, which is the language the
|
|
50
|
+
* plugin's own copy and its Chinese-first users read.
|
|
51
|
+
*/
|
|
52
|
+
let activeLanguage = "zh";
|
|
53
|
+
/** Record the host's active locale; anything but `en` selects the Chinese title. */
|
|
54
|
+
function setActiveLanguage(localeId) {
|
|
55
|
+
activeLanguage = typeof localeId === "string" && localeId.toLowerCase().startsWith("en") ? "en" : "zh";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The label one credit account renders with.
|
|
59
|
+
*
|
|
60
|
+
* `packageName` is the host's single-language best guess (the upstream sends a
|
|
61
|
+
* localized title only for campaign grants). Where a title map exists, the
|
|
62
|
+
* active language wins and the other language backstops it, so an English UI
|
|
63
|
+
* says "SOTA Exclusive Credits" and a Chinese one says "SOTA 专属积分" about one
|
|
64
|
+
* package — never the campaign code the upstream `name` field carries.
|
|
65
|
+
*/
|
|
66
|
+
function accountDisplayName(account, language = activeLanguage) {
|
|
67
|
+
const titles = account.packageNameI18n;
|
|
68
|
+
if (titles === void 0) return account.packageName;
|
|
69
|
+
const other = language === "en" ? "zh" : "en";
|
|
70
|
+
return titles[language] ?? titles[other] ?? account.packageName;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format an upstream expiry instant the way every quota surface prints dates.
|
|
74
|
+
*
|
|
75
|
+
* The card once printed the raw ISO string ("2026-10-19T02:00:00.000Z") in a
|
|
76
|
+
* date column: unreadable, and in UTC at that — the day a user's grant ends is
|
|
77
|
+
* the day on their clock, so the render is localized rather than verbatim. An
|
|
78
|
+
* unparseable value stays verbatim rather than becoming a made-up date.
|
|
79
|
+
*/
|
|
80
|
+
function formatExpiryText(value) {
|
|
81
|
+
if (value === void 0) return void 0;
|
|
82
|
+
const parsed = Date.parse(value);
|
|
83
|
+
if (Number.isNaN(parsed)) return value;
|
|
84
|
+
return new Intl.DateTimeFormat(hostLocale(), { dateStyle: "medium" }).format(new Date(parsed));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Format an instant with its time of day, in the language the cards are
|
|
88
|
+
* already using. The PAT's authorization line needs the clock time as well as
|
|
89
|
+
* the day; a package's expiry line needs only the day.
|
|
90
|
+
*/
|
|
91
|
+
function formatStampText(atMs) {
|
|
92
|
+
return new Intl.DateTimeFormat(hostLocale(), {
|
|
93
|
+
dateStyle: "medium",
|
|
94
|
+
timeStyle: "short"
|
|
95
|
+
}).format(new Date(atMs));
|
|
96
|
+
}
|
|
97
|
+
/** The BCP-47 tag the active language formats dates with. */
|
|
98
|
+
function hostLocale() {
|
|
99
|
+
return activeLanguage === "en" ? "en-US" : "zh-CN";
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Group credit accounts by (packageName, packageEndTime) and sum each group.
|
|
103
|
+
*
|
|
104
|
+
* A missing figure counts as 0 (the user's ruling): a package whose total the
|
|
105
|
+
* upstream did not report contributes nothing to the group's `size` rather
|
|
106
|
+
* than poisoning the bar into "unknown". A group whose summed `size` is still
|
|
107
|
+
* 0 keeps an honest "unknown total" rendering in the card — the merge never
|
|
108
|
+
* invents a denominator. `unlimited` is sticky: one unlimited member makes the
|
|
109
|
+
* whole group unlimited, and its sums are not displayed as a quota.
|
|
110
|
+
*
|
|
111
|
+
* MERGE KEY IS THE NAME ALONE (the user's correction): same-named packages
|
|
112
|
+
* whose expiries differ by seconds (stacked purchase batches) must still be
|
|
113
|
+
* one overview row — keying on the expiry exploded 28 same-named packages
|
|
114
|
+
* back into 28 separate bars the moment the host started reporting real
|
|
115
|
+
* dates. The expiry travels on the group (earliest of the members) for the
|
|
116
|
+
* sort/visibility rules; the itemised per-expiry breakdown lives in the
|
|
117
|
+
* dashboard's table.
|
|
118
|
+
*
|
|
119
|
+
* @param accounts - the status document's per-package credit entries.
|
|
120
|
+
* @returns one group per distinct package NAME, in first-seen order.
|
|
121
|
+
*/
|
|
122
|
+
function mergeCreditAccounts(accounts) {
|
|
123
|
+
const groups = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const account of accounts) {
|
|
125
|
+
const key = account.packageName;
|
|
126
|
+
const existing = groups.get(key);
|
|
127
|
+
if (existing === void 0) {
|
|
128
|
+
groups.set(key, {
|
|
129
|
+
packageName: account.packageName,
|
|
130
|
+
...account.packageNameI18n === void 0 ? {} : { packageNameI18n: account.packageNameI18n },
|
|
131
|
+
packageEndTime: account.packageEndTime,
|
|
132
|
+
remain: account.remain,
|
|
133
|
+
size: account.size,
|
|
134
|
+
unlimited: account.unlimited === true
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
existing.remain += account.remain;
|
|
139
|
+
existing.size += account.size;
|
|
140
|
+
existing.unlimited = existing.unlimited || account.unlimited === true;
|
|
141
|
+
if (existing.packageEndTime !== void 0 && account.packageEndTime !== void 0) {
|
|
142
|
+
const a = Date.parse(existing.packageEndTime);
|
|
143
|
+
const b = Date.parse(account.packageEndTime);
|
|
144
|
+
if (!Number.isNaN(a) && !Number.isNaN(b) && b < a) existing.packageEndTime = account.packageEndTime;
|
|
145
|
+
} else existing.packageEndTime = existing.packageEndTime ?? account.packageEndTime;
|
|
146
|
+
}
|
|
147
|
+
return [...groups.values()];
|
|
148
|
+
}
|
|
149
|
+
/** Clamp helper shared by the card's percent math. */
|
|
150
|
+
function clampPercent(remain, size) {
|
|
151
|
+
if (!(size > 0)) return void 0;
|
|
152
|
+
const percent = remain / size * 100;
|
|
153
|
+
if (!Number.isFinite(percent)) return void 0;
|
|
154
|
+
return Math.min(100, Math.max(0, percent));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The percent format every surface prints a RATIO with: at most two decimals,
|
|
158
|
+
* with a whole value left whole — "100", never "100.00".
|
|
159
|
+
*
|
|
160
|
+
* Both cards show the SAME package, so they must print the SAME number: while
|
|
161
|
+
* the sidebar rounded to whole percent and the settings card kept one decimal,
|
|
162
|
+
* 2,291 / 2,300 read "100%" in one place and "99.6%" in the other, and the two
|
|
163
|
+
* surfaces contradicted each other about one figure.
|
|
164
|
+
*
|
|
165
|
+
* Two decimals, not a fixed two: the padding would buy no accuracy, and the
|
|
166
|
+
* sidebar's percent column is 38px wide, where "99.61%" measures 36px while
|
|
167
|
+
* "100.00%" measures 42px.
|
|
168
|
+
*
|
|
169
|
+
* @param percent - an already-clamped percentage value.
|
|
170
|
+
* @returns the percent as text, without the trailing sign.
|
|
171
|
+
*/
|
|
172
|
+
function formatPercentText(percent) {
|
|
173
|
+
return new Intl.NumberFormat(void 0, { maximumFractionDigits: 2 }).format(percent);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The same ratio read the other way round: the cycle's USED percentage as a
|
|
177
|
+
* bare number string ("3"), ready for the `quotaPackageUsed` copy.
|
|
178
|
+
*
|
|
179
|
+
* Both surfaces that print it call this ONE function — the sidebar card's label
|
|
180
|
+
* suffix and the settings card's credit bar — because they must never disagree
|
|
181
|
+
* about the same package.
|
|
182
|
+
*
|
|
183
|
+
* Unlike {@link formatPercentText}, this one stays whole percent on purpose:
|
|
184
|
+
* the suffix rides inside the package label, and at two decimals the sidebar's
|
|
185
|
+
* label measures 105px against the 93px its row can spare, which would ellipsize
|
|
186
|
+
* the very suffix it exists to show.
|
|
187
|
+
*
|
|
188
|
+
* Derived from remain/size rather than read from the upstream's per-package
|
|
189
|
+
* `used`: the transport normalizes `remaining` from `used`, so the value is the
|
|
190
|
+
* same, while a package the upstream reports without `used` normalizes to a
|
|
191
|
+
* literal 0 — adopting that field would print "已使用0%" under a half-empty bar.
|
|
192
|
+
*
|
|
193
|
+
* @param remain - the package's remaining credit.
|
|
194
|
+
* @param size - the package's granted total; a non-positive size has no ratio.
|
|
195
|
+
* @returns the used percentage as a bare number string, or undefined when there is no ratio to state.
|
|
196
|
+
*/
|
|
197
|
+
function usedPercentText(remain, size) {
|
|
198
|
+
const percent = clampPercent(remain, size);
|
|
199
|
+
return percent === void 0 ? void 0 : String(Math.round(100 - percent));
|
|
200
|
+
}
|
|
201
|
+
/** Parse an upstream expiry string ("YYYY-MM-DD HH:mm:ss") into a timestamp; undefined when unparseable. */
|
|
202
|
+
function parseExpiry(value) {
|
|
203
|
+
if (value === void 0) return void 0;
|
|
204
|
+
const parsed = Date.parse(value);
|
|
205
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
206
|
+
}
|
|
207
|
+
/** Whether a group is spent (remain 0) and NOT unlimited. */
|
|
208
|
+
function isSpent(group) {
|
|
209
|
+
return !group.unlimited && group.remain <= 0;
|
|
210
|
+
}
|
|
211
|
+
/** Whether a spent group's expiry date has already passed (undated → false: no proof of expiry). */
|
|
212
|
+
function isExpired(group, now) {
|
|
213
|
+
if (!isSpent(group)) return false;
|
|
214
|
+
const expiry = parseExpiry(group.packageEndTime);
|
|
215
|
+
return expiry !== void 0 && expiry < now;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* SIDEBAR overview rule (the user's spec, restored after it was wrongly
|
|
219
|
+
* applied to the panel):
|
|
220
|
+
*
|
|
221
|
+
* - A group with credit LEFT (remain > 0, or unlimited) renders.
|
|
222
|
+
* - A SPENT group (remain 0) renders ONLY when EVERY group is spent AND it
|
|
223
|
+
* is not expired — the account's standing quota whose emptiness is itself
|
|
224
|
+
* the news. When anything still has credit, spent rows are noise.
|
|
225
|
+
* - An EXPIRED group (spent and its expiry date has passed) renders
|
|
226
|
+
* NOWHERE, whatever the rest of the account looks like.
|
|
227
|
+
*
|
|
228
|
+
* Applied AFTER the merge so the sums are settled before the test.
|
|
229
|
+
* "Now" is injectable for tests.
|
|
230
|
+
*
|
|
231
|
+
* @param groups - merged groups, in first-seen order.
|
|
232
|
+
* @param now - current timestamp (defaults to Date.now()).
|
|
233
|
+
* @returns the groups to display, in first-seen order.
|
|
234
|
+
*/
|
|
235
|
+
function visibleQuotaGroups(groups, now = Date.now()) {
|
|
236
|
+
const hasCredit = groups.some((group) => group.unlimited || group.remain > 0);
|
|
237
|
+
return groups.filter((group) => {
|
|
238
|
+
if (group.unlimited || group.remain > 0) return true;
|
|
239
|
+
return !hasCredit && !isExpired(group, now);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* PANEL detail-table ordering (the user's spec): every row renders — the
|
|
244
|
+
* panel is the itemised ledger — but spent-yet-still-active rows sink to the
|
|
245
|
+
* BOTTOM (lowest priority), and expired rows are dropped entirely. Rows keep
|
|
246
|
+
* their first-seen order within each band.
|
|
247
|
+
*
|
|
248
|
+
* @param rows - per-package rows, unmerged, in first-seen order.
|
|
249
|
+
* @param now - current timestamp (defaults to Date.now()).
|
|
250
|
+
*/
|
|
251
|
+
function sortPackageRows(rows, now = Date.now()) {
|
|
252
|
+
const live = [];
|
|
253
|
+
const spent = [];
|
|
254
|
+
for (const row of rows) {
|
|
255
|
+
const unlimited = row.unlimited === true;
|
|
256
|
+
const expiry = parseExpiry(row.packageEndTime);
|
|
257
|
+
if (!unlimited && row.remain <= 0 && expiry !== void 0 && expiry < now) continue;
|
|
258
|
+
if (unlimited || row.remain > 0) live.push(row);
|
|
259
|
+
else spent.push(row);
|
|
260
|
+
}
|
|
261
|
+
return [...live, ...spent];
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
43
264
|
//#region src/client/status-document.ts
|
|
44
265
|
/**
|
|
45
266
|
* Whether a parsed status response really is a status document.
|
|
@@ -125,6 +346,16 @@ window.__ModuleLoader__.load({
|
|
|
125
346
|
function quotaSignInState() {
|
|
126
347
|
return signInSnapshot;
|
|
127
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Note that the label language changed and wake every subscribed surface.
|
|
351
|
+
*
|
|
352
|
+
* A package title is re-read at render, and the cards re-render on this
|
|
353
|
+
* revision — so without a bump an English-UI user who switched to Chinese kept
|
|
354
|
+
* seeing "SOTA Exclusive Credits" until the next poll.
|
|
355
|
+
*/
|
|
356
|
+
function noteQuotaLanguage() {
|
|
357
|
+
bump();
|
|
358
|
+
}
|
|
128
359
|
/** Subscribe to any flag change; returns the disposer. */
|
|
129
360
|
function onQuotaSettingsChange(listener) {
|
|
130
361
|
listeners.add(listener);
|
|
@@ -1235,9 +1466,6 @@ window.__ModuleLoader__.load({
|
|
|
1235
1466
|
function formatNumber(value) {
|
|
1236
1467
|
return new Intl.NumberFormat(void 0).format(value);
|
|
1237
1468
|
}
|
|
1238
|
-
function formatPercent(value) {
|
|
1239
|
-
return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value);
|
|
1240
|
-
}
|
|
1241
1469
|
function formatTime(ms) {
|
|
1242
1470
|
return new Intl.DateTimeFormat(void 0, {
|
|
1243
1471
|
dateStyle: "medium",
|
|
@@ -1301,13 +1529,14 @@ window.__ModuleLoader__.load({
|
|
|
1301
1529
|
size: formatNumber(size)
|
|
1302
1530
|
}) : t("creditPackageUnknownSize", { remain: formatNumber(remain) });
|
|
1303
1531
|
const percent = sizeKnown ? remain / size * 100 : void 0;
|
|
1304
|
-
const display = percent === void 0 ? t("percentUnknown") : t("percentRemaining", { percent:
|
|
1532
|
+
const display = percent === void 0 ? t("percentUnknown") : t("percentRemaining", { percent: formatPercentText(percent) });
|
|
1533
|
+
const usedPercent = usedPercentText(remain, size);
|
|
1305
1534
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1306
1535
|
style: quotaGroupStyle,
|
|
1307
1536
|
children: [
|
|
1308
1537
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1309
1538
|
style: quotaLabelStyle,
|
|
1310
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1539
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [label, usedPercent === void 0 ? null : t("quotaPackageUsed", { percent: usedPercent })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display })]
|
|
1311
1540
|
}),
|
|
1312
1541
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1313
1542
|
style: progressTrackStyle,
|
|
@@ -2443,19 +2672,26 @@ window.__ModuleLoader__.load({
|
|
|
2443
2672
|
style: tabPanelStyle,
|
|
2444
2673
|
children: [status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2445
2674
|
style: quotaListStyle,
|
|
2446
|
-
children: [
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2675
|
+
children: [
|
|
2676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2677
|
+
style: rowStyle,
|
|
2678
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2679
|
+
style: quotaTitleStyle,
|
|
2680
|
+
children: t("creditsHeading")
|
|
2681
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2682
|
+
style: bodyStyle,
|
|
2683
|
+
children: status.credits.unlimited === true ? t("creditsTotalUnlimited") : t("creditsUsed", { percent: formatPercentText(status.credits.total) })
|
|
2684
|
+
})]
|
|
2685
|
+
}),
|
|
2686
|
+
status.credits.cycleResetTime === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2687
|
+
style: descriptionStyle,
|
|
2688
|
+
children: t("cycleResetAt", { time: formatCycleReset(status.credits.cycleResetTime) })
|
|
2689
|
+
}),
|
|
2690
|
+
status.credits.cycleResetTime !== void 0 || status.pat?.savedAtMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2691
|
+
style: descriptionStyle,
|
|
2692
|
+
children: t("patAuthorizedAt", { time: formatTime(status.pat.savedAtMs) })
|
|
2693
|
+
})
|
|
2694
|
+
]
|
|
2459
2695
|
}), status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2460
2696
|
style: errorStyle,
|
|
2461
2697
|
children: t("creditsError", { message: status.creditsError })
|
|
@@ -2497,7 +2733,7 @@ window.__ModuleLoader__.load({
|
|
|
2497
2733
|
style: quotaTitleStyle,
|
|
2498
2734
|
children: t("creditsDetailHeading")
|
|
2499
2735
|
}), status.credits.accounts.filter((account) => account.remain > 0 || account.unlimited === true).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
|
|
2500
|
-
label: account
|
|
2736
|
+
label: accountDisplayName(account),
|
|
2501
2737
|
remain: account.remain,
|
|
2502
2738
|
size: account.size,
|
|
2503
2739
|
unlimited: account.unlimited,
|
|
@@ -3080,124 +3316,6 @@ window.__ModuleLoader__.load({
|
|
|
3080
3316
|
}
|
|
3081
3317
|
};
|
|
3082
3318
|
//#endregion
|
|
3083
|
-
//#region src/client/quota-merge.ts
|
|
3084
|
-
/**
|
|
3085
|
-
* Group credit accounts by (packageName, packageEndTime) and sum each group.
|
|
3086
|
-
*
|
|
3087
|
-
* A missing figure counts as 0 (the user's ruling): a package whose total the
|
|
3088
|
-
* upstream did not report contributes nothing to the group's `size` rather
|
|
3089
|
-
* than poisoning the bar into "unknown". A group whose summed `size` is still
|
|
3090
|
-
* 0 keeps an honest "unknown total" rendering in the card — the merge never
|
|
3091
|
-
* invents a denominator. `unlimited` is sticky: one unlimited member makes the
|
|
3092
|
-
* whole group unlimited, and its sums are not displayed as a quota.
|
|
3093
|
-
*
|
|
3094
|
-
* MERGE KEY IS THE NAME ALONE (the user's correction): same-named packages
|
|
3095
|
-
* whose expiries differ by seconds (stacked purchase batches) must still be
|
|
3096
|
-
* one overview row — keying on the expiry exploded 28 same-named packages
|
|
3097
|
-
* back into 28 separate bars the moment the host started reporting real
|
|
3098
|
-
* dates. The expiry travels on the group (earliest of the members) for the
|
|
3099
|
-
* sort/visibility rules; the itemised per-expiry breakdown lives in the
|
|
3100
|
-
* dashboard's table.
|
|
3101
|
-
*
|
|
3102
|
-
* @param accounts - the status document's per-package credit entries.
|
|
3103
|
-
* @returns one group per distinct package NAME, in first-seen order.
|
|
3104
|
-
*/
|
|
3105
|
-
function mergeCreditAccounts(accounts) {
|
|
3106
|
-
const groups = /* @__PURE__ */ new Map();
|
|
3107
|
-
for (const account of accounts) {
|
|
3108
|
-
const key = account.packageName;
|
|
3109
|
-
const existing = groups.get(key);
|
|
3110
|
-
if (existing === void 0) {
|
|
3111
|
-
groups.set(key, {
|
|
3112
|
-
packageName: account.packageName,
|
|
3113
|
-
packageEndTime: account.packageEndTime,
|
|
3114
|
-
remain: account.remain,
|
|
3115
|
-
size: account.size,
|
|
3116
|
-
unlimited: account.unlimited === true
|
|
3117
|
-
});
|
|
3118
|
-
continue;
|
|
3119
|
-
}
|
|
3120
|
-
existing.remain += account.remain;
|
|
3121
|
-
existing.size += account.size;
|
|
3122
|
-
existing.unlimited = existing.unlimited || account.unlimited === true;
|
|
3123
|
-
if (existing.packageEndTime !== void 0 && account.packageEndTime !== void 0) {
|
|
3124
|
-
const a = Date.parse(existing.packageEndTime);
|
|
3125
|
-
const b = Date.parse(account.packageEndTime);
|
|
3126
|
-
if (!Number.isNaN(a) && !Number.isNaN(b) && b < a) existing.packageEndTime = account.packageEndTime;
|
|
3127
|
-
} else existing.packageEndTime = existing.packageEndTime ?? account.packageEndTime;
|
|
3128
|
-
}
|
|
3129
|
-
return [...groups.values()];
|
|
3130
|
-
}
|
|
3131
|
-
/** Clamp helper shared by the card's percent math. */
|
|
3132
|
-
function clampPercent(remain, size) {
|
|
3133
|
-
if (!(size > 0)) return void 0;
|
|
3134
|
-
const percent = remain / size * 100;
|
|
3135
|
-
if (!Number.isFinite(percent)) return void 0;
|
|
3136
|
-
return Math.min(100, Math.max(0, percent));
|
|
3137
|
-
}
|
|
3138
|
-
/** Parse an upstream expiry string ("YYYY-MM-DD HH:mm:ss") into a timestamp; undefined when unparseable. */
|
|
3139
|
-
function parseExpiry(value) {
|
|
3140
|
-
if (value === void 0) return void 0;
|
|
3141
|
-
const parsed = Date.parse(value);
|
|
3142
|
-
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
3143
|
-
}
|
|
3144
|
-
/** Whether a group is spent (remain 0) and NOT unlimited. */
|
|
3145
|
-
function isSpent(group) {
|
|
3146
|
-
return !group.unlimited && group.remain <= 0;
|
|
3147
|
-
}
|
|
3148
|
-
/** Whether a spent group's expiry date has already passed (undated → false: no proof of expiry). */
|
|
3149
|
-
function isExpired(group, now) {
|
|
3150
|
-
if (!isSpent(group)) return false;
|
|
3151
|
-
const expiry = parseExpiry(group.packageEndTime);
|
|
3152
|
-
return expiry !== void 0 && expiry < now;
|
|
3153
|
-
}
|
|
3154
|
-
/**
|
|
3155
|
-
* SIDEBAR overview rule (the user's spec, restored after it was wrongly
|
|
3156
|
-
* applied to the panel):
|
|
3157
|
-
*
|
|
3158
|
-
* - A group with credit LEFT (remain > 0, or unlimited) renders.
|
|
3159
|
-
* - A SPENT group (remain 0) renders ONLY when EVERY group is spent AND it
|
|
3160
|
-
* is not expired — the account's standing quota whose emptiness is itself
|
|
3161
|
-
* the news. When anything still has credit, spent rows are noise.
|
|
3162
|
-
* - An EXPIRED group (spent and its expiry date has passed) renders
|
|
3163
|
-
* NOWHERE, whatever the rest of the account looks like.
|
|
3164
|
-
*
|
|
3165
|
-
* Applied AFTER the merge so the sums are settled before the test.
|
|
3166
|
-
* "Now" is injectable for tests.
|
|
3167
|
-
*
|
|
3168
|
-
* @param groups - merged groups, in first-seen order.
|
|
3169
|
-
* @param now - current timestamp (defaults to Date.now()).
|
|
3170
|
-
* @returns the groups to display, in first-seen order.
|
|
3171
|
-
*/
|
|
3172
|
-
function visibleQuotaGroups(groups, now = Date.now()) {
|
|
3173
|
-
const hasCredit = groups.some((group) => group.unlimited || group.remain > 0);
|
|
3174
|
-
return groups.filter((group) => {
|
|
3175
|
-
if (group.unlimited || group.remain > 0) return true;
|
|
3176
|
-
return !hasCredit && !isExpired(group, now);
|
|
3177
|
-
});
|
|
3178
|
-
}
|
|
3179
|
-
/**
|
|
3180
|
-
* PANEL detail-table ordering (the user's spec): every row renders — the
|
|
3181
|
-
* panel is the itemised ledger — but spent-yet-still-active rows sink to the
|
|
3182
|
-
* BOTTOM (lowest priority), and expired rows are dropped entirely. Rows keep
|
|
3183
|
-
* their first-seen order within each band.
|
|
3184
|
-
*
|
|
3185
|
-
* @param rows - per-package rows, unmerged, in first-seen order.
|
|
3186
|
-
* @param now - current timestamp (defaults to Date.now()).
|
|
3187
|
-
*/
|
|
3188
|
-
function sortPackageRows(rows, now = Date.now()) {
|
|
3189
|
-
const live = [];
|
|
3190
|
-
const spent = [];
|
|
3191
|
-
for (const row of rows) {
|
|
3192
|
-
const unlimited = row.unlimited === true;
|
|
3193
|
-
const expiry = parseExpiry(row.packageEndTime);
|
|
3194
|
-
if (!unlimited && row.remain <= 0 && expiry !== void 0 && expiry < now) continue;
|
|
3195
|
-
if (unlimited || row.remain > 0) live.push(row);
|
|
3196
|
-
else spent.push(row);
|
|
3197
|
-
}
|
|
3198
|
-
return [...live, ...spent];
|
|
3199
|
-
}
|
|
3200
|
-
//#endregion
|
|
3201
3319
|
//#region src/client/SidebarQuotaCard.tsx
|
|
3202
3320
|
/**
|
|
3203
3321
|
* The sidebar footer quota card + the center-column dashboard it opens.
|
|
@@ -3236,9 +3354,10 @@ window.__ModuleLoader__.load({
|
|
|
3236
3354
|
const percent = group.unlimited ? void 0 : clampPercent(group.remain, group.size);
|
|
3237
3355
|
const detail = group.unlimited ? "∞" : percent === void 0 ? `${group.remain.toLocaleString()} · ?` : `${group.remain.toLocaleString()} / ${group.size.toLocaleString()}`;
|
|
3238
3356
|
bars.push({
|
|
3239
|
-
label: group
|
|
3357
|
+
label: accountDisplayName(group),
|
|
3240
3358
|
detail,
|
|
3241
|
-
percent: percent === void 0 ? void 0 : `${
|
|
3359
|
+
percent: percent === void 0 ? void 0 : `${formatPercentText(percent)}%`,
|
|
3360
|
+
usedPercent: usedPercentText(group.remain, group.size),
|
|
3242
3361
|
barPercent: percent === void 0 ? 0 : Math.max(2, percent),
|
|
3243
3362
|
warn: !group.unlimited && percent !== void 0 && percent < 20,
|
|
3244
3363
|
packageEndTime: group.packageEndTime
|
|
@@ -3296,7 +3415,7 @@ window.__ModuleLoader__.load({
|
|
|
3296
3415
|
return sortPackageRows(accounts).map((account) => {
|
|
3297
3416
|
const percent = account.unlimited === true ? void 0 : clampPercent(account.remain, account.size);
|
|
3298
3417
|
return {
|
|
3299
|
-
name: account
|
|
3418
|
+
name: accountDisplayName(account),
|
|
3300
3419
|
remain: account.remain,
|
|
3301
3420
|
size: account.size,
|
|
3302
3421
|
percent,
|
|
@@ -3345,7 +3464,7 @@ window.__ModuleLoader__.load({
|
|
|
3345
3464
|
const fetchedAt = quotaStatusFetchedAt(variantId);
|
|
3346
3465
|
const title = [
|
|
3347
3466
|
t(nameKey),
|
|
3348
|
-
...bars.map((bar) => `${bar.label} ${bar.detail}${bar.percent === void 0 ? "" : ` (${bar.percent})`}`),
|
|
3467
|
+
...bars.map((bar) => `${bar.label} ${bar.detail}${bar.percent === void 0 ? "" : ` (${bar.percent})`}${bar.usedPercent === void 0 ? "" : ` ${t("quotaPackageUsed", { percent: bar.usedPercent })}`}`),
|
|
3349
3468
|
fetchedAt !== void 0 ? `${t("quotaUpdated")} ${timeText(fetchedAt)}` : ""
|
|
3350
3469
|
].filter((part) => part !== "").join(" · ");
|
|
3351
3470
|
if (!wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -3413,10 +3532,10 @@ window.__ModuleLoader__.load({
|
|
|
3413
3532
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3414
3533
|
className: "qdp-footHead",
|
|
3415
3534
|
children: [
|
|
3416
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3535
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3417
3536
|
className: "qdp-footLabel",
|
|
3418
|
-
title: bar.packageEndTime !== void 0 ? `${t("quotaExpires")} ${bar.packageEndTime}` : t("quotaNoExpiry"),
|
|
3419
|
-
children: bar.label
|
|
3537
|
+
title: bar.packageEndTime !== void 0 ? `${t("quotaExpires")} ${formatExpiryText(bar.packageEndTime) ?? bar.packageEndTime}` : t("quotaNoExpiry"),
|
|
3538
|
+
children: [bar.label, bar.usedPercent === void 0 ? null : t("quotaPackageUsed", { percent: bar.usedPercent })]
|
|
3420
3539
|
}),
|
|
3421
3540
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3422
3541
|
className: "qdp-footAmount",
|
|
@@ -3462,6 +3581,7 @@ window.__ModuleLoader__.load({
|
|
|
3462
3581
|
const loading = state.loading;
|
|
3463
3582
|
const fetchedAt = quotaStatusFetchedAt(activeVariant);
|
|
3464
3583
|
const credits = status !== void 0 && "credits" in status ? status.credits : void 0;
|
|
3584
|
+
const patAuthorizedAt = status !== void 0 && status.status === "signed-in" ? status.pat?.savedAtMs : void 0;
|
|
3465
3585
|
const rows = credits === void 0 ? [] : buildPackageRows(credits.accounts ?? []);
|
|
3466
3586
|
const nameKey = activeVariant === "qoder-global" ? "quotaCardGlobal" : "quotaCardCN";
|
|
3467
3587
|
const signedIn = status?.status === "signed-in";
|
|
@@ -3652,7 +3772,7 @@ window.__ModuleLoader__.load({
|
|
|
3652
3772
|
}),
|
|
3653
3773
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3654
3774
|
className: "qdp-expiry",
|
|
3655
|
-
children: row.packageEndTime ?? t("quotaNoExpiry")
|
|
3775
|
+
children: (row.packageEndTime !== void 0 ? formatExpiryText(row.packageEndTime) : void 0) ?? t("quotaNoExpiry")
|
|
3656
3776
|
})
|
|
3657
3777
|
] }, `${index}\u0000${row.name}\u0000${row.packageEndTime ?? ""}`)) })]
|
|
3658
3778
|
}),
|
|
@@ -3661,9 +3781,13 @@ window.__ModuleLoader__.load({
|
|
|
3661
3781
|
children: [
|
|
3662
3782
|
t("quotaExpires"),
|
|
3663
3783
|
" ",
|
|
3664
|
-
credits.cycleResetTime
|
|
3784
|
+
formatExpiryText(credits.cycleResetTime) ?? credits.cycleResetTime
|
|
3665
3785
|
]
|
|
3666
|
-
}) : null
|
|
3786
|
+
}) : null,
|
|
3787
|
+
patAuthorizedAt === void 0 || credits.cycleResetTime !== void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3788
|
+
className: "qdp-windowReset",
|
|
3789
|
+
children: t("patAuthorizedAt", { time: formatStampText(patAuthorizedAt) })
|
|
3790
|
+
})
|
|
3667
3791
|
] })]
|
|
3668
3792
|
})
|
|
3669
3793
|
]
|
|
@@ -3740,7 +3864,9 @@ window.__ModuleLoader__.load({
|
|
|
3740
3864
|
.qdp-footBar{display:block;background:var(--dsw-alias-bg-layer-2);border-radius:999px;height:5px;overflow:hidden}
|
|
3741
3865
|
.qdp-footFill{display:block;background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
|
|
3742
3866
|
.qdp-footFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
3743
|
-
|
|
3867
|
+
/* 38px, not 34px: two-decimal copy is wider, and "99.61%" measures 36px in this
|
|
3868
|
+
font. nowrap keeps a long value from wrapping the row into a second line. */
|
|
3869
|
+
.qdp-footPct{flex:none;width:38px;color:var(--dsw-alias-label-secondary);text-align:right;font-size:11px;line-height:16px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
3744
3870
|
|
|
3745
3871
|
/* The 56px rail: one icon button on the shell's own rail geometry (36px cell),
|
|
3746
3872
|
so the collapsed column keeps a single glyph like its siblings. */
|
|
@@ -4001,9 +4127,12 @@ window.__ModuleLoader__.load({
|
|
|
4001
4127
|
quotaTotalRemain: "Remaining",
|
|
4002
4128
|
quotaTotalShare: "{percent} of this cycle’s granted total ({remain} / {size})",
|
|
4003
4129
|
quotaCycleUsed: "Used this cycle: {percent}%",
|
|
4130
|
+
quotaPackageUsed: "(used {percent}%)",
|
|
4004
4131
|
quotaColPackage: "Package",
|
|
4005
4132
|
quotaColRemain: "Remaining / Total",
|
|
4006
|
-
quotaColExpiry: "Expires"
|
|
4133
|
+
quotaColExpiry: "Expires",
|
|
4134
|
+
/** Line stating when the PAT in effect was authorized (saved). */
|
|
4135
|
+
patAuthorizedAt: "PAT authorized {time}"
|
|
4007
4136
|
};
|
|
4008
4137
|
const zh = {
|
|
4009
4138
|
title: "Qoder(国内版)",
|
|
@@ -4173,9 +4302,11 @@ window.__ModuleLoader__.load({
|
|
|
4173
4302
|
quotaTotalRemain: "剩余额度",
|
|
4174
4303
|
quotaTotalShare: "占本轮总额度 {percent}({remain} / {size})",
|
|
4175
4304
|
quotaCycleUsed: "本轮已用 {percent}%",
|
|
4305
|
+
quotaPackageUsed: "(已使用{percent}%)",
|
|
4176
4306
|
quotaColPackage: "资源包",
|
|
4177
4307
|
quotaColRemain: "剩余 / 总量",
|
|
4178
|
-
quotaColExpiry: "到期时间"
|
|
4308
|
+
quotaColExpiry: "到期时间",
|
|
4309
|
+
patAuthorizedAt: "PAT 授权于 {time}"
|
|
4179
4310
|
};
|
|
4180
4311
|
//#endregion
|
|
4181
4312
|
//#region src/client/index.tsx
|
|
@@ -4293,6 +4424,19 @@ window.__ModuleLoader__.load({
|
|
|
4293
4424
|
en
|
|
4294
4425
|
}), "dsh-qoder-connect: settings copy");
|
|
4295
4426
|
const t = ctx.locale.bind(namespace);
|
|
4427
|
+
const applyLanguage = (localeId) => {
|
|
4428
|
+
setActiveLanguage(localeId);
|
|
4429
|
+
noteQuotaLanguage();
|
|
4430
|
+
};
|
|
4431
|
+
applyLanguage(ctx.locale.getLocale().active);
|
|
4432
|
+
ctx.effect(() => {
|
|
4433
|
+
const dispose = ctx.on("locale/change", (snapshot) => {
|
|
4434
|
+
applyLanguage(snapshot.active);
|
|
4435
|
+
});
|
|
4436
|
+
return () => {
|
|
4437
|
+
dispose();
|
|
4438
|
+
};
|
|
4439
|
+
}, "dsh-qoder-connect: package title language");
|
|
4296
4440
|
/**
|
|
4297
4441
|
* The bound quota settings face, filled in as soon as a host configuration
|
|
4298
4442
|
* service becomes available (see the two `ctx.inject` callbacks below).
|
package/lib/index.d.ts
CHANGED
|
@@ -17,9 +17,12 @@ import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
|
17
17
|
* seed — writes there, so a profile directory never collects loose
|
|
18
18
|
* `.qoder-*` files and the whole plugin's footprint is one folder.
|
|
19
19
|
*
|
|
20
|
-
* Fallbacks, in order: `DSH_QODER_DATA_DIR` env override → the
|
|
21
|
-
* profile → the Harness home (a checkout
|
|
22
|
-
* loading the plugin from outside any profile).
|
|
20
|
+
* Fallbacks, in order: `DSH_QODER_DATA_DIR` env override → the profile the host
|
|
21
|
+
* reports as running → the discovered profile → the Harness home (a checkout
|
|
22
|
+
* running its own tests, or a host loading the plugin from outside any profile).
|
|
23
|
+
*
|
|
24
|
+
* The host-reported profile outranks discovery because it is the running
|
|
25
|
+
* profile rather than an inference about it; see {@link configurePluginPaths}.
|
|
23
26
|
*
|
|
24
27
|
* Split out of `auth.ts` so catalog/probe/heartbeat stores can import the
|
|
25
28
|
* directory without pulling in the credential code (and its upstream
|
|
@@ -34,10 +37,10 @@ declare const QODER_DATA_DIR_ENV = "DSH_QODER_DATA_DIR";
|
|
|
34
37
|
/**
|
|
35
38
|
* The plugin's data directory: `<profile>/.dsh-qoder-connect`.
|
|
36
39
|
*
|
|
37
|
-
* Falls back to the Harness home when no profile can
|
|
38
|
-
* checkout running its own tests, or a host that loads the
|
|
39
|
-
* outside a profile — so the plugin always has somewhere to write,
|
|
40
|
-
* `DSH_QODER_DATA_DIR` overrides either way.
|
|
40
|
+
* Falls back to profile discovery, then to the Harness home when no profile can
|
|
41
|
+
* be determined — a checkout running its own tests, or a host that loads the
|
|
42
|
+
* plugin from outside a profile — so the plugin always has somewhere to write,
|
|
43
|
+
* and `DSH_QODER_DATA_DIR` overrides either way.
|
|
41
44
|
*/
|
|
42
45
|
declare function qoderPluginDataDir(): string;
|
|
43
46
|
//#endregion
|
|
@@ -181,9 +184,23 @@ interface QoderWebCreditAccount {
|
|
|
181
184
|
* When the package's credit expires, carried through in whatever form the
|
|
182
185
|
* upstream reported it (an ISO string or a numeric epoch-millisecond value
|
|
183
186
|
* normalized to an ISO string). Absent when the upstream reported no
|
|
184
|
-
* expiry — rendered as "no expiry", never guessed into a date.
|
|
187
|
+
* expiry — rendered as "no expiry", never guessed into a date. The
|
|
188
|
+
* year-9999 "never" sentinel the upstream uses for an uncapped allowance is
|
|
189
|
+
* dropped at the transport, so it never arrives here as a date.
|
|
185
190
|
*/
|
|
186
191
|
packageEndTime?: string;
|
|
192
|
+
/**
|
|
193
|
+
* The package's localized display titles, when the upstream named it (a
|
|
194
|
+
* campaign grant's `SOTA 专属积分` / `SOTA Exclusive Credits`). `packageName`
|
|
195
|
+
* remains the best single-language guess for renderers that do not consult
|
|
196
|
+
* this map.
|
|
197
|
+
*/
|
|
198
|
+
packageNameI18n?: {
|
|
199
|
+
zh?: string | undefined;
|
|
200
|
+
en?: string | undefined;
|
|
201
|
+
};
|
|
202
|
+
/** The opaque upstream campaign code behind the same grant, when known. */
|
|
203
|
+
packageCode?: string;
|
|
187
204
|
}
|
|
188
205
|
/** Aggregated credit answer rendered by the plugin card. */
|
|
189
206
|
interface QoderWebCredits {
|
|
@@ -686,6 +703,38 @@ interface QoderQuota {
|
|
|
686
703
|
percentage: number;
|
|
687
704
|
unit: string;
|
|
688
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* A named grant of credits that expires on its own schedule.
|
|
708
|
+
*
|
|
709
|
+
* The international upstream reports these in `dedicatedResourcePackages`: one
|
|
710
|
+
* entry per campaign package (the SOTA exclusive credits, for one). Every
|
|
711
|
+
* bucket the reader already knows (`userQuota`, `orgResourcePackage`,
|
|
712
|
+
* `addOnQuota`) shares the single cycle-level `expiresAt`, whereas a dedicated
|
|
713
|
+
* package carries its OWN `expiresAt` and its own localized title — so it is
|
|
714
|
+
* its own shape, not just another `QoderQuota`.
|
|
715
|
+
*/
|
|
716
|
+
interface QoderDedicatedResourcePackage extends QoderQuota {
|
|
717
|
+
/** Upstream package id (`01a1426f-…`), when reported. */
|
|
718
|
+
id?: string | undefined;
|
|
719
|
+
/** Raw upstream name — usually an opaque campaign code, not display copy. */
|
|
720
|
+
name?: string | undefined;
|
|
721
|
+
/** Localized display title from `displayLabels`, when the upstream sends one. */
|
|
722
|
+
title?: {
|
|
723
|
+
zh?: string | undefined;
|
|
724
|
+
en?: string | undefined;
|
|
725
|
+
} | undefined;
|
|
726
|
+
/**
|
|
727
|
+
* This package's own expiry, ISO. Deliberately NOT the account's cycle-level
|
|
728
|
+
* `expiresAt`: a campaign grant ends when the grant ends, and reporting the
|
|
729
|
+
* account's (sentinel `9999`) date against it is what made the card's dates
|
|
730
|
+
* wrong.
|
|
731
|
+
*/
|
|
732
|
+
expiresAt?: string | undefined;
|
|
733
|
+
/** Whether the upstream says the package is currently usable. */
|
|
734
|
+
available?: boolean | undefined;
|
|
735
|
+
/** Upstream lifecycle marker (`QUOTA_DETAIL_STATUS_ACTIVE`). */
|
|
736
|
+
status?: string | undefined;
|
|
737
|
+
}
|
|
689
738
|
interface QoderQuotaUsage {
|
|
690
739
|
userQuota?: QoderQuota | undefined;
|
|
691
740
|
orgResourcePackage?: QoderQuota | undefined;
|
|
@@ -697,6 +746,15 @@ interface QoderQuotaUsage {
|
|
|
697
746
|
* than the account actually holds (two on the web, one here).
|
|
698
747
|
*/
|
|
699
748
|
addOnQuota?: QoderQuota | undefined;
|
|
749
|
+
/**
|
|
750
|
+
* Campaign grants with their own expiry and display title (the international
|
|
751
|
+
* edition's SOTA exclusive credits land here).
|
|
752
|
+
*
|
|
753
|
+
* Dropping this array hid a whole 2,000-credit package from the card: the
|
|
754
|
+
* reader mapped only the three named buckets above, so the credit was real
|
|
755
|
+
* and simply never rendered.
|
|
756
|
+
*/
|
|
757
|
+
dedicatedResourcePackages?: readonly QoderDedicatedResourcePackage[] | undefined;
|
|
700
758
|
totalUsagePercentage?: number | undefined;
|
|
701
759
|
isQuotaExceeded?: boolean | undefined;
|
|
702
760
|
expiresAt?: string | undefined;
|
|
@@ -804,8 +862,25 @@ interface QoderCreditAccount {
|
|
|
804
862
|
remain: number;
|
|
805
863
|
size: number;
|
|
806
864
|
unlimited?: true;
|
|
807
|
-
/**
|
|
865
|
+
/**
|
|
866
|
+
* Expiry of this package, verbatim from the upstream (ISO string).
|
|
867
|
+
*
|
|
868
|
+
* Absent when the upstream reported no expiry — which now includes the
|
|
869
|
+
* year-9999 "never" sentinel the transport drops rather than forwarding.
|
|
870
|
+
*/
|
|
808
871
|
packageEndTime?: string;
|
|
872
|
+
/**
|
|
873
|
+
* The package's own localized titles, when the upstream sent a real display
|
|
874
|
+
* name (a campaign grant's `SOTA 专属积分`). The renderer picks the active
|
|
875
|
+
* language; `packageName` stays the single-language best guess it falls back
|
|
876
|
+
* to, so a card that never consults this map still shows a sensible label.
|
|
877
|
+
*/
|
|
878
|
+
packageNameI18n?: {
|
|
879
|
+
zh?: string | undefined;
|
|
880
|
+
en?: string | undefined;
|
|
881
|
+
};
|
|
882
|
+
/** Opaque upstream code for the same grant (`act-20261014-129`), when known. */
|
|
883
|
+
packageCode?: string;
|
|
809
884
|
}
|
|
810
885
|
/** The aggregated credit answer the card renders. */
|
|
811
886
|
interface QoderCredits {
|
|
@@ -915,10 +990,17 @@ declare class QoderUpstreamClient {
|
|
|
915
990
|
/**
|
|
916
991
|
* The subscriber's quota answer, mapped onto the card's credit shape.
|
|
917
992
|
*
|
|
918
|
-
* Qoder reports one personal quota
|
|
919
|
-
* package
|
|
920
|
-
*
|
|
921
|
-
*
|
|
993
|
+
* Qoder reports one personal quota, an optional organization resource
|
|
994
|
+
* package, bonus credits, and — on the international edition — a list of
|
|
995
|
+
* campaign grants under `dedicatedResourcePackages`. Each becomes a package
|
|
996
|
+
* row the card already knows how to draw. An unbounded personal allowance
|
|
997
|
+
* (`total === 0`, not exceeded) is the account's "unlimited" state.
|
|
998
|
+
*
|
|
999
|
+
* A campaign grant carries its OWN expiry and its OWN localized title, and
|
|
1000
|
+
* neither may be borrowed from the account: `usage.expiresAt` is the
|
|
1001
|
+
* year-9999 "never" sentinel for a personal account, while the grant really
|
|
1002
|
+
* does end, and `name` is an opaque campaign code while the readable copy
|
|
1003
|
+
* lives in `displayLabels`.
|
|
922
1004
|
*/
|
|
923
1005
|
fetchCredits(signal?: AbortSignal): Promise<QoderCredits>;
|
|
924
1006
|
/**
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as QoderCatalog, B as QODER_PAT_ENV_GLOBAL, C as getMachineId, D as QoderCatalogStore, E as randomSentinel, F as QODER_PROBE_PATH, G as QODER_DATA_DIR_ENV, H as patTail, I as QODER_SETTINGS_FACE_PATH, J as
|
|
1
|
+
import { A as QoderCatalog, B as QODER_PAT_ENV_GLOBAL, C as getMachineId, D as QoderCatalogStore, E as randomSentinel, F as QODER_PROBE_PATH, G as QODER_DATA_DIR_ENV, H as patTail, I as QODER_SETTINGS_FACE_PATH, J as qoderMachineIdPath, K as QODER_DATA_DIR_NAME, L as QODER_STATUS_PATH, M as QODER_GLOBAL_AUTH_PATH, N as QODER_GLOBAL_PROBE_PATH, O as qoderCatalogPath, P as QODER_GLOBAL_STATUS_PATH, R as QODER_AUTH_FILENAME, S as utc8WallClock, T as probeModel, U as qoderCredentialIdentity, V as QoderCredentialStore, W as qoderOwnAuthPath, X as qoderStateDir, Y as qoderPluginDataDir, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_AUTH_PATH, k as FALLBACK_QODER_MODELS, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, q as configurePluginPaths, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as PROBE_EFFORT_CANDIDATES, x as formatUtc8Date, y as validateApiKey, z as QODER_PAT_ENV_CN } from "./variants-Dus_Yq3R.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -2541,6 +2541,11 @@ const lastLiveFetchAt = /* @__PURE__ */ new Map();
|
|
|
2541
2541
|
* without re-registering the provider.
|
|
2542
2542
|
*/
|
|
2543
2543
|
function apply(ctx, config) {
|
|
2544
|
+
const profileContext = ctx.get("profileContext");
|
|
2545
|
+
configurePluginPaths({
|
|
2546
|
+
profileDir: typeof profileContext?.dir === "string" ? profileContext.dir : void 0,
|
|
2547
|
+
diagnostics: ctx.logger
|
|
2548
|
+
});
|
|
2544
2549
|
const store = new SettingsStore();
|
|
2545
2550
|
let current = () => ({
|
|
2546
2551
|
...readConfig(config),
|
|
@@ -2,7 +2,7 @@ import crypto, { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
8
8
|
import { EMPTY_RESPONSE_CODE, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage, createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -21,9 +21,12 @@ import { execFileSync } from "node:child_process";
|
|
|
21
21
|
* seed — writes there, so a profile directory never collects loose
|
|
22
22
|
* `.qoder-*` files and the whole plugin's footprint is one folder.
|
|
23
23
|
*
|
|
24
|
-
* Fallbacks, in order: `DSH_QODER_DATA_DIR` env override → the
|
|
25
|
-
* profile → the Harness home (a checkout
|
|
26
|
-
* loading the plugin from outside any profile).
|
|
24
|
+
* Fallbacks, in order: `DSH_QODER_DATA_DIR` env override → the profile the host
|
|
25
|
+
* reports as running → the discovered profile → the Harness home (a checkout
|
|
26
|
+
* running its own tests, or a host loading the plugin from outside any profile).
|
|
27
|
+
*
|
|
28
|
+
* The host-reported profile outranks discovery because it is the running
|
|
29
|
+
* profile rather than an inference about it; see {@link configurePluginPaths}.
|
|
27
30
|
*
|
|
28
31
|
* Split out of `auth.ts` so catalog/probe/heartbeat stores can import the
|
|
29
32
|
* directory without pulling in the credential code (and its upstream
|
|
@@ -40,6 +43,30 @@ const QODER_DATA_DIR_ENV = "DSH_QODER_DATA_DIR";
|
|
|
40
43
|
const PROFILES_DIR_NAME = "profiles";
|
|
41
44
|
/** The npm name of this package, as a profile's manifest declares it. */
|
|
42
45
|
const PLUGIN_PACKAGE_NAME = "dsh-qoder-connect";
|
|
46
|
+
/** Host-reported facts about the running profile; see {@link configurePluginPaths}. */
|
|
47
|
+
let hostProfileDir;
|
|
48
|
+
let diagnostics;
|
|
49
|
+
/**
|
|
50
|
+
* Adopt the host's answer for which profile is running, before any path is
|
|
51
|
+
* resolved.
|
|
52
|
+
*
|
|
53
|
+
* `dsh` provides a `profileContext` service (`{ name, dir, … }`) on the host
|
|
54
|
+
* context of every profile it boots — the CLI launcher and the desktop
|
|
55
|
+
* application both, from the same shared boot code — so `dir` is the directory
|
|
56
|
+
* this process booted in, not a guess about it.
|
|
57
|
+
*
|
|
58
|
+
* Called on every `apply()`, which overwrites both fields: a reload arriving
|
|
59
|
+
* without a profile context clears the previous answer instead of keeping it.
|
|
60
|
+
*
|
|
61
|
+
* @param host - the host's report.
|
|
62
|
+
* @param host.profileDir - `profileContext.dir`, when the host has one.
|
|
63
|
+
* @param host.diagnostics - where an ambiguous discovery fallback is reported.
|
|
64
|
+
*/
|
|
65
|
+
function configurePluginPaths(host) {
|
|
66
|
+
const reported = host.profileDir;
|
|
67
|
+
hostProfileDir = typeof reported === "string" && reported.trim() !== "" ? reported : void 0;
|
|
68
|
+
diagnostics = host.diagnostics;
|
|
69
|
+
}
|
|
43
70
|
function pluginPackageRoot() {
|
|
44
71
|
try {
|
|
45
72
|
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -66,15 +93,40 @@ function profileDeclaresPlugin(profileDir) {
|
|
|
66
93
|
}
|
|
67
94
|
}
|
|
68
95
|
/**
|
|
96
|
+
* Whether a profile's OWN `node_modules` is a link rather than a directory.
|
|
97
|
+
*
|
|
98
|
+
* This is the shape a residual directory takes when it was left behind by
|
|
99
|
+
* copying another profile's manifest and pointing the whole tree at that
|
|
100
|
+
* profile. A real install links the package (`node_modules/<name>`), not the
|
|
101
|
+
* directory holding it, so this shape never identifies the running copy.
|
|
102
|
+
*
|
|
103
|
+
* An ABSENT `node_modules` is deliberately not this shape: a profile whose
|
|
104
|
+
* packages resolve from the shared `profiles/node_modules` has none of its own,
|
|
105
|
+
* and that profile may still be the one running.
|
|
106
|
+
*/
|
|
107
|
+
function profileNodeModulesIsLinked(profileDir) {
|
|
108
|
+
try {
|
|
109
|
+
return lstatSync(join(profileDir, "node_modules")).isSymbolicLink();
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
69
115
|
* Whether a profile's installed copy of this plugin resolves to this package.
|
|
70
116
|
*
|
|
71
117
|
* This is what separates two profiles that both declare the plugin — a `web` and
|
|
72
118
|
* a `desktop` profile can each list it — so the data directory follows the
|
|
73
119
|
* profile whose copy is actually running rather than the first one found.
|
|
120
|
+
*
|
|
121
|
+
* A whole-tree link is rejected first (see {@link profileNodeModulesIsLinked}):
|
|
122
|
+
* the realpath comparison cannot tell that shape apart from a real installation,
|
|
123
|
+
* so a residual directory would claim this package just as convincingly as the
|
|
124
|
+
* live copy.
|
|
74
125
|
*/
|
|
75
126
|
function profileLinksToThisPackage(profileDir) {
|
|
76
127
|
const own = pluginPackageRoot();
|
|
77
128
|
if (own === void 0) return false;
|
|
129
|
+
if (profileNodeModulesIsLinked(profileDir)) return false;
|
|
78
130
|
try {
|
|
79
131
|
return realpathSync(join(profileDir, "node_modules", PLUGIN_PACKAGE_NAME)) === realpathSync(own);
|
|
80
132
|
} catch {
|
|
@@ -85,8 +137,17 @@ function profileLinksToThisPackage(profileDir) {
|
|
|
85
137
|
* The profile directory this plugin belongs to, or undefined when none can be
|
|
86
138
|
* determined.
|
|
87
139
|
*
|
|
88
|
-
* A single declaring profile is accepted without the link test, so a
|
|
89
|
-
*
|
|
140
|
+
* A single declaring profile is accepted without the link test, so a profile
|
|
141
|
+
* whose packages resolve from the shared `profiles/node_modules` — which has no
|
|
142
|
+
* copy of its own to compare — still resolves. A whole-tree link is refused even
|
|
143
|
+
* then: that shape is a residual directory whatever else is true, and there is
|
|
144
|
+
* nothing else it could be.
|
|
145
|
+
*
|
|
146
|
+
* With several declaring profiles only those whose copy resolves here are
|
|
147
|
+
* eligible. Exactly one eligible candidate is an answer; two or more is a tie
|
|
148
|
+
* broken by `readdirSync` order, which Node leaves undefined — so both that and
|
|
149
|
+
* "nothing eligible" are reported, because either way the data directory is
|
|
150
|
+
* about to leave the profile the user expects and nothing else would say so.
|
|
90
151
|
*/
|
|
91
152
|
function discoverProfileDir() {
|
|
92
153
|
const profilesRoot = join(resolveDshHome(), PROFILES_DIR_NAME);
|
|
@@ -103,21 +164,34 @@ function discoverProfileDir() {
|
|
|
103
164
|
if (profileDeclaresPlugin(dir)) candidates.push(dir);
|
|
104
165
|
}
|
|
105
166
|
if (candidates.length === 0) return void 0;
|
|
106
|
-
if (candidates.length === 1)
|
|
107
|
-
|
|
167
|
+
if (candidates.length === 1) {
|
|
168
|
+
const only = candidates[0];
|
|
169
|
+
if (profileNodeModulesIsLinked(only)) {
|
|
170
|
+
diagnostics?.warn(`qoder: the only profile declaring this plugin, ${only}, links its entire node_modules elsewhere and so cannot be the running copy; the data directory falls back to the Harness home.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
return only;
|
|
174
|
+
}
|
|
175
|
+
const eligible = candidates.filter((candidate) => profileLinksToThisPackage(candidate));
|
|
176
|
+
if (eligible.length === 0) {
|
|
177
|
+
diagnostics?.warn(`qoder: ${candidates.length} profiles declare this plugin, none holds the running copy, and the host reported no running profile; the data directory falls back to the Harness home. Candidates: ${candidates.join(", ")}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (eligible.length > 1) diagnostics?.warn(`qoder: ${eligible.length} profiles hold a copy resolving to this package and the host reported no running profile; the data directory follows ${eligible[0]} on nothing but directory order. Candidates: ${eligible.join(", ")}`);
|
|
181
|
+
return eligible[0];
|
|
108
182
|
}
|
|
109
183
|
/**
|
|
110
184
|
* The plugin's data directory: `<profile>/.dsh-qoder-connect`.
|
|
111
185
|
*
|
|
112
|
-
* Falls back to the Harness home when no profile can
|
|
113
|
-
* checkout running its own tests, or a host that loads the
|
|
114
|
-
* outside a profile — so the plugin always has somewhere to write,
|
|
115
|
-
* `DSH_QODER_DATA_DIR` overrides either way.
|
|
186
|
+
* Falls back to profile discovery, then to the Harness home when no profile can
|
|
187
|
+
* be determined — a checkout running its own tests, or a host that loads the
|
|
188
|
+
* plugin from outside a profile — so the plugin always has somewhere to write,
|
|
189
|
+
* and `DSH_QODER_DATA_DIR` overrides either way.
|
|
116
190
|
*/
|
|
117
191
|
function qoderPluginDataDir() {
|
|
118
192
|
const override = process.env[QODER_DATA_DIR_ENV];
|
|
119
193
|
if (override !== void 0 && override.trim() !== "") return override;
|
|
120
|
-
const base = discoverProfileDir() ?? resolveDshHome();
|
|
194
|
+
const base = hostProfileDir ?? discoverProfileDir() ?? resolveDshHome();
|
|
121
195
|
return join(base, QODER_DATA_DIR_NAME);
|
|
122
196
|
}
|
|
123
197
|
/**
|
|
@@ -2505,12 +2579,77 @@ function normalizeQuota(raw) {
|
|
|
2505
2579
|
}
|
|
2506
2580
|
function normalizeExpiresAt(rawExpires) {
|
|
2507
2581
|
if (rawExpires === void 0 || rawExpires === null) return void 0;
|
|
2508
|
-
if (typeof rawExpires === "number" && rawExpires > 0) return
|
|
2582
|
+
if (typeof rawExpires === "number" && rawExpires > 0) return toExpiryIso(rawExpires);
|
|
2509
2583
|
if (typeof rawExpires === "string" && rawExpires.length > 0) {
|
|
2510
2584
|
const parsed = Date.parse(rawExpires);
|
|
2511
|
-
if (!Number.isNaN(parsed) && parsed > 0) return
|
|
2585
|
+
if (!Number.isNaN(parsed) && parsed > 0) return toExpiryIso(parsed);
|
|
2512
2586
|
}
|
|
2513
2587
|
}
|
|
2588
|
+
/**
|
|
2589
|
+
* The year at which Qoder stops meaning a date and starts meaning "never".
|
|
2590
|
+
*
|
|
2591
|
+
* The upstream encodes an uncapped allowance as the year-9999 sentinel
|
|
2592
|
+
* `253402214400000`, which is a valid timestamp and a real date to any parser.
|
|
2593
|
+
* Rendering it as the account's expiry told the user the quota resets in the
|
|
2594
|
+
* year 10000, so the sentinel must be read as the absence of an expiry.
|
|
2595
|
+
*/
|
|
2596
|
+
const NEVER_EXPIRY_YEAR = 9999;
|
|
2597
|
+
/** ISO instant for a parsed expiry, or undefined for the "never" sentinel. */
|
|
2598
|
+
function toExpiryIso(atMs) {
|
|
2599
|
+
return new Date(atMs).getUTCFullYear() >= NEVER_EXPIRY_YEAR ? void 0 : new Date(atMs).toISOString();
|
|
2600
|
+
}
|
|
2601
|
+
/** Localized `displayLabels` values, keyed by bare language (`zh` / `en`). */
|
|
2602
|
+
function labelI18n(raw) {
|
|
2603
|
+
const out = {};
|
|
2604
|
+
if (!raw || typeof raw !== "object") return out;
|
|
2605
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2606
|
+
const text = asString(value);
|
|
2607
|
+
if (text === void 0) continue;
|
|
2608
|
+
const language = key.slice(0, 2).toLowerCase();
|
|
2609
|
+
if (language === "zh") out.zh = text;
|
|
2610
|
+
else if (language === "en") out.en = text;
|
|
2611
|
+
}
|
|
2612
|
+
return out;
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* One campaign grant, including the title the upstream localized for it.
|
|
2616
|
+
*
|
|
2617
|
+
* `name` is an opaque code (`act-20261014-129`) — the human copy lives in
|
|
2618
|
+
* `displayLabels` under `dimension: 'title'`, so that is what the row is
|
|
2619
|
+
* labelled with and the code is kept only as provenance.
|
|
2620
|
+
*/
|
|
2621
|
+
function normalizeDedicatedPackage(raw) {
|
|
2622
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
2623
|
+
const quota = normalizeQuota(raw);
|
|
2624
|
+
if (quota === void 0) return void 0;
|
|
2625
|
+
const title = (Array.isArray(raw.displayLabels) ? raw.displayLabels : []).find((label) => label?.dimension === "title");
|
|
2626
|
+
const id = asString(raw.id);
|
|
2627
|
+
const name = asString(raw.name);
|
|
2628
|
+
const status = asString(raw.status);
|
|
2629
|
+
const expiresAt = normalizeExpiresAt(raw.expiresAt);
|
|
2630
|
+
return {
|
|
2631
|
+
...quota,
|
|
2632
|
+
...id === void 0 ? {} : { id },
|
|
2633
|
+
...name === void 0 ? {} : { name },
|
|
2634
|
+
...title === void 0 ? {} : (() => {
|
|
2635
|
+
const parsed = labelI18n(title.valueI18n);
|
|
2636
|
+
const fallback = asString(title.value);
|
|
2637
|
+
if (parsed.zh === void 0 && parsed.en === void 0 && fallback === void 0) return {};
|
|
2638
|
+
return { title: {
|
|
2639
|
+
...parsed.zh !== void 0 ? { zh: parsed.zh } : fallback !== void 0 ? { zh: fallback } : {},
|
|
2640
|
+
...parsed.en !== void 0 ? { en: parsed.en } : fallback !== void 0 ? { en: fallback } : {}
|
|
2641
|
+
} };
|
|
2642
|
+
})(),
|
|
2643
|
+
...expiresAt === void 0 ? {} : { expiresAt },
|
|
2644
|
+
...typeof raw.available === "boolean" ? { available: raw.available } : {},
|
|
2645
|
+
...status === void 0 ? {} : { status }
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
function normalizeDedicatedPackages(raw) {
|
|
2649
|
+
if (!Array.isArray(raw)) return void 0;
|
|
2650
|
+
const packages = raw.map((entry) => normalizeDedicatedPackage(entry)).filter((entry) => entry !== void 0);
|
|
2651
|
+
return packages.length > 0 ? packages : void 0;
|
|
2652
|
+
}
|
|
2514
2653
|
function asString(value) {
|
|
2515
2654
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
2516
2655
|
}
|
|
@@ -2670,6 +2809,10 @@ var QoderUsageReader = class {
|
|
|
2670
2809
|
userQuota: normalizeQuota(data.userQuota),
|
|
2671
2810
|
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
2672
2811
|
addOnQuota: normalizeQuota(data.addOnQuota),
|
|
2812
|
+
...(() => {
|
|
2813
|
+
const packages = normalizeDedicatedPackages(data.dedicatedResourcePackages);
|
|
2814
|
+
return packages === void 0 ? {} : { dedicatedResourcePackages: packages };
|
|
2815
|
+
})(),
|
|
2673
2816
|
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
2674
2817
|
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
2675
2818
|
expiresAt: normalizeExpiresAt(data.expiresAt),
|
|
@@ -4365,10 +4508,17 @@ var QoderUpstreamClient = class {
|
|
|
4365
4508
|
/**
|
|
4366
4509
|
* The subscriber's quota answer, mapped onto the card's credit shape.
|
|
4367
4510
|
*
|
|
4368
|
-
* Qoder reports one personal quota
|
|
4369
|
-
* package
|
|
4370
|
-
*
|
|
4371
|
-
*
|
|
4511
|
+
* Qoder reports one personal quota, an optional organization resource
|
|
4512
|
+
* package, bonus credits, and — on the international edition — a list of
|
|
4513
|
+
* campaign grants under `dedicatedResourcePackages`. Each becomes a package
|
|
4514
|
+
* row the card already knows how to draw. An unbounded personal allowance
|
|
4515
|
+
* (`total === 0`, not exceeded) is the account's "unlimited" state.
|
|
4516
|
+
*
|
|
4517
|
+
* A campaign grant carries its OWN expiry and its OWN localized title, and
|
|
4518
|
+
* neither may be borrowed from the account: `usage.expiresAt` is the
|
|
4519
|
+
* year-9999 "never" sentinel for a personal account, while the grant really
|
|
4520
|
+
* does end, and `name` is an opaque campaign code while the readable copy
|
|
4521
|
+
* lives in `displayLabels`.
|
|
4372
4522
|
*/
|
|
4373
4523
|
async fetchCredits(signal) {
|
|
4374
4524
|
const usage = (await this.transport.readAccount({
|
|
@@ -4397,6 +4547,18 @@ var QoderUpstreamClient = class {
|
|
|
4397
4547
|
size: addOn.total,
|
|
4398
4548
|
...usage.expiresAt === void 0 ? {} : { packageEndTime: usage.expiresAt }
|
|
4399
4549
|
});
|
|
4550
|
+
for (const pack of usage.dedicatedResourcePackages ?? []) {
|
|
4551
|
+
const titles = pack.title;
|
|
4552
|
+
const fallbackName = titles?.zh ?? titles?.en ?? pack.name ?? "专属资源包";
|
|
4553
|
+
accounts.push({
|
|
4554
|
+
packageName: fallbackName,
|
|
4555
|
+
...titles === void 0 ? {} : { packageNameI18n: titles },
|
|
4556
|
+
...pack.name === void 0 ? {} : { packageCode: pack.name },
|
|
4557
|
+
remain: pack.remaining,
|
|
4558
|
+
size: pack.total,
|
|
4559
|
+
...pack.expiresAt === void 0 ? {} : { packageEndTime: pack.expiresAt }
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4400
4562
|
const unlimited = usage.isQuotaExceeded === false && personal !== void 0 && personal.total === 0;
|
|
4401
4563
|
return {
|
|
4402
4564
|
total: usage.totalUsagePercentage ?? personal?.percentage ?? 0,
|
|
@@ -4662,7 +4824,7 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
4662
4824
|
const DEFAULT_CONTEXT_WINDOW = 18e4;
|
|
4663
4825
|
//#endregion
|
|
4664
4826
|
//#region src/version.ts
|
|
4665
|
-
const QODER_CONNECT_VERSION = "0.2.
|
|
4827
|
+
const QODER_CONNECT_VERSION = "0.2.5";
|
|
4666
4828
|
//#endregion
|
|
4667
4829
|
//#region src/host-heartbeat.ts
|
|
4668
4830
|
/**
|
|
@@ -4859,4 +5021,4 @@ function variantFor(id) {
|
|
|
4859
5021
|
return QODER_VARIANTS.find((variant) => variant.id === id);
|
|
4860
5022
|
}
|
|
4861
5023
|
//#endregion
|
|
4862
|
-
export { QoderCatalog as A, QODER_PAT_ENV_GLOBAL as B, getMachineId as C, QoderCatalogStore as D, randomSentinel as E, QODER_PROBE_PATH as F, QODER_DATA_DIR_ENV as G, patTail as H, QODER_SETTINGS_FACE_PATH as I,
|
|
5024
|
+
export { QoderCatalog as A, QODER_PAT_ENV_GLOBAL as B, getMachineId as C, QoderCatalogStore as D, randomSentinel as E, QODER_PROBE_PATH as F, QODER_DATA_DIR_ENV as G, patTail as H, QODER_SETTINGS_FACE_PATH as I, qoderMachineIdPath as J, QODER_DATA_DIR_NAME as K, QODER_STATUS_PATH as L, QODER_GLOBAL_AUTH_PATH as M, QODER_GLOBAL_PROBE_PATH as N, qoderCatalogPath as O, QODER_GLOBAL_STATUS_PATH as P, QODER_AUTH_FILENAME as R, utc8WallClock as S, probeModel as T, qoderCredentialIdentity as U, QoderCredentialStore as V, qoderOwnAuthPath as W, qoderStateDir as X, qoderPluginDataDir as Y, modelInfoOf as _, QODER_HOST_HEARTBEAT_FILENAME as a, createQoderTransport as b, processStartTimeMs as c, writeHostHeartbeat as d, QODER_CONNECT_VERSION as f, kindFromQoderFailure as g, classifyUpstreamError as h, variantFor as i, QODER_AUTH_PATH as j, FALLBACK_QODER_MODELS as k, qoderHostHeartbeatPath as l, QoderUpstreamClient as m, GLOBAL_VARIANT as n, clearHostHeartbeat as o, KIND_STATUS as p, configurePluginPaths as q, QODER_VARIANTS as r, isHeartbeatProcessAlive as s, CHINA_VARIANT as t, readHostHeartbeat as u, normalizeCredits as v, PROBE_EFFORT_CANDIDATES as w, formatUtc8Date as x, validateApiKey as y, QODER_PAT_ENV_CN as z };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-qoder-connect",
|
|
3
3
|
"displayName": "DSH Qoder Connect",
|
|
4
4
|
"description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.5",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|